diff --git a/Coder.Test/Ast/ConstantDeclarationTests.cs b/Coder.Test/Ast/ConstantDeclarationTests.cs index b024ee7..9337898 100644 --- a/Coder.Test/Ast/ConstantDeclarationTests.cs +++ b/Coder.Test/Ast/ConstantDeclarationTests.cs @@ -100,10 +100,12 @@ public void JavaScript_WritesStaticForAMember() } /// - /// Tests that a private constant member is spelled with both, since the two are independent. + /// Tests that a private constant member is still spelled static, and under the name it was + /// declared with: constancy is a modifier JavaScript has, and visibility is one it does not, so + /// asking for the second leaves the first alone rather than renaming what it modifies. /// [TestMethod] - public void JavaScript_CombinesStaticWithAPrivateName() + public void JavaScript_WritesStaticOnAPrivateConstantWithoutRenamingIt() { ClassDeclaration declaration = new("Limits"); declaration.Members.Add(new VariableDeclaration("MAX", "int", Literal.Number(10)) @@ -112,7 +114,10 @@ public void JavaScript_CombinesStaticWithAPrivateName() Visibility = Visibility.Private, }); - StringAssert.Contains(new JavaScriptGenerator().Generate(declaration), "static #MAX = 10;", StringComparison.Ordinal); + string code = new JavaScriptGenerator().Generate(declaration); + + Assert.Contains("static MAX = 10;", code, StringComparison.Ordinal); + Assert.DoesNotContain("#MAX", code, StringComparison.Ordinal); } /// diff --git a/Coder.Test/Ast/FunctionModifierTests.cs b/Coder.Test/Ast/FunctionModifierTests.cs index dc18595..12d4b44 100644 --- a/Coder.Test/Ast/FunctionModifierTests.cs +++ b/Coder.Test/Ast/FunctionModifierTests.cs @@ -139,18 +139,20 @@ public void JavaScript_WritesStaticOnAMethod() } /// - /// A static private member keeps both spellings, which are separate parts of the same declaration. + /// A static private member keeps the one spelling JavaScript has for it. static is a + /// modifier in front of the name and survives; privacy is a # on the name itself, so + /// writing it would rename the method and leave every call to it naming the old one. /// [TestMethod] - public void JavaScript_WritesStaticAlongsideThePrivatePrefix() + public void JavaScript_WritesStaticOnAPrivateMethodWithoutRenamingIt() { ClassDeclaration declaration = SampleClass(); ((FunctionDeclaration)declaration.Members[0]).Visibility = Visibility.Private; - Assert.Contains( - "static #distance(scale)", - new JavaScriptGenerator().Generate(declaration), - StringComparison.Ordinal); + string code = new JavaScriptGenerator().Generate(declaration); + + Assert.Contains("static distance(scale)", code, StringComparison.Ordinal); + Assert.DoesNotContain("#distance", code, StringComparison.Ordinal); } /// diff --git a/Coder.Test/Ast/VisibilityTests.cs b/Coder.Test/Ast/VisibilityTests.cs index f06a037..7d4dc1d 100644 --- a/Coder.Test/Ast/VisibilityTests.cs +++ b/Coder.Test/Ast/VisibilityTests.cs @@ -14,8 +14,9 @@ namespace ktsu.Coder.Test.Ast; /// /// /// Visibility is the first thing the AST models that no two languages spell the same way — a C# -/// keyword, a C++ access label, a JavaScript name prefix, and nothing at all in Python — so these -/// cover each spelling rather than asserting one shape four times. +/// keyword, a C++ access label, and nothing at all in Python or JavaScript, both of whose only +/// convention is a spelling of the name itself and so would rename a declaration out from under +/// every reference to it — so these cover each spelling rather than asserting one shape four times. /// [TestClass] public class VisibilityTests @@ -144,32 +145,68 @@ public void Cpp_TreatsInternalAsPublic() } /// - /// Tests that JavaScript spells a private member with the # prefix its own private syntax - /// uses, and leaves the others as ordinary members. + /// Tests that JavaScript drops visibility rather than renaming the declaration, since its private + /// syntax is a # on the name rather than a modifier in front of it. /// [TestMethod] - public void JavaScript_SpellsAPrivateMemberWithAHash() + public void JavaScript_DropsVisibilityRatherThanRenaming() { string code = new JavaScriptGenerator().Generate(SampleClass()); - StringAssert.Contains(code, "#x = 0;", StringComparison.Ordinal); - StringAssert.Contains(code, "origin = 0;", StringComparison.Ordinal); + Assert.Contains("x = 0;", code, StringComparison.Ordinal); + Assert.Contains("origin = 0;", code, StringComparison.Ordinal); - // protected has no JavaScript spelling, so the method is an ordinary one. - StringAssert.Contains(code, "area() {", StringComparison.Ordinal); + // protected has no JavaScript spelling either, so the method is an ordinary one. + Assert.Contains("area() {", code, StringComparison.Ordinal); + + Assert.DoesNotContain("#", code, StringComparison.Ordinal); + Assert.DoesNotContain("private", code, StringComparison.Ordinal); } /// - /// Tests that a private method is spelled with the prefix too, since JavaScript's # applies - /// to any class member rather than to fields alone. + /// Tests that a private method is left alone too, since the prefix applies to any class member + /// rather than to fields alone and so would strand a call to one just as it strands a read. /// [TestMethod] - public void JavaScript_SpellsAPrivateMethodWithAHash() + public void JavaScript_DropsVisibilityOnAMethodToo() { ClassDeclaration declaration = new("Point"); declaration.Members.Add(new FunctionDeclaration("recompute") { ReturnType = "void", Visibility = Visibility.Private }); - StringAssert.Contains(new JavaScriptGenerator().Generate(declaration), "#recompute() {", StringComparison.Ordinal); + string code = new JavaScriptGenerator().Generate(declaration); + + Assert.Contains("recompute() {", code, StringComparison.Ordinal); + Assert.DoesNotContain("#", code, StringComparison.Ordinal); + } + + /// + /// Tests that a private member and the body that reads it come out naming the same identifier. + /// + /// + /// This is the half the declaration tests above cannot see. # is a part of the name, so + /// prefixing a declaration renames it, and nothing prefixes a — + /// the shared path writes one verbatim. A class whose method reads a private field would then + /// declare #x and read x, which resolves to nothing and throws a + /// ReferenceError under a module's implicit strict mode. Every name the body mentions has + /// to be one the class body declares, whichever way the generator spells it. + /// + [TestMethod] + public void JavaScript_ReferencesThePrivateMemberItDeclared() + { + ClassDeclaration declaration = new("Counter"); + declaration.Members.Add(new VariableDeclaration("count", "int", Literal.Number(0)) { Visibility = Visibility.Private }); + + FunctionDeclaration increment = new("increment") { ReturnType = "void" }; + increment.Body.Add(new ReturnStatement(new VariableReference("count"))); + declaration.Members.Add(increment); + + string code = new JavaScriptGenerator().Generate(declaration); + + // The declared name and the referenced name are the same one, so the body reads what the + // class holds rather than an identifier nothing declares. + Assert.Contains("count = 0;", code, StringComparison.Ordinal); + Assert.Contains("return count;", code, StringComparison.Ordinal); + Assert.DoesNotContain("#count", code, StringComparison.Ordinal); } /// diff --git a/Coder/Languages/JavaScriptGenerator.cs b/Coder/Languages/JavaScriptGenerator.cs index 5d993f8..a05b963 100644 --- a/Coder/Languages/JavaScriptGenerator.cs +++ b/Coder/Languages/JavaScriptGenerator.cs @@ -279,7 +279,7 @@ protected override void GenerateFieldDeclaration(FieldDeclaration field, CodeBlo Ensure.NotNull(code); GenerateDocumentation(field, code); - code.Write(MemberName(field.Name ?? "unnamed", field.Visibility)); + code.Write(field.Name ?? "unnamed"); if (field.InitialValue is not null) { @@ -317,9 +317,12 @@ protected override void GenerateFunctionDeclaration(FunctionDeclaration funcDecl /// JavaScript's function keyword is a syntax error there. That is why the members are /// emitted here rather than through . /// - /// A private member is spelled with the # prefix, which is JavaScript's own private syntax - /// and enforced by the runtime. The other three visibilities have no spelling: JavaScript draws - /// the line at private, and a protected or internal member is an ordinary one. + /// No visibility is spelled, private included. JavaScript's # is a part of the name rather + /// than a modifier in front of it, so writing it renames the declaration, and nothing renames the + /// references: a is emitted verbatim by the shared path, leaving + /// a body that names an identifier the class no longer declares. That is the reason + /// drops its leading underscore and notes + /// the case of a name rather than changing it, and it is the same reason here. /// /// protected override void GenerateClassDeclaration(ClassDeclaration classDecl, CodeBlocker code) @@ -398,7 +401,7 @@ private void GenerateField(VariableDeclaration field, CodeBlocker code) code.Write(StaticKeyword); } - code.Write(MemberName(field.Name, field.Visibility)); + code.Write(field.Name); if (field.InitialValue is not null) { @@ -438,7 +441,7 @@ private void GenerateMethod(FunctionDeclaration method, CodeBlocker code) code.Write(method.Kind == FunctionKind.Constructor ? "constructor" - : MemberName(method.Name ?? "unnamedMethod", method.Visibility)); + : method.Name ?? "unnamedMethod"); code.Write("("); GenerateParameterList(method.Parameters, code); @@ -503,19 +506,6 @@ protected override void GenerateVariableDeclaration(VariableDeclaration varDecl, EndStatement(code); } - /// - /// Spells a class member's name for its visibility. - /// - /// The member's name in the AST. - /// The visibility it was declared with. - /// The name as the class body should spell it. - /// - /// # is a part of the name in JavaScript rather than a modifier in front of it, so private - /// members are spelled here rather than by writing a keyword before the declaration. - /// - private static string MemberName(string name, Visibility visibility) => - visibility == Visibility.Private ? $"#{name}" : name; - /// /// /// JavaScript has no entry point of its own — a module runs top to bottom — so the function is diff --git a/README.md b/README.md index 45b855e..3612db6 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ rather than the modifier's text because no two languages spell visibility the sa | C | Nothing inside a struct, which has no access control; a `Private` declaration at file scope is `static`, which is the internal linkage C has instead | | Rust | `pub`, or `pub(crate)` for `Internal` and `Protected`; `Private` writes nothing, which is already Rust's default. `Unspecified` is `pub`, since a generated type nothing outside the module can read is not what saying nothing asked for | | Go | Nothing. Go exports a name whose first letter is a capital and has no keyword at all, so a declaration whose name disagrees with what it asked for gets a note — renaming it would not rename the references to it. `Internal` is exactly Go's unexported, and `Private` and `Protected` are as near as there is | -| JavaScript | A private class member takes the `#` prefix, which is JavaScript's own private syntax; nothing for the rest | +| JavaScript | Nothing — the `#` that makes a class member private is a part of the name rather than a modifier in front of it, so writing one renames the declaration and leaves every reference to it naming something the class no longer declares | | Python | Nothing — Python has no access modifiers, and its leading-underscore convention renames the declaration rather than modifying it | ### Constants and entry points @@ -211,7 +211,7 @@ directly. |---|---|---|---| | `python` | `PythonGenerator` | `py` | Type hints, `None` for void, `pass` for an empty body or class; `self` on methods; `main` with its `__main__` guard | | `csharp` | `CSharpGenerator` | `cs` | Mapped type names, `var` for inferred declarations, visibility keywords, `const`, `static Main` | -| `javascript` | `JavaScriptGenerator` | `js` | Untyped; `const`/`let`; strict `===` and `!==`; method, `static` and `#private` syntax inside a class | +| `javascript` | `JavaScriptGenerator` | `js` | Untyped; `const`/`let`; strict `===` and `!==`; method and `static` syntax inside a class | | `cpp` | `CppGenerator` | `cpp` | Mapped type spellings (`str` → `std::string`); `auto` for inferred declarations; access labels, `static constexpr` members and a terminating `;` on a class | | `c` | `CGenerator` | `c` | `typedef struct` for every kind of type; a member function is a free `Type_name(Type* self, …)`; an interface is a struct of function pointers; a base type is the first member; enumeration members are qualified by their enumeration; `_Static_assert`, `main(void)`, and `static const` for a constant | | `rust` | `RustGenerator` | `rs` | A `struct` for the data and an `impl` block for the behaviour; an interface is a `trait` and a base type on one is a supertrait; a destructor is `impl Drop`, an operator is its `std::ops` trait, a conversion is `impl From`, and a specialisation is `impl Trait for Type`; `#[repr]`, `#[must_use]`, `const fn`, and `const _: () = assert!(…)` |