Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions Coder.Test/Languages/StringLiteralTrigraphTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Coder.Test.Languages;

using System.Linq;
using ktsu.Coder.Ast;
using ktsu.Coder.Languages;
using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Tests that a string literal holding a trigraph keeps its value in C and C++.
/// </summary>
/// <remarks>
/// C up to C17, and C++ before C++17, replace <c>??!</c>, <c>??/</c> and the other trigraphs even
/// inside a string literal, so <c>"what??!"</c> compiled with <c>-std=c11</c> prints <c>what|</c>.
/// </remarks>
[TestClass]
public class StringLiteralTrigraphTests
{
/// <summary>
/// Every trigraph, a run of three question marks, and a lone one that needs no escape.
/// </summary>
private const string Value = "what??! a??/b ??=??'??(??)??<??>??- ???! ok?";

/// <summary>
/// The compilers to look for, in the order a C project would.
/// </summary>
private static readonly string[] Compilers = ["cc", "gcc", "clang"];

/// <summary>
/// Tests that C and C++ escape the second question mark of every pair, and nothing else.
/// </summary>
/// <param name="language">The generator's language identifier.</param>
[TestMethod]
[DataRow("c")]
[DataRow("cpp")]
public void CFamilyBreaksEveryTrigraph(string language)
{
ILanguageGenerator generator = Generators().Single(g => g.LanguageId == language);

string code = generator.Generate(Literal.Text(Value));

Assert.Contains(
@"""what?\?! a?\?/b ?\?=?\?'?\?(?\?)?\?<?\?>?\?- ?\?\?! ok?""",
code,
StringComparison.Ordinal,
$"{language} wrote {code}");
Assert.DoesNotContain("??", code, StringComparison.Ordinal, $"{language} left a trigraph in {code}");
}

/// <summary>
/// Tests that the languages without trigraphs write the question marks as they are, since
/// <c>\?</c> is not an escape in Go, Rust or JavaScript.
/// </summary>
/// <param name="language">The generator's language identifier.</param>
[TestMethod]
[DataRow("csharp")]
[DataRow("javascript")]
[DataRow("python")]
[DataRow("go")]
[DataRow("rust")]
public void OtherLanguagesWriteQuestionMarksAsIs(string language)
{
ILanguageGenerator generator = Generators().Single(g => g.LanguageId == language);

string code = generator.Generate(Literal.Text(Value));

Assert.Contains($"\"{Value}\"", code, StringComparison.Ordinal, $"{language} wrote {code}");
}

/// <summary>
/// Tests that the generated literals hold exactly the characters they were given once a C compiler
/// in a strict standard mode, where trigraphs are on, has read them.
/// </summary>
[TestMethod]
public void CompiledWithTrigraphsOn_TheStringsKeepTheirValue()
{
string? compiler = ToolchainHarness.FindOnPath("--version", Compilers);
if (compiler is null)
{
Assert.Inconclusive("No C compiler on the path, so nothing was compiled.");
return;
}

ToolchainHarness.InTemporaryDirectory(directory =>
{
File.WriteAllText(Path.Combine(directory, "messages.h"), new CGenerator().Generate(Exemplar()));

// The driver spells the expected text one character at a time, so it holds no trigraph of
// its own, and reports which string differed through the exit code.
File.WriteAllText(Path.Combine(directory, "driver.c"), """
#include <string.h>
#include "messages.h"

int main(void)
{
static const char exclaim[] = { 'w', 'h', 'a', 't', '?', '?', '!', 0 };
static const char slash[] = { 'a', '?', '?', '/', 'b', 0 };

if (strcmp(Messages_exclaim(), exclaim) != 0)
{
return 1;
}

if (strcmp(Messages_slash(), slash) != 0)
{
return 2;
}

return 0;
}

""");

(int exitCode, string output) = ToolchainHarness.Run(
compiler,
"-std=c11 -Wall -Wextra -pedantic -Werror=trigraphs driver.c -o driver",
directory);
Assert.AreEqual(0, exitCode, $"{compiler} rejected the generated literals:{Environment.NewLine}{output}");

(int runExit, string runOutput) = ToolchainHarness.Run(Path.Combine(directory, "driver"), string.Empty, directory);
Assert.AreEqual(0, runExit, $"String {runExit} changed value under trigraph replacement.{Environment.NewLine}{runOutput}");
});
}

/// <summary>
/// Builds a header whose two static members return a literal holding a trigraph.
/// </summary>
/// <returns>The file to generate.</returns>
private static SourceFile Exemplar()
{
SourceFile file = new("messages") { IsHeader = true };
file.HeaderComment.Add("Generated by Coder. Do not edit.");

ClassDeclaration messages = new("Messages") { Kind = TypeDeclarationKind.Struct };
messages.Documentation.Add("Literals that hold a trigraph.");
messages.Members.Add(new VariableDeclaration("unused", "int"));

FunctionDeclaration exclaim = new("exclaim") { ReturnType = "string", IsStatic = true };
exclaim.Body.Add(new ReturnStatement(new LiteralExpression<string>("what??!")));
messages.Members.Add(exclaim);

FunctionDeclaration slash = new("slash") { ReturnType = "string", IsStatic = true };
slash.Body.Add(new ReturnStatement(new LiteralExpression<string>("a??/b")));
messages.Members.Add(slash);

file.Members.Add(messages);
return file;
}

private static ILanguageGenerator[] Generators() =>
[
new CSharpGenerator(),
new CGenerator(),
new CppGenerator(),
new GoGenerator(),
new RustGenerator(),
new PythonGenerator(),
new JavaScriptGenerator(),
];
}
29 changes: 29 additions & 0 deletions Coder/Languages/CFamilyGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,35 @@ protected override void WriteDesignator(string name, CodeBlocker code)
protected void WriteBracedList(ConstructionExpression construction, CodeBlocker code, string emptyList) =>
WriteElementList(construction, code, "{", "}", emptyList);

