Skip to content

fix(generator): add defensive checks for cross-package type alias namespace resolution - #4931

Closed
ddmoney420 wants to merge 4 commits into
wailsapp:v3-alphafrom
ddmoney420:fix-issue-4578
Closed

ddmoney420 wants to merge 4 commits into
wailsapp:v3-alphafrom
ddmoney420:fix-issue-4578

Conversation

@ddmoney420

@ddmoney420 ddmoney420 commented Jan 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds defensive checks in the TypeScript bindings generator to ensure external package imports are properly resolved when handling type aliases across packages.

Fixes #4578

Problem

When a Go type alias from package A points to a type in package B, the TypeScript bindings generator could produce incorrect namespace references like $0.Model.createFrom instead of data_models$0.Model.createFrom.

This occurs when m.Imports.External[pkgPath] returns a zero-value ImportInfo{Name: "", Index: 0} because the package path was not properly registered in the imports map.

Solution

Added defensive checks in three files to ensure external package imports exist before using them:

  • v3/internal/generator/render/create.go - in PostponedCreates
  • v3/internal/generator/render/type.go - in renderNamedType
  • v3/internal/generator/render/default.go - in renderNamedDefault (two places)

If an import is missing, it's now dynamically added to prevent the empty namespace prefix.

Test plan

  • All existing generator tests pass (8 configurations)
  • Test with cross-package type aliases in a real project

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved code generation handling for external packages to prevent invalid output
    • Enhanced runtime stability in server-side rendering environments with proper environment detection
    • Fixed file dialog result collection on Linux
  • Chores

    • Internal improvements to import resolution logic
    • Added browser environment guards across the runtime layer for better compatibility

ddmoney420 and others added 3 commits January 29, 2026 02:15
The runChooserDialog function had a race condition that caused GTK
assertion failures and crashes on Linux:

1. InvokeAsync scheduled gtk_dialog_run on the GTK thread
2. After dialog closed, a goroutine was spawned OFF the GTK thread
3. gtk_widget_destroy was called immediately (before goroutine ran)
4. Goroutine tried to call gtk_file_chooser_get_filenames on destroyed widget

Fix:
- Extract filenames on GTK thread BEFORE destroying widget
- Call gtk_widget_destroy on GTK thread AFTER extraction
- Goroutine only handles sending Go strings (no GTK calls)

Fixes wailsapp#3683

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add typeof window/document checks to all module-level browser API
accesses in the TypeScript runtime. This fixes the "window is not
defined" error when using @wailsio/runtime with SSR frameworks
like Next.js.

Files modified:
- runtime.ts: guard window.location access
- calls.ts: guard window._wails initialization
- contextmenu.ts: guard addEventListener call
- drag.ts: guard window._wails and drag initialization
- events.ts: guard window._wails initialization
- index.ts: guard all window._wails setup and invoke calls
- dialogs.ts: guard window._wails initialization
- utils.ts: guard document.addEventListener for DOMContentLoaded
- system.ts: guard IIFE that detects WebView environment

Fixes wailsapp#4679

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When rendering JS/TS code for types from external packages, the code was
directly looking up the package in m.Imports.External without checking if
it existed. If for some reason the package wasn't added during type
collection (edge case), this would result in a zero-value ImportInfo with
an empty Name, producing invalid code like "$0.Model.createFrom" instead
of "data_models$0.Model.createFrom".

This fix adds defensive checks in create.go, type.go, and default.go to
dynamically add missing packages to the imports map if they're not already
present, ensuring valid namespace references are always generated.

Fixes wailsapp#4578

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jan 29, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

This pull request addresses import resolution and browser compatibility issues. It fixes incorrect namespace references when generating TypeScript bindings from Go type aliases across packages by dynamically resolving missing imports in the code generator. Additionally, it adds server-side rendering (SSR) guards across the TypeScript runtime to prevent window/document access in non-browser environments, and restructures GTK file chooser result handling on the Go side for thread safety.

Changes

Cohort / File(s) Summary
Go Code Generator - Dynamic Import Resolution
v3/internal/generator/render/create.go, v3/internal/generator/render/default.go, v3/internal/generator/render/type.go
Added fallback logic to dynamically register missing external package imports before code generation. When an external package isn't pre-registered in imports, the code now calls m.Imports.Add() to register it on-the-fly and retrieves the resolved import alias, ensuring correct namespace qualification in generated TypeScript bindings. This addresses issue #4578 where type aliases from other packages were generating incorrect namespace references.
TypeScript Runtime - SSR Compatibility Guards
v3/internal/runtime/desktop/@wailsio/runtime/src/calls.ts, contextmenu.ts, dialogs.ts, drag.ts, events.ts, index.ts, runtime.ts, system.ts, utils.ts
Wrapped all window and document access in typeof window !== "undefined" checks throughout the runtime modules to prevent errors in server-side rendering environments. Browser-specific initialization, event listeners, and API calls now safely skip execution when running on servers, while preserving full functionality in browser contexts.
Linux Platform - GTK Dialog Result Handling
v3/pkg/application/linux_cgo.go
Restructured runChooserDialog to collect file chooser results on the GTK thread before widget destruction, avoiding potential GTK call sequencing issues. Results are now pre-collected into a local slice and sent through the channel, eliminating redundant GTK calls from the goroutine and improving thread safety.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

go, v3-alpha, size:L

Suggested reviewers

  • ndianabasi

Poem

