From 40ab9007ff371be1f624b2eeb90a3749ee6cf05a Mon Sep 17 00:00:00 2001 From: Brandan Date: Thu, 1 Oct 2026 11:51:58 +0900 Subject: [PATCH] Sanitize non-identifier characters in preview replacement module name ReplacementModuleName embeds the source file's base name (minus the .swift extension) in the module name. File names such as FooView.ui.swift or Foo+Bar.swift therefore produce an invalid identifier and swiftc rejects the thunk before compiling anything: module name "App_PreviewReplacement_FooView.ui_0" is not a valid identifier Replace every character outside [A-Za-z0-9_] with '_'. --- cmd/internal/preview/codegen/compiler.go | 5 +++++ cmd/internal/preview/codegen/thunk_test.go | 3 +++ 2 files changed, 8 insertions(+) diff --git a/cmd/internal/preview/codegen/compiler.go b/cmd/internal/preview/codegen/compiler.go index da1cbf9..f47430b 100644 --- a/cmd/internal/preview/codegen/compiler.go +++ b/cmd/internal/preview/codegen/compiler.go @@ -5,15 +5,20 @@ import ( "fmt" "log/slog" "path/filepath" + "regexp" "strings" "github.com/k-kohey/axe/internal/preview/buildlock" ) +var nonIdentifierRe = regexp.MustCompile(`[^A-Za-z0-9_]`) + // ReplacementModuleName generates the module name for a thunk dylib, matching // Xcode's convention: {Module}_PreviewReplacement_{FileName}_{N}. func ReplacementModuleName(moduleName, sourceFileName string, counter int) string { base := strings.TrimSuffix(filepath.Base(sourceFileName), filepath.Ext(sourceFileName)) + // "Foo+Bar.swift" or "FooView.ui.swift" would otherwise yield an invalid module identifier. + base = nonIdentifierRe.ReplaceAllString(base, "_") return fmt.Sprintf("%s_PreviewReplacement_%s_%d", moduleName, base, counter) } diff --git a/cmd/internal/preview/codegen/thunk_test.go b/cmd/internal/preview/codegen/thunk_test.go index 79d39e2..9d2866c 100644 --- a/cmd/internal/preview/codegen/thunk_test.go +++ b/cmd/internal/preview/codegen/thunk_test.go @@ -20,6 +20,9 @@ func TestReplacementModuleName(t *testing.T) { {"MyApp", "HomeView.swift", 3, "MyApp_PreviewReplacement_HomeView_3"}, // Should use only the base name, not subdirectory path {"App", "Views/HogeView.swift", 1, "App_PreviewReplacement_HogeView_1"}, + // Non-identifier characters in the file name are replaced + {"MyFeature", "FooView.ui.swift", 0, "MyFeature_PreviewReplacement_FooView_ui_0"}, + {"App", "Foo+Bar.swift", 2, "App_PreviewReplacement_Foo_Bar_2"}, } for _, tt := range tests { got := ReplacementModuleName(tt.moduleName, tt.sourceFileName, tt.counter)