/// <inheritdoc/>
/// <remarks>
/// Adds one escape to the shared ones: a <c>?</c> that follows another <c>?</c> is written <c>\?</c>.
/// C up to C17, and C++ before C++17, replace a trigraph such as <c>??!</c> or <c>??/</c> even inside
/// a string literal, so <c>"what??!"</c> would read as <c>what|</c> and <c>??/</c> would become a
/// backslash that escapes whatever follows it. <c>\?</c> is a question mark in every C and C++
/// standard, so the string's contents do not change, and escaping only the second of a pair breaks
/// every trigraph while leaving a lone <c>?</c> as it was written. The override stays here because
/// <c>\?</c> is not an escape in Go, Rust or JavaScript.
/// </remarks>
protected override string EscapeString(string value)
{
string escaped = base.EscapeString(value);
if (!escaped.Contains("??", StringComparison.Ordinal))
{
return escaped;
}

StringBuilder builder = new(escaped.Length + 4);
char previous = '\0';
foreach (char c in escaped)
{
builder.Append(c == '?' && previous == '?' ? "\\?" : c.ToString());
previous = c;
}

return builder.ToString();
}

/// <inheritdoc/>
/// <remarks>
/// Octal, one escape per byte of the character's UTF-8 encoding. <c>\x</c> would read on through
Expand Down
2 changes: 1 addition & 1 deletion Coder/Languages/LanguageGeneratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@
}
}

foreach (object item in expected)

Check warning on line 372 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method

Check warning on line 372 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method

Check warning on line 372 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method
{
if (!emittedPreambleItems!.Contains(item))
{
Expand All @@ -387,7 +387,7 @@
/// Read off the nodes' public properties rather than a list of node types, so a node added to the
/// AST is walked without anybody remembering to add it here.
/// </remarks>
protected static IEnumerable<AstNode> SelfAndDescendants(AstNode root)

Check warning on line 390 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 26 to the 15 allowed.

Check warning on line 390 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 26 to the 15 allowed.

Check warning on line 390 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 26 to the 15 allowed.
{
HashSet<AstNode> visited = new(ReferenceEqualityComparer.Instance);
Stack<AstNode> pending = new();
Expand Down Expand Up @@ -1042,7 +1042,7 @@
/// <see cref="EscapeCodeUnit"/>, because written raw they break the literal: Go and Python reject a
/// NUL in source, and C# reads U+0085, U+2028 and U+2029 as ending the line.
/// </remarks>
protected string EscapeString(string value)
protected virtual string EscapeString(string value)
{
Ensure.NotNull(value);

Expand Down Expand Up @@ -1128,7 +1128,7 @@
/// the generator adds through <see cref="RequiredImports"/>.
/// </remarks>
protected virtual string SpellNonFiniteDouble(double value) =>
double.IsNaN(value) ? "NaN" : value > 0 ? "Infinity" : "-Infinity";

Check warning on line 1131 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 1131 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

/// <summary>
/// Formats a single-precision value so that every target reads it as floating-point.
Expand Down Expand Up @@ -1193,7 +1193,7 @@
string? origin = null;
long offset = 0;

foreach (EnumMember member in enumDecl.Members)

Check warning on line 1196 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loop should be simplified by calling Select(member => member.Value)

Check warning on line 1196 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loop should be simplified by calling Select(member => member.Value)
{
if (member.Value is not null)
{
Expand Down
Loading