🐰 Hopping through imports with care,
SSR guards float in the air,
No window crashes, just right,
GTK threads gleam so bright,
Bindings now dance without despair! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 4
❌ Failed checks (4 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is missing several required checklist items from the template and lacks critical testing information. Complete all checklist items including changelog update, self-review, and testing verification across platforms (Windows/macOS/Linux).
Linked Issues check ⚠️ Warning The PR addresses the namespace reference issue but implements a render-time fix that introduces concurrency safety concerns, conflicting with maintainer feedback recommending a collector-phase fix. Revert render-time mutations in create.go, type.go, and default.go; implement the fix in the collector phase by replacing typeutil.Map with a plain map keyed by types.Object to avoid alias resolution side-effects.
Out of Scope Changes check ⚠️ Warning The PR contains three unrelated commits: GTK thread race fix, SSR runtime guards for Next.js, and the bindings namespace fix, mixing distinct concerns. Split commits into separate PRs: one for Linux GTK fix, one for SSR guards, and one for the bindings namespace resolution to maintain focused, reviewable changesets.
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: adding defensive checks for cross-package type alias namespace resolution in the bindings generator.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@fbbdev
fbbdev changed the base branch from master to v3-alpha February 1, 2026 17:17
@fbbdev

fbbdev commented Feb 1, 2026

Copy link
Copy Markdown
Contributor

Hi @ddmoney420 , I am the original author of the v3 binding generator.
I've not had enough bandwidth to contribute to the project lately, so thank you for taking this on.

Your PR was mistakenly opened against master (v2) so in order to review it I took the liberty to retarget it towards the v3-alpha branch.

I noticed that you pushed onto the same branch two commits that look unrelated (although valuable!). Not sure if this was intentional. Before we move forward with this PR, could you please split the additional commits into their own PRs? (I recommend one PR per commit since they solve two different issues).

@fbbdev

fbbdev commented Feb 1, 2026 •

Copy link
Copy Markdown
Contributor

Now getting to the actual fix.. I see that you tracked down the issue to a missing entry in the import map at render time.

Unfortunately your approach of filling in missing entries on the fly is unsound: the rendering phase is parallelized and relies critically on the assumption that input data is mostly immutable. Breaking that assumption will cause data races and potentially nasty bugs.

Filling import maps accurately is the collector's task, specifically the ImportMap.addTypeImpl method (v3/internal/generator/collect/imports.go:130). If import maps are incomplete, then we must have a bug there.

This sent me down an unexpected rabbit hole, because nothing in that method looks wrong. Actually, aliases share most of the implementation with named types, which work perfectly. As I started to despair, the light came to me: the culprit is the typeutil.Map we use to track visited types and break cycles (see v3/internal/generator/collect/imports.go:139).

case *types.Alias, *types.Named:
	if visited.Set(typ, true) != nil {
		// Break type cycles.
		return
	}

The issue is that typeutil.Map resolves aliases internally, hence as soon as the alias is visited, the aliased named type is also marked as visited and its package is never added to the import map.

Fortunately, we only use this map for aliases and named types, so we can replace it with a regular map indexed by objects: visited map[types.Object]bool and then if visited[obj] { return }; visited[obj] = true. That should fix the issue for good.

Before merging it will also be essential to add a test case, check that it reproduces the issue and check that it's fixed afterwards. It should be as simple as adding a new sub-package inside testcases/aliases and referencing it through an alias.

My recommendation is to revert revert to upstream first, add the test case, regenerate test results (see task generator:test:regenerate), check the result, commit the fix, regenerate and check again.

@sonarqubecloud

sonarqubecloud Bot commented Feb 2, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
18.2% Duplication on New Code (required ≤ 3%)
B Maintainability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
v3/internal/runtime/desktop/@wailsio/runtime/src/system.ts (1)

106-162: ⚠️ Potential issue | 🟡 Minor

Unguarded window access in Is* functions will throw in SSR.

The functions IsWindows, IsLinux, IsMac, IsAMD64, IsARM, IsARM64, and IsDebug directly access (window as any)._wails without SSR guards. Calling these in a server context will throw ReferenceError: window is not defined.

Consider adding guards to return sensible defaults (e.g., false) in SSR:

💡 Suggested fix for one function (apply pattern to others)
 export function IsWindows(): boolean {
+    if (typeof window === "undefined") return false;
     return (window as any)._wails?.environment?.OS === "windows";
 }
🤖 Fix all issues with AI agents
In `@v3/internal/generator/render/type.go`:
- Around line 213-219: Rendering mutates m.Imports.External via m.Imports.Add
inside render (see m.Imports.External, m.Imports.Add, m.collector.Package,
AddType), causing data races when render runs in parallel; fix it by moving
import population to collection time so render is read-only: in the collector
replace the visited tracking typeutil.Map with a plain map[types.Object]bool and
ensure alias resolution visits aliased types so the collector builds the full
import map (so m.Imports.External contains pkgPath entries before render), then
remove any runtime calls to m.Imports.Add from render code.

In `@v3/internal/runtime/desktop/`@wailsio/runtime/src/runtime.ts:
- Around line 13-14: The SSR fallback sets runtimeURL to an empty string which
causes new URL(runtimeURL) in runtimeCallWithID to throw an "Invalid URL"; add
an early guard in runtimeCallWithID that checks runtimeURL (and whether a custom
transport is configured) and throws a clear, descriptive error (e.g., "No
runtime URL available: set a custom transport for SSR or call from a browser")
instead of letting new URL() blow up; reference runtimeURL and the
runtimeCallWithID function and ensure the guard runs before any new URL(...) or
network/transport logic.

In `@v3/pkg/application/linux_cgo.go`:
- Around line 2119-2141: The code currently limits file chooser results to 1024
items by using a count cap while iterating the GList returned by
gtk_file_chooser_get_filenames, which silently drops additional selections;
remove the artificial count cap and iterate the full GList in the block guarded
by response == C.GTK_RESPONSE_ACCEPT (the loop using iter, iter.next,
buildStringAndFree and C.g_slist_free), or if you must limit, explicitly
record/truncate and send a sentinel or error over the selections channel so
callers are aware; ensure the iteration and freeing still occur on the GTK
thread and that the goroutine sending into selections remains unchanged.

Comment on lines +213 to +219
info := m.Imports.External[pkgPath]
if info.Name == "" {
// Package not in imports - this should not happen if AddType was called correctly.
// Add the package dynamically to avoid generating invalid code.
m.Imports.Add(m.collector.Package(typ.Obj().Pkg()))
info = m.Imports.External[pkgPath]
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Data race: mutating import map during parallel rendering is unsafe.

Per the maintainer's comment on this PR, rendering is parallelized and assumes input data is immutable. Mutating m.Imports.External via m.Imports.Add() during render time can cause data races and non-deterministic bugs when multiple goroutines render concurrently.

The recommended fix is to address the root cause in the collector phase—specifically, replacing typeutil.Map with a plain map[types.Object]bool in the visited tracking so that alias resolution side-effects don't cause the aliased type's package to be skipped when building the import map.

🤖 Prompt for AI Agents
In `@v3/internal/generator/render/type.go` around lines 213 - 219, Rendering
mutates m.Imports.External via m.Imports.Add inside render (see
m.Imports.External, m.Imports.Add, m.collector.Package, AddType), causing data
races when render runs in parallel; fix it by moving import population to
collection time so render is read-only: in the collector replace the visited
tracking typeutil.Map with a plain map[types.Object]bool and ensure alias
resolution visits aliased types so the collector builds the full import map (so
m.Imports.External contains pkgPath entries before render), then remove any
runtime calls to m.Imports.Add from render code.

Comment on lines +13 to +14
// SSR guard: only access window.location in browser environment
const runtimeURL = typeof window !== "undefined" ? window.location.origin + "/wails/runtime" : "";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential runtime error when runtimeURL is empty.

In SSR environments, runtimeURL defaults to an empty string. If a runtime call is made without a custom transport set, Line 110 (new URL(runtimeURL)) will throw "Invalid URL" because an empty string is not a valid URL.

Consider either:

  1. Adding an early guard in runtimeCallWithID to throw a more descriptive error
  2. Documenting that a custom transport must be set for SSR usage
💡 Suggested improvement for clearer error messaging
 async function runtimeCallWithID(objectID: number, method: number, windowName: string, args: any): Promise<any> {
     // Use custom transport if available
     if (customTransport) {
         return customTransport.call(objectID, method, windowName, args);
     }

+    // Guard against SSR usage without custom transport
+    if (!runtimeURL) {
+        throw new Error("Wails runtime calls require a browser environment or a custom transport");
+    }
+
     // Default HTTP fetch transport
     let url = new URL(runtimeURL);
🤖 Prompt for AI Agents
In `@v3/internal/runtime/desktop/`@wailsio/runtime/src/runtime.ts around lines 13
- 14, The SSR fallback sets runtimeURL to an empty string which causes new
URL(runtimeURL) in runtimeCallWithID to throw an "Invalid URL"; add an early
guard in runtimeCallWithID that checks runtimeURL (and whether a custom
transport is configured) and throws a clear, descriptive error (e.g., "No
runtime URL available: set a custom transport for SSR or call from a browser")
instead of letting new URL() blow up; reference runtimeURL and the
runtimeCallWithID function and ensure the guard runs before any new URL(...) or
network/transport logic.

Comment on lines 2119 to 2141
response := C.gtk_dialog_run((*C.GtkDialog)(fc))
// Extract results on GTK thread BEFORE destroying widget
var results []string
if response == C.GTK_RESPONSE_ACCEPT {
filenames := C.gtk_file_chooser_get_filenames((*C.GtkFileChooser)(fc))
iter := filenames
count := 0
for iter != nil && count < 1024 {
results = append(results, buildStringAndFree(C.gpointer(iter.data)))
iter = iter.next
count++
}
C.g_slist_free(filenames)
}
// Destroy widget after extracting results (on GTK thread)
C.gtk_widget_destroy((*C.GtkWidget)(unsafe.Pointer(fc)))
// Send results from goroutine (safe - no GTK calls)
go func() {
defer handlePanic()
if response == C.GTK_RESPONSE_ACCEPT {
filenames := C.gtk_file_chooser_get_filenames((*C.GtkFileChooser)(fc))
iter := filenames
count := 0
for {
selections <- buildStringAndFree(C.gpointer(iter.data))
iter = iter.next
if iter == nil || count == 1024 {
break
}
count++
}
for _, result := range results {
selections <- result
}
close(selections)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid silently truncating selections at 1024 items.

Line 2126 introduces a hard cap; selecting more than 1024 files will drop the rest without notice. Consider iterating the full list or explicitly signaling truncation.

[suggested fix]

🧩 Remove silent truncation
-            count := 0
-            for iter != nil && count < 1024 {
+            for iter != nil {
                 results = append(results, buildStringAndFree(C.gpointer(iter.data)))
                 iter = iter.next
-                count++
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
response := C.gtk_dialog_run((*C.GtkDialog)(fc))
// Extract results on GTK thread BEFORE destroying widget
var results []string
if response == C.GTK_RESPONSE_ACCEPT {
filenames := C.gtk_file_chooser_get_filenames((*C.GtkFileChooser)(fc))
iter := filenames
count := 0
for iter != nil && count < 1024 {
results = append(results, buildStringAndFree(C.gpointer(iter.data)))
iter = iter.next
count++
}
C.g_slist_free(filenames)
}
// Destroy widget after extracting results (on GTK thread)
C.gtk_widget_destroy((*C.GtkWidget)(unsafe.Pointer(fc)))
// Send results from goroutine (safe - no GTK calls)
go func() {
defer handlePanic()
if response == C.GTK_RESPONSE_ACCEPT {
filenames := C.gtk_file_chooser_get_filenames((*C.GtkFileChooser)(fc))
iter := filenames
count := 0
for {
selections <- buildStringAndFree(C.gpointer(iter.data))
iter = iter.next
if iter == nil || count == 1024 {
break
}
count++
}
for _, result := range results {
selections <- result
}
close(selections)
response := C.gtk_dialog_run((*C.GtkDialog)(fc))
// Extract results on GTK thread BEFORE destroying widget
var results []string
if response == C.GTK_RESPONSE_ACCEPT {
filenames := C.gtk_file_chooser_get_filenames((*C.GtkFileChooser)(fc))
iter := filenames
for iter != nil {
results = append(results, buildStringAndFree(C.gpointer(iter.data)))
iter = iter.next
}
C.g_slist_free(filenames)
}
// Destroy widget after extracting results (on GTK thread)
C.gtk_widget_destroy((*C.GtkWidget)(unsafe.Pointer(fc)))
// Send results from goroutine (safe - no GTK calls)
go func() {
defer handlePanic()
for _, result := range results {
selections <- result
}
close(selections)
🤖 Prompt for AI Agents
In `@v3/pkg/application/linux_cgo.go` around lines 2119 - 2141, The code currently
limits file chooser results to 1024 items by using a count cap while iterating
the GList returned by gtk_file_chooser_get_filenames, which silently drops
additional selections; remove the artificial count cap and iterate the full
GList in the block guarded by response == C.GTK_RESPONSE_ACCEPT (the loop using
iter, iter.next, buildStringAndFree and C.g_slist_free), or if you must limit,
explicitly record/truncate and send a sentinel or error over the selections
channel so callers are aware; ensure the iteration and freeing still occur on
the GTK thread and that the goroutine sending into selections remains unchanged.

@leaanthony leaanthony left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR mixes several unrelated changes and includes duplicates:

Issues:

  1. Duplicate of #4926: The linux_cgo.go changes are identical to PR #4926 which is already under review. Please remove these changes from this PR.

  2. Unrelated SSR guards: The TypeScript runtime SSR guards are a separate concern from the generator namespace resolution fix. Consider splitting these into a separate PR for cleaner review.

The generator fix itself looks good - adding defensive checks for cross-package type alias namespace resolution is the right approach.

Please:

  1. Remove v3/pkg/application/linux_cgo.go changes (they'll come from #4926)
  2. Either remove the SSR guards or split them into a separate PR

This will make the PR focused and easier to review/merge.

@ddmoney420

Copy link
Copy Markdown
Contributor Author

Thank you both for the thorough feedback and guidance. I really appreciate you taking the time to explain the collector vs render phase distinction.

I realize I bit off more than I can chew here - this fix requires deeper knowledge of the Wails build pipeline than I currently have. Rather than risk introducing new bugs, I'm going to close this PR.

If someone with more Wails experience wants to pick this up, @fbbdev's explanation is an excellent roadmap.

@ddmoney420 ddmoney420 closed this Feb 2, 2026
@fbbdev

fbbdev commented Feb 2, 2026

Copy link
Copy Markdown
Contributor

@ddmoney420 thanks a lot anyways for debugging the issue, it pointed me in the right direction.

I opened a PR with the proposed fix at #4948, it works for the test case I created but it would be nice if you could validate it as well on some independent input.

Indeed this kind of fix requires in-depth knowledge of the generator pipeline, if anything to avoid concurrency related traps. But it's good for the project if people other than me become familiar with it, and I'm always happy to offer guidance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[v3] bindings generator Incorrect namespace reference when generating TS code from Go type alias across packages

3 participants