Skip to content

JavaScript's # private prefix renames the declaration but not its references, emitting broken code #34

Description

@matt-edmondson

What happens

JavaScriptGenerator.MemberName prefixes # onto a member declared Visibility.Private:

// JavaScriptGenerator.cs:185-186
private static string MemberName(string name, Visibility visibility) =>
    visibility == Visibility.Private ? $"#{name}" : name;

It is applied to field declarations (GenerateField) and method declarations (GenerateMethod). Nothing applies it anywhere else, and VariableReference is emitted verbatim by the shared path every generator uses:

// LanguageGeneratorBase.cs:109-111
case VariableReference varRef:
    code.Write(varRef.Name);
    return true;

So the declaration is renamed and every reference to it is not.

Failure scenario

A ClassDeclaration with a private field count and a method whose body assigns to a VariableReference("count") generates:

class Counter {
    #count = 0;
    increment() {
        count = count + 1;   // <- refers to nothing; #count is what was declared
    }
}

Under a module's implicit strict mode this is a ReferenceError at run time. The generated file is not usable, and the failure is silent at generation time.

The codebase already argues this is wrong

PythonGenerator's own class remark gives exactly this reason for not applying Python's leading-underscore convention:

The conventions Python does have for both — a leading underscore for a non-public member, an upper-case name for a constant — are spellings of the identifier rather than modifiers on the declaration, and renaming a declaration here would leave every VariableReference to it naming something that no longer exists.

JavaScriptGenerator's own remark concedes the same premise — "# is a part of the name in JavaScript rather than a modifier in front of it" — and then renames anyway. The two generators reached opposite conclusions from an identical observation, and Python's is the one that produces working code.

Test coverage gap

VisibilityTests pins the declaration side only — JavaScript_SpellsAPrivateMethodWithAHash asserts #recompute() {, and the field test asserts #x = 0;. No test generates a class whose body references a private member, which is why this passes. Python_DropsVisibilityRatherThanRenaming sits directly beneath them asserting the opposite policy.

Options

  1. Match Python — drop the prefix. Smallest change, consistent with the reasoning already written down, and costs only that JavaScript output does not express privacy. Given the generator already drops the types the AST carries, this is defensible.
  2. Rename references too. Requires the JavaScript generator to override the VariableReference case with knowledge of the enclosing class's private members, and to prefix the member side of any member access. Genuinely more faithful output, meaningfully more work, and #name is only legal inside the class body — a reference from outside would have to be caught and reported rather than emitted.

Either resolution should also add a test that generates a private member and a reference to it, so the two halves cannot drift apart again.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions