Skip to content

Sanitize non-identifier characters in preview replacement module name - #45

Merged
k-kohey merged 1 commit into
k-kohey:mainfrom
brandan-prnd:fix/module-name-sanitize
Oct 15, 2026
Merged

k-kohey merged 1 commit into
k-kohey:mainfrom
brandan-prnd:fix/module-name-sanitize

Conversation

@brandan-prnd

Copy link
Copy Markdown
Contributor

Summary

ReplacementModuleName embeds the source file's base name in the thunk module name ({Module}_PreviewReplacement_{FileName}_{N}). A file name containing characters that are not valid in a Swift identifier (., +, -, spaces, …) makes that an invalid module name, and swiftc rejects the thunk before compiling anything.

This PR replaces every character outside [A-Za-z0-9_] with _, so previews work regardless of what characters a project uses in its file names.

Problem

A module name has to be a valid Swift identifier, but axe users are free to put other characters in their file names. Whatever the convention, the thunk then fails before anything is compiled:

module name "App_PreviewReplacement_FooView.ui_0" is not a valid identifier

Two examples we hit:

  • Foo+Bar.swift — the common Swift name for an extension file (String+Validation.swift, …).
  • FooView.ui.swift — a project-specific convention in our codebase. We suffix UI component files with .ui so that AI coding tools can tell a UI component from other Swift files by name alone (e.g. to pick capture targets). It is not a general Swift convention, but it is a legal file name, and . is exactly the kind of character this code should tolerate.

The source itself is never parsed; the failure is purely in the -module-name argument.

Fix

var nonIdentifierRe = regexp.MustCompile(`[^A-Za-z0-9_]`)

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)
}

Notes on the choice:

  • Foo+Bar.swift and Foo_Bar.swift map to the same base, but the trailing counter is the per-session reload counter (also used for thunk_<N>.dylib), so two files never share a module name within one host process. This is the same property the existing code already relied on for files with identical base names in different directories.
  • The module name is internal and only shows up in logs; it does not affect what is captured.
  • Non-ASCII letters are replaced too. swiftc does accept Unicode module names, so [^\p{L}\p{N}_] would preserve them, but I kept the conservative ASCII set since Go's \p{L} and Swift's identifier grammar are not guaranteed to agree. Happy to switch if you prefer readable non-ASCII names.

Verification

  • TestReplacementModuleName now covers FooView.ui.swift → FooView_ui and Foo+Bar.swift → Foo_Bar.
  • Verified end to end: a *.ui.swift file that previously failed with the error above now captures.

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 '_'.

@k-kohey k-kohey left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for your contribution!

@k-kohey
k-kohey merged commit e2b3325 into k-kohey:main Oct 15, 2026
2 checks passed
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.

2 participants