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
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,8 @@ internal readonly record struct SourceFile(string Path, SourceText Text)
public static SourceFile Load(string filePath)
{
using var stream = File.OpenRead(filePath);
return new SourceFile(filePath, SourceText.From(stream, Encoding.UTF8));
// Let SourceText.From auto-detect the encoding (including BOM detection)
return new SourceFile(filePath, SourceText.From(stream, encoding: null));
}

public SourceFile WithText(SourceText newText)
Expand All @@ -269,7 +270,9 @@ public SourceFile WithText(SourceText newText)
public void Save()
{
using var stream = File.Open(Path, FileMode.Create, FileAccess.Write);
using var writer = new StreamWriter(stream, Encoding.UTF8);
// Use the encoding from SourceText, which preserves the original BOM state
var encoding = Text.Encoding ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
using var writer = new StreamWriter(stream, encoding);
Text.Write(writer);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,95 @@ public void RemoveMultiple()
"""));
}

/// <summary>
/// Verifies that files without UTF-8 BOM don't get one added when saved.
/// This is critical for shebang (#!) scripts on Unix-like systems.
/// <see href="https://github.com/dotnet/sdk/issues/52054"/>
/// </summary>
[Fact]
public void PreservesNoBomEncoding()
{
var testInstance = _testAssetsManager.CreateTestDirectory();
var tempFile = Path.Join(testInstance.Path, "test.cs");

// Create a file without BOM
var content = "#!/usr/bin/env dotnet run\nConsole.WriteLine();";
File.WriteAllText(tempFile, content, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));

// Load, modify, and save
var sourceFile = SourceFile.Load(tempFile);
var editor = FileBasedAppSourceEditor.Load(sourceFile);
editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" });
editor.SourceFile.Save();

// Verify no BOM was added
var bytes = File.ReadAllBytes(tempFile);
Assert.True(bytes is not [0xEF, 0xBB, 0xBF, ..],
"File should not have UTF-8 BOM");

// Verify the complete file content is correct
var savedContent = File.ReadAllText(tempFile);
var expectedContent = "#!/usr/bin/env dotnet run\n\n#:package [email protected]\n\nConsole.WriteLine();";
Assert.Equal(expectedContent, savedContent);
}

/// <summary>
/// Verifies that files with UTF-8 BOM preserve it when saved.
/// <see href="https://github.com/dotnet/sdk/issues/52054"/>
/// </summary>
[Fact]
public void PreservesBomEncoding()
{
var testInstance = _testAssetsManager.CreateTestDirectory();
var tempFile = Path.Join(testInstance.Path, "test.cs");

// Create a file with BOM
var content = "Console.WriteLine();";
File.WriteAllText(tempFile, content, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));

// Load, modify, and save
var sourceFile = SourceFile.Load(tempFile);
var editor = FileBasedAppSourceEditor.Load(sourceFile);
editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" });
editor.SourceFile.Save();

// Verify BOM is still present
var bytes = File.ReadAllBytes(tempFile);
Assert.True(bytes is [0xEF, 0xBB, 0xBF, ..],
"File should have UTF-8 BOM");
}

/// <summary>
/// Verifies that files with non-UTF-8 encodings (like UTF-16) preserve their encoding when saved.
/// <see href="https://github.com/dotnet/sdk/issues/52054"/>
/// </summary>
[Fact]
public void PreservesNonUtf8Encoding()
{
var testInstance = _testAssetsManager.CreateTestDirectory();
var tempFile = Path.Join(testInstance.Path, "test.cs");

// Create a file with UTF-16 encoding (includes BOM by default)
var content = "Console.WriteLine(\"UTF-16 test\");";
File.WriteAllText(tempFile, content, Encoding.Unicode);

// Load, modify, and save
var sourceFile = SourceFile.Load(tempFile);
var editor = FileBasedAppSourceEditor.Load(sourceFile);
editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" });
editor.SourceFile.Save();

// Verify UTF-16 BOM is still present (0xFF 0xFE for UTF-16 LE)
var bytes = File.ReadAllBytes(tempFile);
Assert.True(bytes is [0xFF, 0xFE, ..],
"File should have UTF-16 LE BOM");

// Verify content is still readable as UTF-16
var savedContent = File.ReadAllText(tempFile, Encoding.Unicode);
Assert.Contains("#:package [email protected]", savedContent);
Assert.Contains("Console.WriteLine", savedContent);
}

private void Verify(
string input,
params ReadOnlySpan<(Action<FileBasedAppSourceEditor> action, string expectedOutput)> verify)
Expand Down