From d8ad1d301d774d2da979b513024f887f70ca2a78 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Nov 2026 05:31:04 +0000 Subject: [PATCH 1/2] Escape keyword-named bindings in the JavaScript generator [patch] JavaScript wrote keyword-named parameters, locals, functions, classes and top-level names verbatim, so `total(in, typeof)` or `function return(...)` failed to parse. These bindings now get an underscore (`in_`, `return_`) at the declaration and at every use. Members keep their names. JavaScript reads a keyword after a `.` and as a field, accessor or method name, so `holder.default` and `this.switch()` stay as written. A new `EscapesMemberNames` hook on LanguageGeneratorBase tells EscapeReference to escape only a reference's leading segment, and SpellCallee to leave a call with a receiver alone and to count only free functions. Part of #106. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW3ect9msbMq1zFsRLD --- .../Languages/ReservedWordIdentifierTests.cs | 134 +++++++++++++++++- Coder/Languages/JavaScriptGenerator.cs | 59 ++++++-- Coder/Languages/LanguageGeneratorBase.cs | 44 +++++- 3 files changed, 215 insertions(+), 22 deletions(-) diff --git a/Coder.Test/Languages/ReservedWordIdentifierTests.cs b/Coder.Test/Languages/ReservedWordIdentifierTests.cs index 7fc7bca..aa948fd 100644 --- a/Coder.Test/Languages/ReservedWordIdentifierTests.cs +++ b/Coder.Test/Languages/ReservedWordIdentifierTests.cs @@ -7,7 +7,7 @@ namespace ktsu.Coder.Test.Languages; using Microsoft.VisualStudio.TestTools.UnitTesting; /// -/// Compiles and runs what the Rust, C++, Python and C# generators write for names that are keywords of +/// Compiles and runs what the Rust, C++, Python, C# and JavaScript generators write for names that are keywords of /// the target: a property called Type or Default, which are ordinary C# names, fields, /// parameters and locals called where, virtual, match, delete, /// loop and new, and, for Python, an enumeration member called None. Each name has to come out escaped where it is declared and spelled the @@ -748,6 +748,138 @@ private static SourceFile CSharpTypes() return file; } + /// + /// The JavaScript driver. A member keeps its keyword name, which JavaScript reads after a + /// ., and a top-level name, which no keyword can be, has had an underscore appended. + /// + private const string JavaScriptDriver = """ + + + const holder = new Holder(); + if (holder.class !== 3 || holder.default !== 4) { process.exit(1); } + if (holder.delete !== 1) { process.exit(2); } + if (holder.total(5, 6) !== 14) { process.exit(3); } + if (holder.twice() !== 3) { process.exit(4); } + if (Flag.null !== 0 || Flag.All !== 1) { process.exit(5); } + if (return_(7) !== 7 || relay() !== 5) { process.exit(6); } + if (var_ !== 7) { process.exit(7); } + if (!(new Child() instanceof import_)) { process.exit(8); } + + """; + + /// + /// Tests that JavaScript appends an underscore to a keyword-named parameter, local, function, + /// class and top-level name, at their declarations and where they are used, and leaves a + /// keyword-named field, accessor, method and enumeration member as declared, which is how + /// JavaScript reads a keyword after a .. + /// + [TestMethod] + public void JavaScript_KeywordNamesRunAndResolve() + { + string? node = ToolchainHarness.FindOnPath("--version", "node"); + if (node is null) + { + Assert.Inconclusive("No node on the path, so nothing was run."); + return; + } + + ToolchainHarness.InTemporaryDirectory(directory => + { + string source = new JavaScriptGenerator().Generate(JavaScriptTypes()) + JavaScriptDriver; + File.WriteAllText(Path.Combine(directory, "holder.js"), source); + + (int ran, string output) = ToolchainHarness.Run(node, "holder.js", directory); + Assert.AreEqual(0, ran, $"the generated script failed, or a keyword-named name read back wrongly (driver check {ran}):{Environment.NewLine}{output}{Environment.NewLine}{source}"); + }); + } + + /// + /// Tests that a JavaScript reference escapes the binding it leads with but not the member after a + /// ., and keeps this and the literals as written. + /// + [TestMethod] + public void JavaScript_ReferenceEscapesTheBindingButNotTheMember() + { + string source = new JavaScriptGenerator().Generate(JavaScriptTypes()); + + Assert.Contains("this.class", source, StringComparison.Ordinal, source); + Assert.Contains("this.switch()", source, StringComparison.Ordinal, source); + Assert.Contains("get delete()", source, StringComparison.Ordinal, source); + Assert.Contains("total(in_, typeof_)", source, StringComparison.Ordinal, source); + Assert.Contains("return_(5)", source, StringComparison.Ordinal, source); + Assert.DoesNotContain("this_", source, StringComparison.Ordinal, source); + Assert.DoesNotContain("class_", source, StringComparison.Ordinal, source); + Assert.DoesNotContain("switch_", source, StringComparison.Ordinal, source); + Assert.DoesNotContain("null_", source, StringComparison.Ordinal, source); + } + + /// + /// Builds classes, an enumeration, functions and a top-level name whose names are JavaScript + /// keywords: members, which keep their names, and bindings, which cannot. + /// + /// The file to generate. + private static SourceFile JavaScriptTypes() + { + ClassDeclaration holder = new("Holder"); + holder.Members.Add(new VariableDeclaration("class", "int", new LiteralExpression(3))); + holder.Members.Add(new VariableDeclaration("default", "int", new LiteralExpression(4))); + holder.Members.Add(new PropertyDeclaration("delete", "int") + { + GetterBody = { new ReturnStatement(new LiteralExpression(1)) }, + }); + + FunctionDeclaration keywordMethod = new("switch") { ReturnType = "int" }; + keywordMethod.Body.Add(new ReturnStatement(new LiteralExpression(2))); + holder.Members.Add(keywordMethod); + + FunctionDeclaration twice = new("twice") { ReturnType = "int" }; + twice.Body.Add(new ReturnStatement(new BinaryExpression( + new CallExpression(new VariableReference("this"), "switch"), + BinaryOperator.Add, + new LiteralExpression(1)))); + holder.Members.Add(twice); + + FunctionDeclaration total = new("total") { ReturnType = "int" }; + total.Parameters.Add(new Parameter("in", "int")); + total.Parameters.Add(new Parameter("typeof", "int")); + total.Body.Add(new VariableDeclaration( + "let", + "int", + new BinaryExpression(new VariableReference("in"), BinaryOperator.Add, new VariableReference("typeof")))); + total.Body.Add(new VariableDeclaration( + "new", + "int", + new BinaryExpression(new VariableReference("let"), BinaryOperator.Add, new VariableReference("this.class")))); + total.Body.Add(new ReturnStatement(new VariableReference("new"))); + holder.Members.Add(total); + + EnumDeclaration flag = new("Flag"); + flag.Members.Add(new EnumMember("null")); + flag.Members.Add(new EnumMember("All")); + + FunctionDeclaration identity = new("return") { ReturnType = "int" }; + identity.Parameters.Add(new Parameter("with", "int")); + identity.Body.Add(new ReturnStatement(new VariableReference("with"))); + + CallExpression callIdentity = new("return"); + callIdentity.Arguments.Add(new LiteralExpression(5)); + FunctionDeclaration relay = new("relay") { ReturnType = "int" }; + relay.Body.Add(new ReturnStatement(callIdentity)); + + ClassDeclaration keywordBase = new("import"); + ClassDeclaration child = new("Child") { BaseType = new TypeReference("import") }; + + SourceFile file = new("holder"); + file.Members.Add(holder); + file.Members.Add(flag); + file.Members.Add(identity); + file.Members.Add(relay); + file.Members.Add(new VariableDeclaration("var", "int", new LiteralExpression(7))); + file.Members.Add(keywordBase); + file.Members.Add(child); + return file; + } + /// /// Builds a struct whose members are all named after a keyword of one target or the other. /// diff --git a/Coder/Languages/JavaScriptGenerator.cs b/Coder/Languages/JavaScriptGenerator.cs index 3edeb2c..6fdbeba 100644 --- a/Coder/Languages/JavaScriptGenerator.cs +++ b/Coder/Languages/JavaScriptGenerator.cs @@ -27,6 +27,33 @@ public class JavaScriptGenerator : StandardLanguageGenerator /// private const string StaticKeyword = "static "; + /// + /// Gets the words JavaScript will not bind as a name in a class body or a module, which is strict + /// code: the reserved words, the strict-mode future reserved words, the literals, and the two + /// names strict code may not bind. + /// + /// + /// A binding — a function, class, parameter, local or top-level name — has no escape here, so it + /// gets an underscore: delete becomes delete_. A member keeps its name, because + /// JavaScript reads a keyword after a . and as a method, accessor or field name as written + /// (), and renaming one would change the object's shape for no + /// reason. + /// + protected override IReadOnlySet ReservedWords { get; } = new HashSet(StringComparer.Ordinal) + { + "arguments", "await", "break", "case", "catch", "class", "const", "continue", "debugger", + "default", "delete", "do", "else", "enum", "eval", "export", "extends", "false", "finally", + "for", "function", "if", "implements", "import", "in", "instanceof", "interface", "let", + "new", "null", "package", "private", "protected", "public", "return", "static", "super", + "switch", "this", "throw", "true", "try", "typeof", "var", "void", "while", "with", "yield", + }; + + /// + protected override bool IsReceiverWord(string word) => word is "this" or "super" or "true" or "false" or "null"; + + /// + protected override bool EscapesMemberNames => false; + /// /// Gets the unique identifier for this language generator. /// @@ -334,11 +361,11 @@ private void GenerateNestedAlias(UsingAlias usingAlias, CodeBlocker code) /// /// The type the alias names, if any. /// A built-in type's JavaScript constructor, an array's Array, or the name itself. - private static string AliasedValue(TypeReference? aliased) => aliased switch + private string AliasedValue(TypeReference? aliased) => aliased switch { null => "Object", { IsArray: true } => "Array", - _ => BuiltInAliasValues.TryGetValue(aliased.Name, out string? value) ? value : aliased.Name, + _ => BuiltInAliasValues.TryGetValue(aliased.Name, out string? value) ? value : EscapeReference(aliased.Name), }; /// @@ -353,7 +380,7 @@ protected override void GenerateUsingAlias(UsingAlias usingAlias, CodeBlocker co Ensure.NotNull(code); WritePreamble(usingAlias, code, PreambleSite.Type); - code.Write($"const {usingAlias.Name} = {AliasedValue(usingAlias.AliasedType)}"); + code.Write($"const {EscapeIdentifier(usingAlias.Name ?? string.Empty)} = {AliasedValue(usingAlias.AliasedType)}"); EndStatement(code); } @@ -377,7 +404,7 @@ protected override void GenerateConstructionExpression(ConstructionExpression co return; } - code.Write($"new {construction.Type.Name}("); + code.Write($"new {EscapeReference(construction.Type.Name)}("); if (named) { @@ -443,7 +470,7 @@ protected override void GenerateEnumDeclaration(EnumDeclaration enumDecl, CodeBl Ensure.NotNull(code); WritePreamble(enumDecl, code, PreambleSite.Enum); - code.WriteLine($"const {enumDecl.Name ?? "UnnamedEnum"} = Object.freeze({{"); + code.WriteLine($"const {EscapeIdentifier(enumDecl.Name ?? "UnnamedEnum")} = Object.freeze({{"); using (IndentScope members = new(code)) { @@ -466,7 +493,7 @@ protected override void GenerateFieldDeclaration(FieldDeclaration field, CodeBlo Ensure.NotNull(field); Ensure.NotNull(code); - WriteField(field, field.IsConstant && field.InitialValue is not null ? "const " : "let ", code); + WriteField(field, field.IsConstant && field.InitialValue is not null ? "const " : "let ", isBinding: true, code); } /// @@ -484,19 +511,21 @@ protected override void GenerateFieldDeclaration(FieldDeclaration field, CodeBlo /// class holds one value rather than each instance holding its own. /// private void GenerateClassField(FieldDeclaration field, CodeBlocker code) => - WriteField(field, field.IsStatic || field.IsConstant ? StaticKeyword : string.Empty, code); + WriteField(field, field.IsStatic || field.IsConstant ? StaticKeyword : string.Empty, isBinding: false, code); /// /// Writes a field declaration after the keyword its scope calls for. /// /// The declaration to emit. /// The keyword, with its trailing space, or empty for none. + /// True when the field is a top-level name, which a keyword cannot be; false for a class field, which keeps its name. /// The writer to emit into. - private void WriteField(FieldDeclaration field, string keyword, CodeBlocker code) + private void WriteField(FieldDeclaration field, string keyword, bool isBinding, CodeBlocker code) { WritePreamble(field, code, PreambleSite.Field); code.Write(keyword); - code.Write(field.Name ?? "unnamed"); + string name = field.Name ?? "unnamed"; + code.Write(isBinding ? EscapeIdentifier(name) : name); if (field.InitialValue is not null) { @@ -541,7 +570,7 @@ protected override void GenerateFunctionDeclaration(FunctionDeclaration funcDecl // JavaScript has no generics, so what a function is written over is written down. WriteTypeParametersDown(funcDecl.TypeParameters, code); - code.Write($"function {funcDecl.Name ?? "unnamedFunction"}("); + code.Write($"function {EscapeIdentifier(funcDecl.Name ?? "unnamedFunction")}("); GenerateParameterList(funcDecl.Parameters, code); // The line is left open, so the scope's brace lands on it: JavaScript braces hang. @@ -634,11 +663,13 @@ private void WriteClassHeading(ClassDeclaration classDecl, CodeBlocker code, str WriteInexpressible(code, $"implements {string.Join(", ", classDecl.Interfaces.Select(contract => contract.Name))}"); } - code.Write($"{binding}class {classDecl.Name ?? "UnnamedClass"}"); + // The class's own name is a binding even where a static field holds it, so it escapes; the + // field in front of it, a member, keeps the name as declared. + code.Write($"{binding}class {EscapeIdentifier(classDecl.Name ?? "UnnamedClass")}"); if (classDecl.BaseType is TypeReference baseType) { - code.Write($" extends {baseType.Name}"); + code.Write($" extends {EscapeReference(baseType.Name)}"); } } @@ -843,7 +874,7 @@ protected override void GenerateParameter(Parameter parameter, CodeBlocker code, Ensure.NotNull(parameter); Ensure.NotNull(code); - code.Write(parameter.Name ?? $"param{position}"); + code.Write(EscapeIdentifier(parameter.Name ?? $"param{position}")); AppendDefaultValue(parameter, code); } @@ -856,7 +887,7 @@ protected override void GenerateVariableDeclaration(VariableDeclaration varDecl, // `const` needs an initializer, so an uninitialized constant has to be declared with `let`. string keyword = varDecl.IsConstant && varDecl.InitialValue is not null ? "const" : "let"; - code.Write($"{keyword} {varDecl.Name}"); + code.Write($"{keyword} {EscapeIdentifier(varDecl.Name)}"); if (varDecl.InitialValue is not null) { diff --git a/Coder/Languages/LanguageGeneratorBase.cs b/Coder/Languages/LanguageGeneratorBase.cs index a349b55..49e8ee8 100644 --- a/Coder/Languages/LanguageGeneratorBase.cs +++ b/Coder/Languages/LanguageGeneratorBase.cs @@ -911,7 +911,7 @@ protected virtual void GenerateCallExpression(CallExpression callExpr, CodeBlock code.Write(SpellMemberAccess(callExpr.Receiver)); } - code.Write(SpellCallee(callExpr.Callee)); + code.Write(callExpr.Receiver is not null && !EscapesMemberNames ? callExpr.Callee : SpellCallee(callExpr.Callee)); code.Write("("); for (int index = 0; index < callExpr.Arguments.Count; index++) { @@ -1213,6 +1213,18 @@ protected virtual string EscapeIdentifier(string name) /// protected virtual bool IsReceiverWord(string word) => false; + /// + /// Gets whether a member's name, where it follows a ., is escaped like any other name. + /// + /// + /// True by default, because most targets reserve a keyword everywhere: C++ cannot read + /// holder.default, so the member is declared default_ and every access says so. + /// A target that accepts a keyword as a member name after a . — JavaScript reads + /// holder.default and this.delete() as written — returns false, and then only the + /// names a keyword would bind are escaped: what leads a reference, and a call with no receiver. + /// + protected virtual bool EscapesMemberNames => true; + /// /// Spells a reference so any segment naming a declared member escapes as its declaration did. /// @@ -1237,6 +1249,11 @@ protected string EscapeReference(string reference) string[] segments = reference.Split('.'); for (int i = 0; i < segments.Length; i++) { + if (i > 0 && !EscapesMemberNames) + { + continue; + } + string[] path = segments[i].Split("::"); if (path.Length > 1) { @@ -1270,17 +1287,30 @@ protected string EscapeReference(string reference) /// keyword. A callee is otherwise text and stays verbatim: C++'s sizeof and delete /// are reserved but not declared, and escaping them would break the call. Any function or method /// of the name counts, whatever its receiver, because every one of them was escaped the same way - /// and no target accepts the bare keyword as a callee. + /// and no target accepts the bare keyword as a callee. A target that does not escape member names + /// () counts only free functions, since only they were escaped. /// protected string SpellCallee(string callee) { Ensure.NotNull(callee); - return ReservedWords.Contains(callee) - && GenerationRoot is AstNode root - && SelfAndDescendants(root).OfType().Any(function => function.Name == callee) - ? EscapeIdentifier(callee) - : callee; + if (!ReservedWords.Contains(callee) || GenerationRoot is not AstNode root) + { + return callee; + } + + IEnumerable functions = SelfAndDescendants(root).OfType(); + if (!EscapesMemberNames) + { + // A method keeps its keyword name, so only a free function was escaped where it was declared. + HashSet methods = SelfAndDescendants(root) + .OfType() + .SelectMany(type => type.Members.OfType()) + .ToHashSet(ReferenceEqualityComparer.Instance); + functions = functions.Where(function => !methods.Contains(function)); + } + + return functions.Any(function => function.Name == callee) ? EscapeIdentifier(callee) : callee; } /// From b3ecee7e4c1c03ebfc21882b3c3c0bc9a16811a3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Nov 2026 05:32:05 +0000 Subject: [PATCH 2/2] Move the JavaScript keyword tests into their own file Keeps ReservedWordIdentifierTests.cs untouched, so this branch and the Go keyword PR (#445) merge in either order. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW3ect9msbMq1zFsRLD --- .../JavaScriptReservedWordIdentifierTests.cs | 153 ++++++++++++++++++ .../Languages/ReservedWordIdentifierTests.cs | 134 +-------------- 2 files changed, 154 insertions(+), 133 deletions(-) create mode 100644 Coder.Test/Languages/JavaScriptReservedWordIdentifierTests.cs diff --git a/Coder.Test/Languages/JavaScriptReservedWordIdentifierTests.cs b/Coder.Test/Languages/JavaScriptReservedWordIdentifierTests.cs new file mode 100644 index 0000000..6fc1576 --- /dev/null +++ b/Coder.Test/Languages/JavaScriptReservedWordIdentifierTests.cs @@ -0,0 +1,153 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Coder.Test.Languages; + +using ktsu.Coder.Ast; +using ktsu.Coder.Languages; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Runs what the JavaScript generator writes for names that are JavaScript keywords. A binding — a +/// parameter, local, function, class or top-level name — has no escape, so it has to come out with an +/// underscore wherever it is declared and used. A member keeps its name, because JavaScript reads a +/// keyword after a . and as a field, accessor or method name. +/// +/// +/// The run is inconclusive rather than failing where no node is on the path, which is the honest +/// result: nothing was checked. +/// +[TestClass] +public class JavaScriptReservedWordIdentifierTests +{ + /// + /// The JavaScript driver. A member keeps its keyword name, which JavaScript reads after a + /// ., and a top-level name, which no keyword can be, has had an underscore appended. + /// + private const string JavaScriptDriver = """ + + + const holder = new Holder(); + if (holder.class !== 3 || holder.default !== 4) { process.exit(1); } + if (holder.delete !== 1) { process.exit(2); } + if (holder.total(5, 6) !== 14) { process.exit(3); } + if (holder.twice() !== 3) { process.exit(4); } + if (Flag.null !== 0 || Flag.All !== 1) { process.exit(5); } + if (return_(7) !== 7 || relay() !== 5) { process.exit(6); } + if (var_ !== 7) { process.exit(7); } + if (!(new Child() instanceof import_)) { process.exit(8); } + + """; + + /// + /// Tests that JavaScript appends an underscore to a keyword-named parameter, local, function, + /// class and top-level name, at their declarations and where they are used, and leaves a + /// keyword-named field, accessor, method and enumeration member as declared, which is how + /// JavaScript reads a keyword after a .. + /// + [TestMethod] + public void JavaScript_KeywordNamesRunAndResolve() + { + string? node = ToolchainHarness.FindOnPath("--version", "node"); + if (node is null) + { + Assert.Inconclusive("No node on the path, so nothing was run."); + return; + } + + ToolchainHarness.InTemporaryDirectory(directory => + { + string source = new JavaScriptGenerator().Generate(JavaScriptTypes()) + JavaScriptDriver; + File.WriteAllText(Path.Combine(directory, "holder.js"), source); + + (int ran, string output) = ToolchainHarness.Run(node, "holder.js", directory); + Assert.AreEqual(0, ran, $"the generated script failed, or a keyword-named name read back wrongly (driver check {ran}):{Environment.NewLine}{output}{Environment.NewLine}{source}"); + }); + } + + /// + /// Tests that a JavaScript reference escapes the binding it leads with but not the member after a + /// ., and keeps this and the literals as written. + /// + [TestMethod] + public void JavaScript_ReferenceEscapesTheBindingButNotTheMember() + { + string source = new JavaScriptGenerator().Generate(JavaScriptTypes()); + + Assert.Contains("this.class", source, StringComparison.Ordinal, source); + Assert.Contains("this.switch()", source, StringComparison.Ordinal, source); + Assert.Contains("get delete()", source, StringComparison.Ordinal, source); + Assert.Contains("total(in_, typeof_)", source, StringComparison.Ordinal, source); + Assert.Contains("return_(5)", source, StringComparison.Ordinal, source); + Assert.DoesNotContain("this_", source, StringComparison.Ordinal, source); + Assert.DoesNotContain("class_", source, StringComparison.Ordinal, source); + Assert.DoesNotContain("switch_", source, StringComparison.Ordinal, source); + Assert.DoesNotContain("null_", source, StringComparison.Ordinal, source); + } + + /// + /// Builds classes, an enumeration, functions and a top-level name whose names are JavaScript + /// keywords: members, which keep their names, and bindings, which cannot. + /// + /// The file to generate. + private static SourceFile JavaScriptTypes() + { + ClassDeclaration holder = new("Holder"); + holder.Members.Add(new VariableDeclaration("class", "int", new LiteralExpression(3))); + holder.Members.Add(new VariableDeclaration("default", "int", new LiteralExpression(4))); + holder.Members.Add(new PropertyDeclaration("delete", "int") + { + GetterBody = { new ReturnStatement(new LiteralExpression(1)) }, + }); + + FunctionDeclaration keywordMethod = new("switch") { ReturnType = "int" }; + keywordMethod.Body.Add(new ReturnStatement(new LiteralExpression(2))); + holder.Members.Add(keywordMethod); + + FunctionDeclaration twice = new("twice") { ReturnType = "int" }; + twice.Body.Add(new ReturnStatement(new BinaryExpression( + new CallExpression(new VariableReference("this"), "switch"), + BinaryOperator.Add, + new LiteralExpression(1)))); + holder.Members.Add(twice); + + FunctionDeclaration total = new("total") { ReturnType = "int" }; + total.Parameters.Add(new Parameter("in", "int")); + total.Parameters.Add(new Parameter("typeof", "int")); + total.Body.Add(new VariableDeclaration( + "let", + "int", + new BinaryExpression(new VariableReference("in"), BinaryOperator.Add, new VariableReference("typeof")))); + total.Body.Add(new VariableDeclaration( + "new", + "int", + new BinaryExpression(new VariableReference("let"), BinaryOperator.Add, new VariableReference("this.class")))); + total.Body.Add(new ReturnStatement(new VariableReference("new"))); + holder.Members.Add(total); + + EnumDeclaration flag = new("Flag"); + flag.Members.Add(new EnumMember("null")); + flag.Members.Add(new EnumMember("All")); + + FunctionDeclaration identity = new("return") { ReturnType = "int" }; + identity.Parameters.Add(new Parameter("with", "int")); + identity.Body.Add(new ReturnStatement(new VariableReference("with"))); + + CallExpression callIdentity = new("return"); + callIdentity.Arguments.Add(new LiteralExpression(5)); + FunctionDeclaration relay = new("relay") { ReturnType = "int" }; + relay.Body.Add(new ReturnStatement(callIdentity)); + + ClassDeclaration keywordBase = new("import"); + ClassDeclaration child = new("Child") { BaseType = new TypeReference("import") }; + + SourceFile file = new("holder"); + file.Members.Add(holder); + file.Members.Add(flag); + file.Members.Add(identity); + file.Members.Add(relay); + file.Members.Add(new VariableDeclaration("var", "int", new LiteralExpression(7))); + file.Members.Add(keywordBase); + file.Members.Add(child); + return file; + } +} diff --git a/Coder.Test/Languages/ReservedWordIdentifierTests.cs b/Coder.Test/Languages/ReservedWordIdentifierTests.cs index aa948fd..7fc7bca 100644 --- a/Coder.Test/Languages/ReservedWordIdentifierTests.cs +++ b/Coder.Test/Languages/ReservedWordIdentifierTests.cs @@ -7,7 +7,7 @@ namespace ktsu.Coder.Test.Languages; using Microsoft.VisualStudio.TestTools.UnitTesting; /// -/// Compiles and runs what the Rust, C++, Python, C# and JavaScript generators write for names that are keywords of +/// Compiles and runs what the Rust, C++, Python and C# generators write for names that are keywords of /// the target: a property called Type or Default, which are ordinary C# names, fields, /// parameters and locals called where, virtual, match, delete, /// loop and new, and, for Python, an enumeration member called None. Each name has to come out escaped where it is declared and spelled the @@ -748,138 +748,6 @@ private static SourceFile CSharpTypes() return file; } - /// - /// The JavaScript driver. A member keeps its keyword name, which JavaScript reads after a - /// ., and a top-level name, which no keyword can be, has had an underscore appended. - /// - private const string JavaScriptDriver = """ - - - const holder = new Holder(); - if (holder.class !== 3 || holder.default !== 4) { process.exit(1); } - if (holder.delete !== 1) { process.exit(2); } - if (holder.total(5, 6) !== 14) { process.exit(3); } - if (holder.twice() !== 3) { process.exit(4); } - if (Flag.null !== 0 || Flag.All !== 1) { process.exit(5); } - if (return_(7) !== 7 || relay() !== 5) { process.exit(6); } - if (var_ !== 7) { process.exit(7); } - if (!(new Child() instanceof import_)) { process.exit(8); } - - """; - - /// - /// Tests that JavaScript appends an underscore to a keyword-named parameter, local, function, - /// class and top-level name, at their declarations and where they are used, and leaves a - /// keyword-named field, accessor, method and enumeration member as declared, which is how - /// JavaScript reads a keyword after a .. - /// - [TestMethod] - public void JavaScript_KeywordNamesRunAndResolve() - { - string? node = ToolchainHarness.FindOnPath("--version", "node"); - if (node is null) - { - Assert.Inconclusive("No node on the path, so nothing was run."); - return; - } - - ToolchainHarness.InTemporaryDirectory(directory => - { - string source = new JavaScriptGenerator().Generate(JavaScriptTypes()) + JavaScriptDriver; - File.WriteAllText(Path.Combine(directory, "holder.js"), source); - - (int ran, string output) = ToolchainHarness.Run(node, "holder.js", directory); - Assert.AreEqual(0, ran, $"the generated script failed, or a keyword-named name read back wrongly (driver check {ran}):{Environment.NewLine}{output}{Environment.NewLine}{source}"); - }); - } - - /// - /// Tests that a JavaScript reference escapes the binding it leads with but not the member after a - /// ., and keeps this and the literals as written. - /// - [TestMethod] - public void JavaScript_ReferenceEscapesTheBindingButNotTheMember() - { - string source = new JavaScriptGenerator().Generate(JavaScriptTypes()); - - Assert.Contains("this.class", source, StringComparison.Ordinal, source); - Assert.Contains("this.switch()", source, StringComparison.Ordinal, source); - Assert.Contains("get delete()", source, StringComparison.Ordinal, source); - Assert.Contains("total(in_, typeof_)", source, StringComparison.Ordinal, source); - Assert.Contains("return_(5)", source, StringComparison.Ordinal, source); - Assert.DoesNotContain("this_", source, StringComparison.Ordinal, source); - Assert.DoesNotContain("class_", source, StringComparison.Ordinal, source); - Assert.DoesNotContain("switch_", source, StringComparison.Ordinal, source); - Assert.DoesNotContain("null_", source, StringComparison.Ordinal, source); - } - - /// - /// Builds classes, an enumeration, functions and a top-level name whose names are JavaScript - /// keywords: members, which keep their names, and bindings, which cannot. - /// - /// The file to generate. - private static SourceFile JavaScriptTypes() - { - ClassDeclaration holder = new("Holder"); - holder.Members.Add(new VariableDeclaration("class", "int", new LiteralExpression(3))); - holder.Members.Add(new VariableDeclaration("default", "int", new LiteralExpression(4))); - holder.Members.Add(new PropertyDeclaration("delete", "int") - { - GetterBody = { new ReturnStatement(new LiteralExpression(1)) }, - }); - - FunctionDeclaration keywordMethod = new("switch") { ReturnType = "int" }; - keywordMethod.Body.Add(new ReturnStatement(new LiteralExpression(2))); - holder.Members.Add(keywordMethod); - - FunctionDeclaration twice = new("twice") { ReturnType = "int" }; - twice.Body.Add(new ReturnStatement(new BinaryExpression( - new CallExpression(new VariableReference("this"), "switch"), - BinaryOperator.Add, - new LiteralExpression(1)))); - holder.Members.Add(twice); - - FunctionDeclaration total = new("total") { ReturnType = "int" }; - total.Parameters.Add(new Parameter("in", "int")); - total.Parameters.Add(new Parameter("typeof", "int")); - total.Body.Add(new VariableDeclaration( - "let", - "int", - new BinaryExpression(new VariableReference("in"), BinaryOperator.Add, new VariableReference("typeof")))); - total.Body.Add(new VariableDeclaration( - "new", - "int", - new BinaryExpression(new VariableReference("let"), BinaryOperator.Add, new VariableReference("this.class")))); - total.Body.Add(new ReturnStatement(new VariableReference("new"))); - holder.Members.Add(total); - - EnumDeclaration flag = new("Flag"); - flag.Members.Add(new EnumMember("null")); - flag.Members.Add(new EnumMember("All")); - - FunctionDeclaration identity = new("return") { ReturnType = "int" }; - identity.Parameters.Add(new Parameter("with", "int")); - identity.Body.Add(new ReturnStatement(new VariableReference("with"))); - - CallExpression callIdentity = new("return"); - callIdentity.Arguments.Add(new LiteralExpression(5)); - FunctionDeclaration relay = new("relay") { ReturnType = "int" }; - relay.Body.Add(new ReturnStatement(callIdentity)); - - ClassDeclaration keywordBase = new("import"); - ClassDeclaration child = new("Child") { BaseType = new TypeReference("import") }; - - SourceFile file = new("holder"); - file.Members.Add(holder); - file.Members.Add(flag); - file.Members.Add(identity); - file.Members.Add(relay); - file.Members.Add(new VariableDeclaration("var", "int", new LiteralExpression(7))); - file.Members.Add(keywordBase); - file.Members.Add(child); - return file; - } - /// /// Builds a struct whose members are all named after a keyword of one target or the other. ///