Skip to content

JavaScript ignores FieldDeclaration.IsStatic/IsConstant: static class fields become per-instance, and a file-scope field is an undeclared assignment that throws in an ES module #104

Closed

Description

@matt-edmondson

What's wrong

JavaScriptGenerator.GenerateFieldDeclaration (Coder/Languages/JavaScriptGenerator.cs:273-287) writes name = value; and nothing else. It never reads IsStatic or IsConstant, and it writes the same text wherever the field stands:

  1. Inside a class, a static or constant field becomes an ordinary instance field. Counter.MAX is undefined, and every instance gets its own copy of what was meant to be shared state. The VariableDeclaration-as-field path in the same file (GenerateField, :391-409) already writes static for a constant, with a comment explaining why. The FieldDeclaration path, which is the node the AST documents for fields, does not.
  2. At file or namespace scope, the declaration has no const/let, so it is a bare assignment to an undeclared name. The generator writes imports as ES module imports (SpellImport at :107 gives import "...";), and modules are always strict. In a module that assignment throws ReferenceError at load. In a sloppy-mode script it silently creates a global instead.

The other targets all honour these flags for the same AST:

Target File-scope constant Static field
C++ inline constexpr int Limit = 10; inline static int created = 0;
C# — public static int created / static readonly
Go const Limit int = 10 var CounterCreated int = 0

Rust's equivalent gap is tracked separately in #81; this issue is JavaScript only.

Reproduction

var file = new SourceFile("Sample");
file.Members.Add(new FieldDeclaration("Limit", "int") { IsConstant = true, InitialValue = new LiteralExpression<int>(10) });
var counter = new ClassDeclaration("Counter");
counter.Members.Add(new FieldDeclaration("created", "int") { IsStatic = true, InitialValue = new LiteralExpression<int>(0) });
counter.Members.Add(new FieldDeclaration("MAX", "int") { IsStatic = true, IsConstant = true, InitialValue = new LiteralExpression<int>(99) });
file.Members.Add(counter);
Console.WriteLine(new JavaScriptGenerator().Generate(file));

Actual output:

Limit = 10;

class Counter {
    created = 0;
    MAX = 99;
}

Run as an ES module (node gen.mjs), node 22 fails on line 1:

Limit = 10;
      ^
ReferenceError: Limit is not defined

Run as a classic script, it loads, but console.log(Counter.MAX, globalThis.Limit) prints undefined 10. The static field is missing from the class, and the file-scope constant leaked onto the global object.

Expected:

const Limit = 10;

class Counter {
    static created = 0;
    static MAX = 99;
}

Why it matters

Constant tables and shared counters are among the main things the AST is documented to generate (see FieldDeclaration.IsStatic/IsConstant in CLAUDE.md). In JavaScript they either crash the module at load, or quietly change from one shared value to one value per instance.

Suggested fix / acceptance criteria

  • Inside a class body, write static when IsStatic || IsConstant, matching GenerateField for VariableDeclaration.
  • Outside a class, meaning directly in a SourceFile or NamespaceDeclaration, write const for IsConstant and let otherwise, as GenerateVariableDeclaration already does for locals.
  • Add tests covering: a static field in a class, a constant field in a class, and a constant field at file scope. Ideally run the output with node when it is available and check Counter.MAX === 99.

Activity

  1. matt-edmondson commented on Sep 27, 2026

    @matt-edmondson
    ContributorAuthor

    Triage


    Generated by Claude Code

  2. added a commit that references this issue on Oct 5, 2026
    40094af
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't workingreadyFully specified; implement as written

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions