diff --git a/.sonarlint/sonar-local.globalconfig b/.sonarlint/sonar-local.globalconfig
new file mode 100644
index 0000000..b52b70b
--- /dev/null
+++ b/.sonarlint/sonar-local.globalconfig
@@ -0,0 +1,34 @@
+is_global = true
+
+# Rule severities for the local SonarCloud reproduction (see sonar-local.props).
+# Applied only when building with
+# dotnet build -p:CustomAfterMicrosoftCommonProps=$PWD/.sonarlint/sonar-local.props
+
+# CI's SonarCloud quality profile reports these, but the SonarAnalyzer NuGet package ships them
+# disabled by default. Raise them so a local run sees what CI sees.
+#
+# S3267 and S2699 are here because CI reported them on PR #87 - S3267 as new issues on
+# DocComment.Validate, S2699 against the older test files. The rest are carried over from the
+# equivalent config in ktsu.Semantics, whose quality profile is the closest available reference.
+dotnet_diagnostic.S107.severity = warning
+dotnet_diagnostic.S1075.severity = warning
+dotnet_diagnostic.S1172.severity = warning
+dotnet_diagnostic.S1192.severity = warning
+dotnet_diagnostic.S1871.severity = warning
+dotnet_diagnostic.S2583.severity = warning
+dotnet_diagnostic.S2699.severity = warning
+dotnet_diagnostic.S3267.severity = warning
+dotnet_diagnostic.S3358.severity = warning
+dotnet_diagnostic.S3458.severity = warning
+dotnet_diagnostic.S3776.severity = warning
+dotnet_diagnostic.S6444.severity = warning
+
+# Enabled by default in the analyzer package. Left enabled here: unlike ktsu.Semantics, this
+# repository has not been shown to have a profile that excludes it, so a false positive is
+# cheaper than a missed finding.
+# dotnet_diagnostic.S1481.severity = none
+
+# KNOWN GAP: SonarCloud reported one new issue on PR #87 that this configuration does not
+# reproduce. The rule behind it is either absent from the analyzer package or shipped disabled
+# and not listed above. If you have dashboard access and can identify it, add it here - the
+# calibration is only as good as the rules it names.
diff --git a/.sonarlint/sonar-local.props b/.sonarlint/sonar-local.props
new file mode 100644
index 0000000..311f56c
--- /dev/null
+++ b/.sonarlint/sonar-local.props
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
diff --git a/CLAUDE.md b/CLAUDE.md
index 80aae20..f53305a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -18,6 +18,36 @@ dotnet test --filter "TestMethodName"
dotnet test --logger "console;verbosity=detailed"
```
+### Reproducing SonarCloud warnings locally
+
+CI analyses this repository with the SonarCloud scanner, which injects the Sonar analyzers into the
+compilation. A plain `dotnet build` does **not** run them, so Sonar findings are invisible locally
+and only surface after a push — and the bot comment links to a dashboard rather than naming them.
+To run the same analyzers:
+
+```bash
+dotnet build -p:CustomAfterMicrosoftCommonProps=$PWD/.sonarlint/sonar-local.props
+```
+
+```powershell
+dotnet build -p:CustomAfterMicrosoftCommonProps=$PWD\.sonarlint\sonar-local.props
+```
+
+Note **`After`**, not `Before`. Every project here declares its SDK with ``
+elements rather than the `` attribute, and `CustomBeforeMicrosoftCommonProps`
+does not reach that form.
+
+The opt-in lives in `.sonarlint/sonar-local.props` (the analyzer package) and
+`.sonarlint/sonar-local.globalconfig` (rule severities — it raises the rules CI reports that the
+analyzer package ships disabled). Nothing imports these automatically, so normal builds, the CI
+pipeline, and packaging are unaffected.
+
+**Known gap:** SonarCloud reported one new issue on PR #87 that this configuration does not
+reproduce, and sonarcloud.io is not reachable from the agent sandbox to identify it. The rule
+behind it is either absent from the analyzer package or shipped disabled and not listed in the
+globalconfig. If you have dashboard access, add it — the calibration is only as good as the rules
+it names.
+
## Project Structure
- **CodeBlocker/**: Main library - an `IndentedTextWriter` wrapper for generating code blocks with automatic indentation
@@ -25,14 +55,24 @@ dotnet test --logger "console;verbosity=detailed"
## Architecture
-The library consists of three main classes:
+The library is built around these types:
1. **`CodeBlocker`** (`CodeBlocker/CodeBlocker.cs`): Wraps `System.CodeDom.Compiler.IndentedTextWriter` to provide simplified code generation with:
- Factory methods (`Create()`, `Create(string indentString)`) that manage `StringWriter` lifecycle
+ - Constructors over any `TextWriter`, for streaming straight to a file — such a writer stays the
+ caller's to dispose, and `IsBuffered`/`ToString()` only work over a `StringWriter`
- Indentation control via `Indent()`, `Outdent()`, and `CurrentIndent` property
- Output methods: `Write()`, `WriteLine()`, `NewLine()`
- Implements `IDisposable` with proper resource cleanup
+ **Line endings.** `IndentedTextWriter` terminates lines with `Environment.NewLine`;
+ `CodeBlocker` deliberately does not. `DefaultNewLineString` is `NewLines.Lf`, so output is
+ byte-identical on every platform — generated code is committed, diffed and compared against
+ golden files, all of which want reproducibility over the local convention. `NewLines.Host` is
+ the opt-in for the platform terminator. Tests must therefore assert against
+ `CodeBlocker.DefaultNewLineString`, never `Environment.NewLine`: the latter passes on Linux
+ for the wrong reason and hides a Windows break.
+
2. **`Scope`** (`CodeBlocker/Scope.cs`): Extends `ktsu.ScopedAction` to provide automatic brace handling:
- On creation: writes `{` and increases indent
- On disposal: decreases indent and writes `}`
@@ -43,6 +83,22 @@ The library consists of three main classes:
- On disposal: decreases indent and writes `};`
- Useful for C/C++ enum declarations, struct initializers, etc.
+4. **Other scopes** (`CodeBlocker/Scopes.cs`): `DelimiterScope` and its `ParenScope`/`BracketScope`
+ derivations, plus `IndentScope`, `RegionScope`, `DirectiveScope` and `PragmaScope`.
+
+5. **Preamble helpers** (`CodeBlocker/CodeBlockerExtensions.cs`): one call each for the
+ auto-generated marker, the nullable context, the file-scoped namespace and the using directives.
+
+6. **Template object model** (`CodeBlocker/Templates/`): `SourceFileTemplate`, `ClassTemplate`,
+ `MethodTemplate`, `PropertyTemplate`, `OperatorTemplate` and friends describe a source file as
+ objects and own all the punctuation, spacing and indentation. Rendering lives in the internal
+ `TemplateRendering`; a `BodyFactory` writes only the body, with no leading separator.
+
+ When emitting a multi-line fragment inside a template, route it through
+ `TemplateRendering.SpliceFragment` rather than `NewLine()`/`WriteLineNoTabs` —
+ `IndentedTextWriter.WriteLineNoTabs` does not re-arm the pending-tab flag, so the next line
+ silently lands at column 0.
+
## SDK and Dependencies
This project uses:
diff --git a/CodeBlocker.Test/CodeBlockerTests.cs b/CodeBlocker.Test/CodeBlockerTests.cs
index eecc4bf..6ae4b06 100644
--- a/CodeBlocker.Test/CodeBlockerTests.cs
+++ b/CodeBlocker.Test/CodeBlockerTests.cs
@@ -51,7 +51,7 @@ public void WriteLineShouldAddLineWithIndentation()
// Assert
- Assert.AreEqual("test line" + Environment.NewLine, result);
+ Assert.AreEqual("test line" + CodeBlocker.DefaultNewLineString, result);
}
[TestMethod]
@@ -68,7 +68,7 @@ public void NewLineShouldAddEmptyLine()
// Assert
- Assert.AreEqual(Environment.NewLine, result);
+ Assert.AreEqual(CodeBlocker.DefaultNewLineString, result);
}
[TestMethod]
@@ -86,7 +86,7 @@ public void WriteLineWithIndentationShouldRespectIndentLevel()
// Assert
- Assert.AreEqual("\tindented line" + Environment.NewLine, result);
+ Assert.AreEqual("\tindented line" + CodeBlocker.DefaultNewLineString, result);
}
[TestMethod]
@@ -107,7 +107,7 @@ public void MultipleLinesShouldMaintainProperIndentation()
// Assert
- string expected = "line 1" + Environment.NewLine + "\tline 2 indented" + Environment.NewLine + "line 3" + Environment.NewLine;
+ string expected = "line 1" + CodeBlocker.DefaultNewLineString + "\tline 2 indented" + CodeBlocker.DefaultNewLineString + "line 3" + CodeBlocker.DefaultNewLineString;
Assert.AreEqual(expected, result);
}
@@ -154,7 +154,7 @@ public void CreateWithCustomIndentStringShouldUseSpecifiedIndent()
// Assert
- Assert.AreEqual(" test line" + Environment.NewLine, result);
+ Assert.AreEqual(" test line" + CodeBlocker.DefaultNewLineString, result);
Assert.AreEqual(customIndent, codeBlocker.IndentString);
}
@@ -175,7 +175,7 @@ public void ConstructorWithCustomIndentStringShouldWork()
// Assert
- Assert.AreEqual(" indented content" + Environment.NewLine, result);
+ Assert.AreEqual(" indented content" + CodeBlocker.DefaultNewLineString, result);
Assert.AreEqual(customIndent, codeBlocker.IndentString);
}
@@ -211,7 +211,7 @@ public void CustomIndentStringWithMultipleIndentLevels()
// Assert
- string expected = "level 0" + Environment.NewLine + ">>level 1" + Environment.NewLine + ">>>>level 2" + Environment.NewLine;
+ string expected = "level 0" + CodeBlocker.DefaultNewLineString + ">>level 1" + CodeBlocker.DefaultNewLineString + ">>>>level 2" + CodeBlocker.DefaultNewLineString;
Assert.AreEqual(expected, result);
Assert.AreEqual(customIndent, codeBlocker.IndentString);
}
@@ -232,7 +232,7 @@ public void WriteLineWithoutParametersShouldAddEmptyLineWithIndentation()
// Assert
- Assert.AreEqual("\t" + Environment.NewLine, result);
+ Assert.AreEqual("\t" + CodeBlocker.DefaultNewLineString, result);
}
[TestMethod]
@@ -287,7 +287,7 @@ public void CurrentIndentSetterShouldUpdateIndentationLevel()
// Assert
Assert.AreEqual(3, codeBlocker.CurrentIndent);
- Assert.AreEqual("\t\t\ttest line" + Environment.NewLine, result);
+ Assert.AreEqual("\t\t\ttest line" + CodeBlocker.DefaultNewLineString, result);
}
[TestMethod]
@@ -308,7 +308,7 @@ public void CurrentIndentSetterWithZeroShouldRemoveIndentation()
// Assert
Assert.AreEqual(0, codeBlocker.CurrentIndent);
- Assert.AreEqual("no indent" + Environment.NewLine, result);
+ Assert.AreEqual("no indent" + CodeBlocker.DefaultNewLineString, result);
}
[TestMethod]
@@ -340,7 +340,7 @@ public void CreateWithNullIndentStringShouldWork()
// Assert - Should work with null indent string (treated as default)
Assert.IsNotNull(result);
- Assert.IsTrue(result.Contains("test" + Environment.NewLine, StringComparison.Ordinal), "Result should contain test line with a line terminator");
+ Assert.IsTrue(result.Contains("test" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain test line with a line terminator");
}
[TestMethod]
@@ -425,7 +425,7 @@ public void MixedWriteAndWriteLineShouldFormatCorrectly()
// Assert
- string expected = "start middle end" + Environment.NewLine + "new line" + Environment.NewLine;
+ string expected = "start middle end" + CodeBlocker.DefaultNewLineString + "new line" + CodeBlocker.DefaultNewLineString;
Assert.AreEqual(expected, result);
}
@@ -450,7 +450,7 @@ public void DeepIndentationStressTest()
// Assert
Assert.AreEqual(maxDepth, codeBlocker.CurrentIndent);
- Assert.IsTrue(result.StartsWith(new string('\t', maxDepth) + "deeply nested" + Environment.NewLine, StringComparison.Ordinal), "Result should start with deeply nested content prefixed by correct number of tabs");
+ Assert.IsTrue(result.StartsWith(new string('\t', maxDepth) + "deeply nested" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should start with deeply nested content prefixed by correct number of tabs");
}
[TestMethod]
@@ -469,7 +469,7 @@ public void LargeStringContentShouldBeHandledCorrectly()
// Assert
Assert.IsTrue(result.Contains(largeString, StringComparison.Ordinal), "Result should contain the large string content");
- Assert.IsTrue(result.EndsWith(Environment.NewLine, StringComparison.Ordinal), "Result should end with a line terminator");
+ Assert.IsTrue(result.EndsWith(CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should end with a line terminator");
}
[TestMethod]
@@ -485,7 +485,7 @@ public void EmptyIndentStringShouldWork()
// Assert
Assert.AreEqual(string.Empty, codeBlocker.IndentString);
- Assert.AreEqual("test" + Environment.NewLine, result); // No indentation with empty string
+ Assert.AreEqual("test" + CodeBlocker.DefaultNewLineString, result); // No indentation with empty string
}
@@ -506,6 +506,6 @@ public void VeryLongIndentStringShouldWork()
// Assert
Assert.AreEqual(longIndent, codeBlocker.IndentString);
- Assert.AreEqual(longIndent + "test" + Environment.NewLine, result);
+ Assert.AreEqual(longIndent + "test" + CodeBlocker.DefaultNewLineString, result);
}
}
diff --git a/CodeBlocker.Test/IntegrationTests.cs b/CodeBlocker.Test/IntegrationTests.cs
index d03a954..115436e 100644
--- a/CodeBlocker.Test/IntegrationTests.cs
+++ b/CodeBlocker.Test/IntegrationTests.cs
@@ -39,22 +39,22 @@ public void ComplexCodeGenerationShouldFormatCorrectly()
// Assert
string result = codeBlocker.ToString();
- string expected = "public class TestClass" + Environment.NewLine +
- "{" + Environment.NewLine +
- "\tpublic void Method1()" + Environment.NewLine +
- "\t{" + Environment.NewLine +
- "\t\tvar x = 1;" + Environment.NewLine +
- "\t\tConsole.WriteLine(x);" + Environment.NewLine +
- "\t}" + Environment.NewLine +
- Environment.NewLine +
- "\tpublic void Method2()" + Environment.NewLine +
- "\t{" + Environment.NewLine +
- "\t\tif (true)" + Environment.NewLine +
- "\t\t{" + Environment.NewLine +
- "\t\t\treturn;" + Environment.NewLine +
- "\t\t}" + Environment.NewLine +
- "\t}" + Environment.NewLine +
- "}" + Environment.NewLine;
+ string expected = "public class TestClass" + CodeBlocker.DefaultNewLineString +
+ "{" + CodeBlocker.DefaultNewLineString +
+ "\tpublic void Method1()" + CodeBlocker.DefaultNewLineString +
+ "\t{" + CodeBlocker.DefaultNewLineString +
+ "\t\tvar x = 1;" + CodeBlocker.DefaultNewLineString +
+ "\t\tConsole.WriteLine(x);" + CodeBlocker.DefaultNewLineString +
+ "\t}" + CodeBlocker.DefaultNewLineString +
+ CodeBlocker.DefaultNewLineString +
+ "\tpublic void Method2()" + CodeBlocker.DefaultNewLineString +
+ "\t{" + CodeBlocker.DefaultNewLineString +
+ "\t\tif (true)" + CodeBlocker.DefaultNewLineString +
+ "\t\t{" + CodeBlocker.DefaultNewLineString +
+ "\t\t\treturn;" + CodeBlocker.DefaultNewLineString +
+ "\t\t}" + CodeBlocker.DefaultNewLineString +
+ "\t}" + CodeBlocker.DefaultNewLineString +
+ "}" + CodeBlocker.DefaultNewLineString;
Assert.AreEqual(expected, result);
}
@@ -85,10 +85,10 @@ public void DeepNestingShouldMaintainCorrectIndentation()
// Verify it contains the expected structure
- Assert.IsTrue(result.Contains("start" + Environment.NewLine, StringComparison.Ordinal), "Result should contain 'start' line");
- Assert.IsTrue(result.Contains("level 1" + Environment.NewLine, StringComparison.Ordinal), "Result should contain 'level 1' line");
- Assert.IsTrue(result.Contains("level 5" + Environment.NewLine, StringComparison.Ordinal), "Result should contain 'level 5' line");
- Assert.IsTrue(result.Contains("end" + Environment.NewLine, StringComparison.Ordinal), "Result should contain 'end' line");
+ Assert.IsTrue(result.Contains("start" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain 'start' line");
+ Assert.IsTrue(result.Contains("level 1" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain 'level 1' line");
+ Assert.IsTrue(result.Contains("level 5" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain 'level 5' line");
+ Assert.IsTrue(result.Contains("end" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain 'end' line");
// Count opening and closing braces to ensure they match
@@ -134,14 +134,14 @@ public void MixedContentTypesShouldFormatCorrectly()
// Verify structure
- Assert.IsTrue(result.StartsWith("// Header comment" + Environment.NewLine, StringComparison.Ordinal), "Result should start with header comment");
- Assert.IsTrue(result.Contains("namespace TestNamespace" + Environment.NewLine, StringComparison.Ordinal), "Result should contain namespace declaration");
- Assert.IsTrue(result.Contains("\tusing System;" + Environment.NewLine, StringComparison.Ordinal), "Result should contain indented using directive");
- Assert.IsTrue(result.Contains("\tpublic interface ITest" + Environment.NewLine, StringComparison.Ordinal), "Result should contain interface declaration");
- Assert.IsTrue(result.Contains("\t\tvoid DoSomething();" + Environment.NewLine, StringComparison.Ordinal), "Result should contain interface method with double indentation");
- Assert.IsTrue(result.Contains("\tpublic class Test : ITest" + Environment.NewLine, StringComparison.Ordinal), "Result should contain class declaration");
- Assert.IsTrue(result.Contains("\t\tpublic void DoSomething()" + Environment.NewLine, StringComparison.Ordinal), "Result should contain class method with double indentation");
- Assert.IsTrue(result.Contains("\t\t\t// Implementation" + Environment.NewLine, StringComparison.Ordinal), "Result should contain implementation comment with triple indentation");
+ Assert.IsTrue(result.StartsWith("// Header comment" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should start with header comment");
+ Assert.IsTrue(result.Contains("namespace TestNamespace" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain namespace declaration");
+ Assert.IsTrue(result.Contains("\tusing System;" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain indented using directive");
+ Assert.IsTrue(result.Contains("\tpublic interface ITest" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain interface declaration");
+ Assert.IsTrue(result.Contains("\t\tvoid DoSomething();" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain interface method with double indentation");
+ Assert.IsTrue(result.Contains("\tpublic class Test : ITest" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain class declaration");
+ Assert.IsTrue(result.Contains("\t\tpublic void DoSomething()" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain class method with double indentation");
+ Assert.IsTrue(result.Contains("\t\t\t// Implementation" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain implementation comment with triple indentation");
}
[TestMethod]
@@ -166,7 +166,7 @@ public void EmptyScopesShouldNotAffectOtherContent()
// Assert
string result = codeBlocker.ToString();
- string expected = "before" + Environment.NewLine + "{" + Environment.NewLine + "}" + Environment.NewLine + "after" + Environment.NewLine;
+ string expected = "before" + CodeBlocker.DefaultNewLineString + "{" + CodeBlocker.DefaultNewLineString + "}" + CodeBlocker.DefaultNewLineString + "after" + CodeBlocker.DefaultNewLineString;
Assert.AreEqual(expected, result);
}
@@ -197,8 +197,8 @@ public void MultipleCodeBlockersShouldBeIndependent()
string result1 = codeBlocker1.ToString();
string result2 = codeBlocker2.ToString();
- string expected1 = "codeBlocker1 content" + Environment.NewLine + "{" + Environment.NewLine + "\tinside scope1" + Environment.NewLine + "}" + Environment.NewLine;
- string expected2 = "codeBlocker2 content" + Environment.NewLine + "{" + Environment.NewLine + "\tinside scope2" + Environment.NewLine + "}" + Environment.NewLine;
+ string expected1 = "codeBlocker1 content" + CodeBlocker.DefaultNewLineString + "{" + CodeBlocker.DefaultNewLineString + "\tinside scope1" + CodeBlocker.DefaultNewLineString + "}" + CodeBlocker.DefaultNewLineString;
+ string expected2 = "codeBlocker2 content" + CodeBlocker.DefaultNewLineString + "{" + CodeBlocker.DefaultNewLineString + "\tinside scope2" + CodeBlocker.DefaultNewLineString + "}" + CodeBlocker.DefaultNewLineString;
Assert.AreEqual(expected1, result1);
Assert.AreEqual(expected2, result2);
@@ -254,13 +254,13 @@ public void ComplexTemplateGenerationWithMultipleIndentTypesShouldWork()
// Verify HTML uses 2-space indentation
- Assert.IsTrue(htmlResult.Contains(" " + Environment.NewLine, StringComparison.Ordinal), "HTML result should contain head tag with 2-space indentation");
- Assert.IsTrue(htmlResult.Contains(" Test Page" + Environment.NewLine, StringComparison.Ordinal), "HTML result should contain title tag with 4-space indentation");
+ Assert.IsTrue(htmlResult.Contains(" " + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "HTML result should contain head tag with 2-space indentation");
+ Assert.IsTrue(htmlResult.Contains(" Test Page" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "HTML result should contain title tag with 4-space indentation");
// Verify JS uses tab indentation
- Assert.IsTrue(jsResult.Contains("\tconst content = document.getElementById('content');" + Environment.NewLine, StringComparison.Ordinal), "JS result should contain const declaration with tab indentation");
- Assert.IsTrue(jsResult.Contains("\t\tcontent.addEventListener('click', handleClick);" + Environment.NewLine, StringComparison.Ordinal), "JS result should contain addEventListener with double tab indentation");
+ Assert.IsTrue(jsResult.Contains("\tconst content = document.getElementById('content');" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "JS result should contain const declaration with tab indentation");
+ Assert.IsTrue(jsResult.Contains("\t\tcontent.addEventListener('click', handleClick);" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "JS result should contain addEventListener with double tab indentation");
}
[TestMethod]
@@ -296,15 +296,15 @@ public void MixedWriteOperationsWithComplexIndentationShouldFormatCorrectly()
string result = codeBlocker.ToString();
string expected =
- "public class MyClass : BaseClass" + Environment.NewLine +
- "{" + Environment.NewLine +
- "\tprivate readonly string _field;" + Environment.NewLine +
- Environment.NewLine +
- "\tpublic MyClass(string field)" + Environment.NewLine +
- "\t{" + Environment.NewLine +
- "\t\t_field = field ?? throw new ArgumentNullException(nameof(field));" + Environment.NewLine +
- "\t}" + Environment.NewLine +
- "}" + Environment.NewLine;
+ "public class MyClass : BaseClass" + CodeBlocker.DefaultNewLineString +
+ "{" + CodeBlocker.DefaultNewLineString +
+ "\tprivate readonly string _field;" + CodeBlocker.DefaultNewLineString +
+ CodeBlocker.DefaultNewLineString +
+ "\tpublic MyClass(string field)" + CodeBlocker.DefaultNewLineString +
+ "\t{" + CodeBlocker.DefaultNewLineString +
+ "\t\t_field = field ?? throw new ArgumentNullException(nameof(field));" + CodeBlocker.DefaultNewLineString +
+ "\t}" + CodeBlocker.DefaultNewLineString +
+ "}" + CodeBlocker.DefaultNewLineString;
Assert.AreEqual(expected, result);
}
@@ -355,11 +355,11 @@ public void LargeScaleCodeGenerationShouldPerformReasonably()
// Verify structure exists
- Assert.IsTrue(result.Contains("namespace LargeTest" + Environment.NewLine, StringComparison.Ordinal), "Result should contain namespace declaration");
- Assert.IsTrue(result.Contains("public class Class0" + Environment.NewLine, StringComparison.Ordinal), "Result should contain first class declaration");
- Assert.IsTrue(result.Contains($"public class Class{classCount - 1}{Environment.NewLine}", StringComparison.Ordinal), "Result should contain last class declaration");
- Assert.IsTrue(result.Contains("public void Method0()" + Environment.NewLine, StringComparison.Ordinal), "Result should contain first method declaration");
- Assert.IsTrue(result.Contains($"public void Method{methodsPerClass - 1}(){Environment.NewLine}", StringComparison.Ordinal), "Result should contain last method declaration");
+ Assert.IsTrue(result.Contains("namespace LargeTest" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain namespace declaration");
+ Assert.IsTrue(result.Contains("public class Class0" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain first class declaration");
+ Assert.IsTrue(result.Contains($"public class Class{classCount - 1}{CodeBlocker.DefaultNewLineString}", StringComparison.Ordinal), "Result should contain last class declaration");
+ Assert.IsTrue(result.Contains("public void Method0()" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain first method declaration");
+ Assert.IsTrue(result.Contains($"public void Method{methodsPerClass - 1}(){CodeBlocker.DefaultNewLineString}", StringComparison.Ordinal), "Result should contain last method declaration");
// Verify performance (should complete in reasonable time)
@@ -406,11 +406,11 @@ public void SharedStringWriterBetweenCodeBlockersShouldWork()
// Verify both CodeBlockers wrote to the same StringWriter
- Assert.IsTrue(result.Contains("// First CodeBlocker" + Environment.NewLine, StringComparison.Ordinal), "Result should contain first CodeBlocker comment");
- Assert.IsTrue(result.Contains("\tcontent from first" + Environment.NewLine, StringComparison.Ordinal), "Result should contain first CodeBlocker content with tab indent");
+ Assert.IsTrue(result.Contains("// First CodeBlocker" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain first CodeBlocker comment");
+ Assert.IsTrue(result.Contains("\tcontent from first" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain first CodeBlocker content with tab indent");
- Assert.IsTrue(result.Contains("// Second CodeBlocker with different indent" + Environment.NewLine, StringComparison.Ordinal), "Result should contain second CodeBlocker comment");
- Assert.IsTrue(result.Contains(" content from second" + Environment.NewLine, StringComparison.Ordinal), "Result should contain second CodeBlocker content with 2-space indent");
+ Assert.IsTrue(result.Contains("// Second CodeBlocker with different indent" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain second CodeBlocker comment");
+ Assert.IsTrue(result.Contains(" content from second" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain second CodeBlocker content with 2-space indent");
}
@@ -446,8 +446,8 @@ public void ErrorRecoveryAfterExceptionShouldNotAffectFutureOperations()
}
string result = codeBlocker.ToString();
- Assert.IsTrue(result.Contains("recovered content" + Environment.NewLine, StringComparison.Ordinal), "Result should contain recovered content after error");
- Assert.IsTrue(result.Contains("scope content" + Environment.NewLine, StringComparison.Ordinal), "Result should contain scope content after recovery");
+ Assert.IsTrue(result.Contains("recovered content" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain recovered content after error");
+ Assert.IsTrue(result.Contains("scope content" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain scope content after recovery");
}
[TestMethod]
@@ -471,9 +471,9 @@ public void UnicodeAndSpecialCharactersShouldBeHandledCorrectly()
string result = codeBlocker.ToString();
- Assert.IsTrue(result.Contains("// Unicode test: αβγδε 中文 🚀" + Environment.NewLine, StringComparison.Ordinal), "Result should contain Unicode comment with Greek, Chinese, and emoji characters");
- Assert.IsTrue(result.Contains("→→string text = \"Hello 世界!\";" + Environment.NewLine, StringComparison.Ordinal), "Result should contain string with Chinese characters and Unicode arrow indent");
- Assert.IsTrue(result.Contains("→→char symbol = '€';" + Environment.NewLine, StringComparison.Ordinal), "Result should contain Euro symbol with Unicode arrow indent");
+ Assert.IsTrue(result.Contains("// Unicode test: αβγδε 中文 🚀" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain Unicode comment with Greek, Chinese, and emoji characters");
+ Assert.IsTrue(result.Contains("→→string text = \"Hello 世界!\";" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain string with Chinese characters and Unicode arrow indent");
+ Assert.IsTrue(result.Contains("→→char symbol = '€';" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain Euro symbol with Unicode arrow indent");
Assert.IsTrue(result.Contains("→→// Special chars: \t\r\n\\\"", StringComparison.Ordinal), "Result should contain special characters with Unicode arrow indent");
}
}
diff --git a/CodeBlocker.Test/NewLineTests.cs b/CodeBlocker.Test/NewLineTests.cs
index a9e0184..9f68d14 100644
--- a/CodeBlocker.Test/NewLineTests.cs
+++ b/CodeBlocker.Test/NewLineTests.cs
@@ -19,12 +19,25 @@ namespace CodeBlocker.Tests;
public sealed class NewLineTests
{
[TestMethod]
- public void DefaultNewLineStringIsTheHostTerminator()
+ public void TheDefaultTerminatorIsLineFeedRatherThanTheHostTerminator()
{
+ // The point of the default: the same calls give the same bytes on every platform. This
+ // assertion is only meaningful on a host whose terminator is not LF, so it is written to
+ // fail loudly there rather than to pass vacuously everywhere.
using CodeBlocker codeBlocker = CodeBlocker.Create();
- Assert.AreEqual(NewLines.Host, codeBlocker.NewLineString);
- Assert.AreEqual(Environment.NewLine, codeBlocker.NewLineString);
+ Assert.AreEqual(NewLines.Lf, codeBlocker.NewLineString);
+ Assert.AreEqual(CodeBlocker.DefaultNewLineString, codeBlocker.NewLineString);
+ }
+
+ [TestMethod]
+ public void TheHostTerminatorIsStillAvailableByAskingForIt()
+ {
+ using CodeBlocker codeBlocker = CodeBlocker.Create(CodeBlocker.DefaultIndentString, NewLines.Host);
+
+ codeBlocker.WriteLine("a");
+
+ Assert.AreEqual($"a{Environment.NewLine}", codeBlocker.ToString());
}
[TestMethod]
@@ -64,11 +77,11 @@ public void NewLineStringIsReportedBackVerbatim()
}
[TestMethod]
- public void NullNewLineStringFallsBackToTheHostTerminator()
+ public void NullNewLineStringFallsBackToTheDefault()
{
using CodeBlocker codeBlocker = CodeBlocker.Create(CodeBlocker.DefaultIndentString, null!);
- Assert.AreEqual(NewLines.Host, codeBlocker.NewLineString);
+ Assert.AreEqual(CodeBlocker.DefaultNewLineString, codeBlocker.NewLineString);
}
[TestMethod]
diff --git a/CodeBlocker.Test/ScopeTests.cs b/CodeBlocker.Test/ScopeTests.cs
index 52c7db6..3592340 100644
--- a/CodeBlocker.Test/ScopeTests.cs
+++ b/CodeBlocker.Test/ScopeTests.cs
@@ -24,7 +24,7 @@ public void ConstructorShouldOpenBraceAndIncreaseIndentation()
Assert.AreEqual(initialIndent + 1, codeBlocker.CurrentIndent);
string result = codeBlocker.ToString();
- Assert.IsTrue(result.Contains("{" + Environment.NewLine, StringComparison.Ordinal), "Result should contain opening brace with newline");
+ Assert.IsTrue(result.Contains("{" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain opening brace with newline");
}
[TestMethod]
@@ -44,7 +44,7 @@ public void DisposeShouldCloseBraceAndDecreaseIndentation()
Assert.AreEqual(initialIndent, codeBlocker.CurrentIndent);
string result = codeBlocker.ToString();
- Assert.IsTrue(result.EndsWith("}" + Environment.NewLine, StringComparison.Ordinal), "Result should end with closing brace and newline");
+ Assert.IsTrue(result.EndsWith("}" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should end with closing brace and newline");
}
[TestMethod]
@@ -64,7 +64,7 @@ public void UsingStatementShouldProperlyOpenAndCloseScope()
// Assert
string result = codeBlocker.ToString();
- string expected = "{" + Environment.NewLine + "\tcontent inside scope" + Environment.NewLine + "}" + Environment.NewLine;
+ string expected = "{" + CodeBlocker.DefaultNewLineString + "\tcontent inside scope" + CodeBlocker.DefaultNewLineString + "}" + CodeBlocker.DefaultNewLineString;
Assert.AreEqual(expected, result);
}
@@ -90,7 +90,7 @@ public void NestedScopesShouldMaintainProperIndentation()
// Assert
string result = codeBlocker.ToString();
- string expected = "{" + Environment.NewLine + "\tlevel 1" + Environment.NewLine + "\t{" + Environment.NewLine + "\t\tlevel 2" + Environment.NewLine + "\t}" + Environment.NewLine + "\tback to level 1" + Environment.NewLine + "}" + Environment.NewLine;
+ string expected = "{" + CodeBlocker.DefaultNewLineString + "\tlevel 1" + CodeBlocker.DefaultNewLineString + "\t{" + CodeBlocker.DefaultNewLineString + "\t\tlevel 2" + CodeBlocker.DefaultNewLineString + "\t}" + CodeBlocker.DefaultNewLineString + "\tback to level 1" + CodeBlocker.DefaultNewLineString + "}" + CodeBlocker.DefaultNewLineString;
Assert.AreEqual(expected, result);
}
@@ -126,7 +126,7 @@ public void ScopeWithoutContentShouldStillFormatCorrectly()
// Assert
string result = codeBlocker.ToString();
- string expected = "{" + Environment.NewLine + "}" + Environment.NewLine;
+ string expected = "{" + CodeBlocker.DefaultNewLineString + "}" + CodeBlocker.DefaultNewLineString;
Assert.AreEqual(expected, result);
}
@@ -152,7 +152,7 @@ public void MultipleSequentialScopesShouldFormatCorrectly()
// Assert
string result = codeBlocker.ToString();
- string expected = "{" + Environment.NewLine + "\tscope 1 content" + Environment.NewLine + "}" + Environment.NewLine + "{" + Environment.NewLine + "\tscope 2 content" + Environment.NewLine + "}" + Environment.NewLine;
+ string expected = "{" + CodeBlocker.DefaultNewLineString + "\tscope 1 content" + CodeBlocker.DefaultNewLineString + "}" + CodeBlocker.DefaultNewLineString + "{" + CodeBlocker.DefaultNewLineString + "\tscope 2 content" + CodeBlocker.DefaultNewLineString + "}" + CodeBlocker.DefaultNewLineString;
Assert.AreEqual(expected, result);
}
@@ -175,7 +175,7 @@ public void ScopeWithCustomIndentStringShouldWork()
// Assert
string result = codeBlocker.ToString();
- string expected = "{" + Environment.NewLine + " custom indented content" + Environment.NewLine + "}" + Environment.NewLine;
+ string expected = "{" + CodeBlocker.DefaultNewLineString + " custom indented content" + CodeBlocker.DefaultNewLineString + "}" + CodeBlocker.DefaultNewLineString;
Assert.AreEqual(expected, result);
Assert.AreEqual(customIndent, codeBlocker.IndentString);
}
@@ -228,8 +228,8 @@ public void ScopeWithVeryDeepNestingShouldWork()
// Assert
string result = codeBlocker.ToString();
- Assert.IsTrue(result.Contains("level 0" + Environment.NewLine, StringComparison.Ordinal), "Result should contain first nesting level");
- Assert.IsTrue(result.Contains($"level {nestingLevels - 1}{Environment.NewLine}", StringComparison.Ordinal), "Result should contain last nesting level");
+ Assert.IsTrue(result.Contains("level 0" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain first nesting level");
+ Assert.IsTrue(result.Contains($"level {nestingLevels - 1}{CodeBlocker.DefaultNewLineString}", StringComparison.Ordinal), "Result should contain last nesting level");
// Count braces to ensure they match
@@ -266,7 +266,7 @@ public void ScopeWithMixedManualIndentAndScopeIndentShouldWork()
// Note: After manual Outdent within scope, the closing }; will be at the current indent level
// The scope ends at whatever the current indent is when Dispose() is called
- string expected = "\t{" + Environment.NewLine + "\t\tdouble indented" + Environment.NewLine + "\tsingle indented" + Environment.NewLine + "}" + Environment.NewLine + "back to manual indent" + Environment.NewLine;
+ string expected = "\t{" + CodeBlocker.DefaultNewLineString + "\t\tdouble indented" + CodeBlocker.DefaultNewLineString + "\tsingle indented" + CodeBlocker.DefaultNewLineString + "}" + CodeBlocker.DefaultNewLineString + "back to manual indent" + CodeBlocker.DefaultNewLineString;
Assert.AreEqual(expected, result);
}
@@ -292,10 +292,10 @@ public void ScopeWithCurrentIndentSetterShouldWork()
Assert.IsGreaterThanOrEqualTo(0, codeBlocker.CurrentIndent); // Should be reasonable value
string result = codeBlocker.ToString();
- Assert.IsTrue(result.Contains("{" + Environment.NewLine, StringComparison.Ordinal), "Result should contain opening brace with newline");
- Assert.IsTrue(result.Contains("\t\t\t\t\tlevel 5 content" + Environment.NewLine, StringComparison.Ordinal), "Result should contain content with 5 tabs indentation");
+ Assert.IsTrue(result.Contains("{" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain opening brace with newline");
+ Assert.IsTrue(result.Contains("\t\t\t\t\tlevel 5 content" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain content with 5 tabs indentation");
- Assert.IsTrue(result.EndsWith("}" + Environment.NewLine, StringComparison.Ordinal), "Result should end with closing brace and newline");
+ Assert.IsTrue(result.EndsWith("}" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should end with closing brace and newline");
}
[TestMethod]
@@ -345,7 +345,7 @@ public void ScopeWithEmptyCustomIndentStringShouldWork()
// Assert
string result = codeBlocker.ToString();
- string expected = "{" + Environment.NewLine + "no indent" + Environment.NewLine + "}" + Environment.NewLine;
+ string expected = "{" + CodeBlocker.DefaultNewLineString + "no indent" + CodeBlocker.DefaultNewLineString + "}" + CodeBlocker.DefaultNewLineString;
Assert.AreEqual(expected, result);
}
diff --git a/CodeBlocker.Test/ScopeWithTrailingSemicolonTests.cs b/CodeBlocker.Test/ScopeWithTrailingSemicolonTests.cs
index 08fe5f8..ed31592 100644
--- a/CodeBlocker.Test/ScopeWithTrailingSemicolonTests.cs
+++ b/CodeBlocker.Test/ScopeWithTrailingSemicolonTests.cs
@@ -24,7 +24,7 @@ public void ConstructorShouldOpenBraceAndIncreaseIndentation()
Assert.AreEqual(initialIndent + 1, codeBlocker.CurrentIndent);
string result = codeBlocker.ToString();
- Assert.IsTrue(result.Contains("{" + Environment.NewLine, StringComparison.Ordinal), "Result should contain opening brace with newline");
+ Assert.IsTrue(result.Contains("{" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should contain opening brace with newline");
}
[TestMethod]
@@ -44,7 +44,7 @@ public void DisposeShouldCloseBraceWithSemicolonAndDecreaseIndentation()
Assert.AreEqual(initialIndent, codeBlocker.CurrentIndent);
string result = codeBlocker.ToString();
- Assert.IsTrue(result.EndsWith("};" + Environment.NewLine, StringComparison.Ordinal), "Result should end with closing brace, semicolon, and newline");
+ Assert.IsTrue(result.EndsWith("};" + CodeBlocker.DefaultNewLineString, StringComparison.Ordinal), "Result should end with closing brace, semicolon, and newline");
}
[TestMethod]
@@ -64,7 +64,7 @@ public void UsingStatementShouldProperlyOpenAndCloseScope()
// Assert
string result = codeBlocker.ToString();
- string expected = "{" + Environment.NewLine + "\tcontent inside scope" + Environment.NewLine + "};" + Environment.NewLine;
+ string expected = "{" + CodeBlocker.DefaultNewLineString + "\tcontent inside scope" + CodeBlocker.DefaultNewLineString + "};" + CodeBlocker.DefaultNewLineString;
Assert.AreEqual(expected, result);
}
@@ -90,7 +90,7 @@ public void NestedScopesShouldMaintainProperIndentation()
// Assert
string result = codeBlocker.ToString();
- string expected = "{" + Environment.NewLine + "\tlevel 1" + Environment.NewLine + "\t{" + Environment.NewLine + "\t\tlevel 2" + Environment.NewLine + "\t};" + Environment.NewLine + "\tback to level 1" + Environment.NewLine + "};" + Environment.NewLine;
+ string expected = "{" + CodeBlocker.DefaultNewLineString + "\tlevel 1" + CodeBlocker.DefaultNewLineString + "\t{" + CodeBlocker.DefaultNewLineString + "\t\tlevel 2" + CodeBlocker.DefaultNewLineString + "\t};" + CodeBlocker.DefaultNewLineString + "\tback to level 1" + CodeBlocker.DefaultNewLineString + "};" + CodeBlocker.DefaultNewLineString;
Assert.AreEqual(expected, result);
}
@@ -125,7 +125,7 @@ public void ScopeWithoutContentShouldStillFormatCorrectly()
// Assert
string result = codeBlocker.ToString();
- string expected = "{" + Environment.NewLine + "};" + Environment.NewLine;
+ string expected = "{" + CodeBlocker.DefaultNewLineString + "};" + CodeBlocker.DefaultNewLineString;
Assert.AreEqual(expected, result);
}
@@ -148,7 +148,7 @@ public void ScopeWithCustomIndentStringShouldWork()
// Assert
string result = codeBlocker.ToString();
- string expected = "{" + Environment.NewLine + " custom indented content" + Environment.NewLine + "};" + Environment.NewLine;
+ string expected = "{" + CodeBlocker.DefaultNewLineString + " custom indented content" + CodeBlocker.DefaultNewLineString + "};" + CodeBlocker.DefaultNewLineString;
Assert.AreEqual(expected, result);
Assert.AreEqual(customIndent, codeBlocker.IndentString);
}
@@ -203,18 +203,18 @@ public void MixedWithRegularScopeShouldWork()
string result = codeBlocker.ToString();
string expected =
- "namespace Test" + Environment.NewLine +
- "{" + Environment.NewLine +
- "\tpublic class Example" + Environment.NewLine +
- "\t{" + Environment.NewLine +
- "\t\tpublic enum Color" + Environment.NewLine +
- "\t\t{" + Environment.NewLine +
- "\t\t\tRed," + Environment.NewLine +
- "\t\t\tGreen," + Environment.NewLine +
- "\t\t\tBlue" + Environment.NewLine +
- "\t\t};" + Environment.NewLine +
- "\t}" + Environment.NewLine +
- "}" + Environment.NewLine;
+ "namespace Test" + CodeBlocker.DefaultNewLineString +
+ "{" + CodeBlocker.DefaultNewLineString +
+ "\tpublic class Example" + CodeBlocker.DefaultNewLineString +
+ "\t{" + CodeBlocker.DefaultNewLineString +
+ "\t\tpublic enum Color" + CodeBlocker.DefaultNewLineString +
+ "\t\t{" + CodeBlocker.DefaultNewLineString +
+ "\t\t\tRed," + CodeBlocker.DefaultNewLineString +
+ "\t\t\tGreen," + CodeBlocker.DefaultNewLineString +
+ "\t\t\tBlue" + CodeBlocker.DefaultNewLineString +
+ "\t\t};" + CodeBlocker.DefaultNewLineString +
+ "\t}" + CodeBlocker.DefaultNewLineString +
+ "}" + CodeBlocker.DefaultNewLineString;
Assert.AreEqual(expected, result);
}
@@ -236,7 +236,7 @@ public void ScopeWithEmptyCustomIndentStringShouldWork()
// Assert
string result = codeBlocker.ToString();
- string expected = "{" + Environment.NewLine + "no indent" + Environment.NewLine + "};" + Environment.NewLine;
+ string expected = "{" + CodeBlocker.DefaultNewLineString + "no indent" + CodeBlocker.DefaultNewLineString + "};" + CodeBlocker.DefaultNewLineString;
Assert.AreEqual(expected, result);
}
}
diff --git a/CodeBlocker.Test/TextWriterTests.cs b/CodeBlocker.Test/TextWriterTests.cs
index 1f356fc..7dc99d2 100644
--- a/CodeBlocker.Test/TextWriterTests.cs
+++ b/CodeBlocker.Test/TextWriterTests.cs
@@ -89,7 +89,7 @@ public void AWriterCreateOwnsIsDisposed()
// Writing after disposal would throw if the StringWriter were still open; instead the
// already-buffered text is all that remains readable.
- Assert.AreEqual($"a{Environment.NewLine}", codeBlocker.ToString());
+ Assert.AreEqual($"a{CodeBlocker.DefaultNewLineString}", codeBlocker.ToString());
}
[TestMethod]
@@ -123,7 +123,7 @@ public void ToStringReturnsTheTypeNameWhenThereIsNothingBuffered()
// Documented behaviour: no copy is kept, and ToString does not throw, so debuggers and
// diagnostics stay safe. The generated code is read from the writer instead.
Assert.AreEqual(typeof(CodeBlocker).ToString(), codeBlocker.ToString());
- Assert.AreEqual("a" + Environment.NewLine, target.ToString());
+ Assert.AreEqual("a" + CodeBlocker.DefaultNewLineString, target.ToString());
}
[TestMethod]
diff --git a/CodeBlocker/CodeBlocker.cs b/CodeBlocker/CodeBlocker.cs
index eb62d3b..d27313c 100644
--- a/CodeBlocker/CodeBlocker.cs
+++ b/CodeBlocker/CodeBlocker.cs
@@ -13,6 +13,18 @@ public class CodeBlocker : IDisposable
/// The indent string used when none is specified: a single tab.
public const string DefaultIndentString = "\t";
+ ///
+ /// The line terminator used when none is specified: a line feed.
+ ///
+ ///
+ /// LF rather than the host's terminator, so the same calls produce the same bytes wherever they
+ /// run. That is what generated code almost always needs: it gets written to a file, committed,
+ /// diffed, or compared against a golden file, and every one of those wants reproducibility more
+ /// than it wants the local convention. Pass explicitly for the old
+ /// behaviour.
+ ///
+ public const string DefaultNewLineString = NewLines.Lf;
+
private readonly TextWriter writer;
private bool disposedValue;
@@ -29,10 +41,8 @@ public class CodeBlocker : IDisposable
/// Get the line terminator written at the end of every line.
///
///
- /// Defaults to , which makes output depend on the operating system it
- /// was produced on. Generators whose output is committed to a repository should pass an explicit
- /// terminator — or — so the same input
- /// always produces the same bytes.
+ /// Defaults to . Pass to follow the
+ /// operating system's convention instead, at the cost of output that differs by platform.
///
public string NewLineString { get; }
@@ -60,7 +70,7 @@ public class CodeBlocker : IDisposable
///
/// The to write to.
public CodeBlocker(StringWriter stringWriter)
- : this((TextWriter)stringWriter, DefaultIndentString, NewLines.Host)
+ : this((TextWriter)stringWriter, DefaultIndentString, DefaultNewLineString)
{
}
@@ -70,7 +80,7 @@ public CodeBlocker(StringWriter stringWriter)
/// The to write to.
/// The string to use for indentation.
public CodeBlocker(StringWriter stringWriter, string indentString)
- : this((TextWriter)stringWriter, indentString, NewLines.Host)
+ : this((TextWriter)stringWriter, indentString, DefaultNewLineString)
{
}
@@ -81,7 +91,7 @@ public CodeBlocker(StringWriter stringWriter, string indentString)
/// The string to use for indentation.
///
/// The line terminator to write at the end of every line. selects
- /// .
+ /// .
///
public CodeBlocker(StringWriter stringWriter, string indentString, string newLineString)
: this((TextWriter)stringWriter, indentString, newLineString)
@@ -97,7 +107,7 @@ public CodeBlocker(StringWriter stringWriter, string indentString, string newLin
/// that makes for itself is disposed here.
///
public CodeBlocker(TextWriter writer)
- : this(writer, DefaultIndentString, NewLines.Host)
+ : this(writer, DefaultIndentString, DefaultNewLineString)
{
}
@@ -108,7 +118,7 @@ public CodeBlocker(TextWriter writer)
/// The to write to.
/// The string to use for indentation.
public CodeBlocker(TextWriter writer, string indentString)
- : this(writer, indentString, NewLines.Host)
+ : this(writer, indentString, DefaultNewLineString)
{
}
@@ -120,7 +130,7 @@ public CodeBlocker(TextWriter writer, string indentString)
/// The string to use for indentation.
///
/// The line terminator to write at the end of every line. selects
- /// .
+ /// .
///
/// is .
public CodeBlocker(TextWriter writer, string indentString, string newLineString)
@@ -129,7 +139,7 @@ public CodeBlocker(TextWriter writer, string indentString, string newLineString)
// indentString is deliberately not null-checked: a null indent has always meant "no
// indentation" here, and CreateWithNullIndentStringShouldWork pins that behaviour.
- newLineString ??= NewLines.Host;
+ newLineString ??= DefaultNewLineString;
this.writer = writer;
IndentString = indentString;
@@ -150,14 +160,14 @@ public CodeBlocker(TextWriter writer, string indentString, string newLineString)
/// Create a new instance of .
///
/// A new instance of .
- public static CodeBlocker Create() => Create(DefaultIndentString, NewLines.Host);
+ public static CodeBlocker Create() => Create(DefaultIndentString, DefaultNewLineString);
///
/// Create a new instance of with a custom indent string.
///
/// The string to use for indentation.
/// A new instance of .
- public static CodeBlocker Create(string indentString) => Create(indentString, NewLines.Host);
+ public static CodeBlocker Create(string indentString) => Create(indentString, DefaultNewLineString);
///
/// Create a new instance of with a custom indent string and line terminator.
diff --git a/CodeBlocker/NewLines.cs b/CodeBlocker/NewLines.cs
index 8834a24..295d94e 100644
--- a/CodeBlocker/NewLines.cs
+++ b/CodeBlocker/NewLines.cs
@@ -21,8 +21,8 @@ public static class NewLines
///
/// The host operating system's line terminator. This is what a
- /// uses by default, and therefore what falls back to when no line
- /// terminator is specified — which makes the output depend on where it was produced.
+ /// uses by default; deliberately does not, because it makes output
+ /// depend on where it was produced. Pass it explicitly when that is what you want.
///
public static string Host => System.Environment.NewLine;
}
diff --git a/README.md b/README.md
index b327b62..999786d 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ CodeBlocker is a specialized utility built on top of `IndentedTextWriter` that s
- **Automatic Indentation**: Properly manages indentation levels as you create nested code blocks
- **Configurable Indentation**: Support for custom indent strings (tabs, spaces, or any custom pattern)
-- **Configurable Line Endings**: Pin the line terminator so the same calls produce byte-identical output on every platform
+- **Deterministic Line Endings**: Lines end with LF by default, so the same calls produce byte-identical output on every platform; pin CRLF or the host terminator when you want them
- **Scope Management**: Uses C# `using` statements for clean, readable scope creation with automatic brace handling powered by `ktsu.ScopedAction`, with optional trailing semicolons via `ScopeWithTrailingSemicolon`
- **More Than Braces**: Parenthesis, bracket, bare-indent, `#region`, `#if` and `#pragma warning` scopes, each balanced by disposal
- **Preamble Helpers**: One call each for the auto-generated marker, the nullable context, the namespace declaration, and the using directives
@@ -200,7 +200,7 @@ internal class TemplateExample
file.Classes.Add(money);
- using CodeBlocker codeBlocker = CodeBlocker.Create(CodeBlocker.DefaultIndentString, NewLines.Lf);
+ using CodeBlocker codeBlocker = CodeBlocker.Create();
codeBlocker.AddSourceFile(file);
return codeBlocker.ToString();
}
@@ -297,7 +297,7 @@ internal class ScopesExample
{
public static string GenerateCode()
{
- using CodeBlocker codeBlocker = CodeBlocker.Create(CodeBlocker.DefaultIndentString, NewLines.Lf);
+ using CodeBlocker codeBlocker = CodeBlocker.Create();
codeBlocker.WriteLine("public class Example");
using (new Scope(codeBlocker))
@@ -349,7 +349,7 @@ public class Example
The lines at the top of a generated file are the same every time, so they get one call each. Every helper that conventionally has a blank line after it writes that blank line, which keeps the spacing consistent no matter which parts a given generator emits.
```csharp
-using CodeBlocker codeBlocker = CodeBlocker.Create(CodeBlocker.DefaultIndentString, NewLines.Lf);
+using CodeBlocker codeBlocker = CodeBlocker.Create();
codeBlocker
.WriteAutoGeneratedHeader("Copyright (c) 2023-2026 ktsu-dev contributors")
@@ -388,7 +388,7 @@ internal class FileExample
public static void GenerateToFile(string path)
{
using StreamWriter file = new(path);
- using CodeBlocker codeBlocker = new(file, CodeBlocker.DefaultIndentString, NewLines.Lf);
+ using CodeBlocker codeBlocker = new(file);
codeBlocker.WriteLine("public class Example");
using (new Scope(codeBlocker))
@@ -406,9 +406,7 @@ Two things to know:
### Line Endings
-`CodeBlocker` writes through `IndentedTextWriter`, which terminates lines with `Environment.NewLine`. That makes output depend on the machine that produced it — the same calls give you CRLF on Windows and LF everywhere else.
-
-If your generated code is committed to a repository, or compared against a golden file, pin the terminator instead:
+`CodeBlocker` terminates lines with a line feed unless you say otherwise. Generated code is nearly always committed to a repository or compared against a golden file, and both of those want the same bytes out of every machine — so the default is the deterministic terminator rather than the local convention:
```csharp
namespace CodeBlockerExample;
@@ -419,8 +417,8 @@ internal class DeterministicExample
{
public static string GenerateCode()
{
- // Byte-identical on every platform.
- using CodeBlocker codeBlocker = CodeBlocker.Create(CodeBlocker.DefaultIndentString, NewLines.Lf);
+ // Byte-identical on every platform: LF, on Windows too.
+ using CodeBlocker codeBlocker = CodeBlocker.Create();
codeBlocker.WriteLine("public class Example");
using (new Scope(codeBlocker))
@@ -433,15 +431,21 @@ internal class DeterministicExample
}
```
-The `NewLines` class names the usual choices:
+This is the one place `CodeBlocker` departs from the `IndentedTextWriter` it writes through, which uses `Environment.NewLine`.
+
+The `NewLines` class names the usual choices, and any other string works too — the terminator is written verbatim:
| Name | Value | Notes |
|------|-------|-------|
-| `NewLines.Lf` | `"\n"` | The conventional choice for reproducible output |
+| `NewLines.Lf` | `"\n"` | The default, exposed as `CodeBlocker.DefaultNewLineString` |
| `NewLines.CrLf` | `"\r\n"` | Use when the target repository stores `.cs` files with CRLF |
-| `NewLines.Host` | `Environment.NewLine` | The default, and the one that varies by platform |
+| `NewLines.Host` | `Environment.NewLine` | Opt in when the output is for this machine rather than for a repository |
-Any other string works too — the terminator is written verbatim.
+To follow the host convention, ask for it:
+
+```csharp
+using CodeBlocker codeBlocker = CodeBlocker.Create(CodeBlocker.DefaultIndentString, NewLines.Host);
+```
### Advanced Usage
@@ -497,15 +501,22 @@ string result = codeBlocker.ToString();
The main class for building indented code blocks.
+#### Constants
+
+| Name | Value | Description |
+|------|-------|-------------|
+| `DefaultIndentString` | `"\t"` | The indent written per level when none is specified |
+| `DefaultNewLineString` | `NewLines.Lf` | The line terminator used when none is specified |
+
#### Constructors
| Name | Description |
|------|-------------|
-| `CodeBlocker(StringWriter stringWriter)` | Creates a new CodeBlocker with the specified StringWriter using tab indentation |
-| `CodeBlocker(StringWriter stringWriter, string indentString)` | Creates a new CodeBlocker with the specified StringWriter and custom indent string |
+| `CodeBlocker(StringWriter stringWriter)` | Creates a new CodeBlocker with the specified StringWriter using tab indentation and LF line endings |
+| `CodeBlocker(StringWriter stringWriter, string indentString)` | Creates a new CodeBlocker with the specified StringWriter and custom indent string, still LF |
| `CodeBlocker(StringWriter stringWriter, string indentString, string newLineString)` | As above, and pins the line terminator written at the end of every line |
-| `CodeBlocker(TextWriter writer)` | Creates a new CodeBlocker over any TextWriter using tab indentation |
-| `CodeBlocker(TextWriter writer, string indentString)` | Creates a new CodeBlocker over any TextWriter with a custom indent string |
+| `CodeBlocker(TextWriter writer)` | Creates a new CodeBlocker over any TextWriter using tab indentation and LF line endings |
+| `CodeBlocker(TextWriter writer, string indentString)` | Creates a new CodeBlocker over any TextWriter with a custom indent string, still LF |
| `CodeBlocker(TextWriter writer, string indentString, string newLineString)` | As above, and pins the line terminator |
#### Properties
@@ -514,7 +525,7 @@ The main class for building indented code blocks.
|------|------|-------------|
| `CurrentIndent` | `int` | Gets or sets the current indentation level |
| `IndentString` | `string` | Gets the current indent string being used (e.g., "\t", " ", " ") |
-| `NewLineString` | `string` | Gets the line terminator written at the end of every line |
+| `NewLineString` | `string` | Gets the line terminator written at the end of every line; `DefaultNewLineString` (LF) unless one was passed |
| `IsBuffered` | `bool` | Whether `ToString()` can return the generated code, i.e. whether the underlying writer is a `StringWriter` |
#### Methods
@@ -528,8 +539,8 @@ The main class for building indented code blocks.
| `Indent()` | `void` | Increases the indent level |
| `Outdent()` | `void` | Decreases the indent level |
| `ToString()` | `string` | Returns the generated code as a string |
-| `Create()` | `CodeBlocker` | Static factory method to create a new CodeBlocker instance with tab indentation |
-| `Create(string indentString)` | `CodeBlocker` | Static factory method to create a new CodeBlocker instance with custom indentation |
+| `Create()` | `CodeBlocker` | Static factory method to create a new CodeBlocker instance with tab indentation and LF line endings |
+| `Create(string indentString)` | `CodeBlocker` | Static factory method to create a new CodeBlocker instance with custom indentation, still LF |
| `Create(string indentString, string newLineString)` | `CodeBlocker` | Static factory method to create a new CodeBlocker instance with custom indentation and a pinned line terminator |
| `Dispose()` | `void` | Disposes of the CodeBlocker and underlying resources |
@@ -602,7 +613,7 @@ Named line terminators. See [Line Endings](#line-endings).
| Name | Value |
|------|-------|
-| `Lf` | `"\n"` |
+| `Lf` | `"\n"` (the default — see `CodeBlocker.DefaultNewLineString`) |
| `CrLf` | `"\r\n"` |
| `Host` | `Environment.NewLine` |