Tsonic GitHub

Normalize Phase

The Normalize phase reserves all TypeScript identifiers and resolves naming conflicts.

Entry Point

File: Normalize/NameReservation.cs

public static void ReserveAll(BuildContext ctx, SymbolGraph graph)
{
    // 1. Reserve type names in namespace scopes
    foreach (var ns in graph.Namespaces)
    {
        var scope = ScopeFactory.Namespace(ns.Name);
        foreach (var type in ns.Types)
        {
            ctx.Renamer.ReserveTypeName(
                type.StableId,
                type.TsEmitName,
                scope,
                "TypeReservation");
        }
    }

    // 2. Reserve member names in type scopes
    foreach (var type in graph.AllTypes)
    {
        ReserveMembers(ctx, type);
    }
}

Scope System

Names are reserved in hierarchical scopes:

Namespace Scope: "ns:System.Collections.Generic#internal"
    └── Type names (List_1, Dictionary_2, ...)

Type Scope: "type:System.Collections.Generic.List`1#instance"
    └── Instance member names (add, remove, count, ...)

Type Scope: "type:System.Collections.Generic.List`1#static"
    └── Static member names (empty, ...)

View Scope: "view:System.Collections.Generic.List`1:IEnumerable`1#instance"
    └── View member names (getEnumerator, ...)

Conflict Resolution

When a name is already taken in a scope, numeric suffixes are added:

// First reservation
"add" -> "add"

// Conflict - add suffix
"add" -> "add2"

// Another conflict
"add" -> "add3"

Naming Style Transform

The --naming js option applies camelCase transform:

ctx.Renamer.AdoptMemberStyleTransform(name => ToCamelCase(name));

// GetEnumerator -> getEnumerator
// WriteLine -> writeLine
// XMLReader -> xmlReader

Type names are NOT transformed (always PascalCase).

Reserved Word Handling

TypeScript reserved words get trailing underscore:

public static class TypeScriptReservedWords
{
    public static (string Sanitized, bool WasSanitized) Sanitize(string name)
    {
        if (IsReserved(name))
            return (name + "_", true);
        return (name, false);
    }
}

// Examples:
// "default" -> "default_"
// "class" -> "class_"
// "function" -> "function_"

Rename Decisions

Every rename is recorded for bindings generation:

public sealed record RenameDecision
{
    public StableId Id { get; }
    public string Requested { get; }      // Original name
    public string Final { get; }          // Final TypeScript name
    public string Reason { get; }         // Why renamed
    public string Strategy { get; }       // None, NumericSuffix, ReservedWord
    public string ScopeKey { get; }
    public bool? IsStatic { get; }
}

Querying Final Names

After normalization, all names are queryable:

// Type names
string finalName = ctx.Renamer.GetFinalTypeName(type);
string instanceName = ctx.Renamer.GetInstanceTypeName(type);  // T$instance
string staticName = ctx.Renamer.GetStaticInterfaceName(type); // T$static

// Member names
var scope = ScopeFactory.ClassSurface(type, isStatic: false);
string memberName = ctx.Renamer.GetFinalMemberName(member.StableId, scope);

Explicit Interface Members

Explicit interface implementations get interface-suffixed names:

// C#: void ICollection.Clear() { }
// Requested: "clear" (conflicts with own Clear method)
// Final: "clear_ICollection"