Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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 @@ -77,7 +77,6 @@ void ReportErrorFor(SyntaxTrivia trivia)
}
}

// The result should be ordered by source location, RemoveDirectivesFromFile depends on that.
return builder.ToImmutable();
}

Expand Down Expand Up @@ -112,7 +111,7 @@ public static void FindLeadingDirectives(
{
TextSpan span = GetFullSpan(previousWhiteSpaceSpan, trivia);

var whiteSpace = GetWhiteSpaceInfo(triviaList, index);
var whiteSpace = GetWhiteSpaceInfo(triviaList, index, span);
var info = new CSharpDirective.ParseInfo
{
SourceFile = sourceFile,
Expand All @@ -134,7 +133,7 @@ public static void FindLeadingDirectives(
var value = parts.Length > 1 ? parts[1] : "";
Debug.Assert(!(parts.Length > 2));

var whiteSpace = GetWhiteSpaceInfo(triviaList, index);
var whiteSpace = GetWhiteSpaceInfo(triviaList, index, span);
var context = new CSharpDirective.ParseContext
{
Info = new()
Expand Down Expand Up @@ -183,35 +182,42 @@ static TextSpan GetFullSpan(TextSpan previousWhiteSpaceSpan, SyntaxTrivia trivia
return previousWhiteSpaceSpan.IsEmpty ? trivia.FullSpan : TextSpan.FromBounds(previousWhiteSpaceSpan.Start, trivia.FullSpan.End);
}

static (WhiteSpaceInfo Leading, WhiteSpaceInfo Trailing) GetWhiteSpaceInfo(in SyntaxTriviaList triviaList, int index)
static (WhiteSpaceInfo Leading, WhiteSpaceInfo Trailing) GetWhiteSpaceInfo(in SyntaxTriviaList triviaList, int index, TextSpan excludeSpan)
{
(WhiteSpaceInfo Leading, WhiteSpaceInfo Trailing) result = default;

for (int i = index - 1; i >= 0; i--)
{
if (!Fill(ref result.Leading, triviaList, i)) break;
if (!Fill(ref result.Leading, triviaList, i, excludeSpan)) break;
}

for (int i = index + 1; i < triviaList.Count; i++)
{
if (!Fill(ref result.Trailing, triviaList, i)) break;
if (!Fill(ref result.Trailing, triviaList, i, excludeSpan)) break;
}

return result;

static bool Fill(ref WhiteSpaceInfo info, in SyntaxTriviaList triviaList, int index)
static bool Fill(ref WhiteSpaceInfo info, in SyntaxTriviaList triviaList, int index, TextSpan excludeSpan)
{
var trivia = triviaList[index];

var length = trivia.FullSpan.Length - (trivia.FullSpan.Intersection(excludeSpan)?.Length ?? 0);

if (trivia.IsKind(SyntaxKind.EndOfLineTrivia))
{
info.LineBreaks += 1;
info.TotalLength += trivia.FullSpan.Length;
if (length != 0)
{
info.BlankLineLength += info.RestLength + length;
info.RestLength = 0;
}

return true;
}

if (trivia.IsKind(SyntaxKind.WhitespaceTrivia))
{
info.TotalLength += trivia.FullSpan.Length;
info.RestLength += length;
return true;
}

Expand Down Expand Up @@ -272,8 +278,15 @@ internal static partial class Patterns

internal struct WhiteSpaceInfo
{
public int LineBreaks;
public int TotalLength;
/// <summary>
/// Size of whitespace that consists of only blank lines (i.e., lines that contain only whitespace).
/// </summary>
public int BlankLineLength;

/// <summary>
/// Size of the remaining whitespace on a not-entirely-blank line.
/// </summary>
public int RestLength;
}

/// <summary>
Expand All @@ -287,11 +300,20 @@ internal abstract class CSharpDirective(in CSharpDirective.ParseInfo info)
public readonly struct ParseInfo
{
public required SourceFile SourceFile { get; init; }

/// <summary>
/// Span of the full line including the trailing line break.
/// </summary>
public required TextSpan Span { get; init; }

/// <summary>
/// Additional leading whitespace not included in <see cref="Span"/>.
/// </summary>
public required WhiteSpaceInfo LeadingWhiteSpace { get; init; }

/// <summary>
/// Additional trailing whitespace not included in <see cref="Span"/>.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checking my understanding: #: directives cannot have trailing // comments or similar? Is that right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's right, they cannot, everything on the same line as the directive is just its "content"

/// </summary>
public required WhiteSpaceInfo TrailingWhiteSpace { get; init; }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ Microsoft.DotNet.FileBasedPrograms.SourceFile.Text.init -> void
Microsoft.DotNet.FileBasedPrograms.SourceFile.WithPath(string! newPath) -> Microsoft.DotNet.FileBasedPrograms.SourceFile
Microsoft.DotNet.FileBasedPrograms.SourceFile.WithText(Microsoft.CodeAnalysis.Text.SourceText! newText) -> Microsoft.DotNet.FileBasedPrograms.SourceFile
Microsoft.DotNet.FileBasedPrograms.WhiteSpaceInfo
Microsoft.DotNet.FileBasedPrograms.WhiteSpaceInfo.LineBreaks -> int
Microsoft.DotNet.FileBasedPrograms.WhiteSpaceInfo.TotalLength -> int
Microsoft.DotNet.FileBasedPrograms.WhiteSpaceInfo.BlankLineLength -> int
Microsoft.DotNet.FileBasedPrograms.WhiteSpaceInfo.RestLength -> int
Microsoft.DotNet.FileBasedPrograms.WhiteSpaceInfo.WhiteSpaceInfo() -> void
Microsoft.DotNet.ProjectTools.ProjectLocator
override abstract Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ToString() -> string!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public override int Execute()
}
else
{
VirtualProjectBuilder.RemoveDirectivesFromFile(evaluatedDirectives, builder.EntryPointSourceFile, targetFile);
VirtualProjectBuildingCommand.RemoveDirectivesFromFile(builder.EntryPointSourceFile, targetFile);
}

// Create project file.
Expand Down Expand Up @@ -112,7 +112,7 @@ public override int Execute()
else
{
var sourceFile = SourceFile.Load(item.FullPath);
VirtualProjectBuilder.RemoveDirectivesFromFile(evaluatedDirectives, sourceFile, targetItemFullPath);
VirtualProjectBuildingCommand.RemoveDirectivesFromFile(sourceFile, targetItemFullPath);
}
}
else
Expand Down
28 changes: 19 additions & 9 deletions src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,27 +230,37 @@ public void Remove(CSharpDirective directive)
{
var span = directive.Info.Span;
var start = span.Start;
var length = span.Length + DetermineTrailingLengthToRemove(directive);
var length = span.Length;

DetermineWhiteSpaceToRemove(directive, out int leadingLength, out int trailingLength);
start -= leadingLength;
length += trailingLength;

SourceFile = SourceFile.WithText(SourceFile.Text.Replace(start: start, length: length, newText: string.Empty));
}

private static int DetermineTrailingLengthToRemove(CSharpDirective directive)
private static void DetermineWhiteSpaceToRemove(CSharpDirective directive, out int leadingLength, out int trailingLength)
{
// If there are blank lines both before and after the directive, remove the trailing white space.
if (directive.Info.LeadingWhiteSpace.LineBreaks > 0 && directive.Info.TrailingWhiteSpace.LineBreaks > 0)
// If there are blank lines both before and after the directive, remove the trailing blank lines.
if (directive.Info.LeadingWhiteSpace.BlankLineLength > 0 && directive.Info.TrailingWhiteSpace.BlankLineLength > 0)
{
return directive.Info.TrailingWhiteSpace.TotalLength;
leadingLength = 0;
trailingLength = directive.Info.TrailingWhiteSpace.BlankLineLength;
return;
}

// If the directive (including leading white space) starts at the beginning of the file,
// remove both the leading and trailing white space.
var startBeforeWhiteSpace = directive.Info.Span.Start - directive.Info.LeadingWhiteSpace.TotalLength;
// remove both the leading and trailing blank lines.
var startBeforeWhiteSpace = directive.Info.Span.Start - directive.Info.LeadingWhiteSpace.BlankLineLength;
if (startBeforeWhiteSpace == 0)
{
return directive.Info.LeadingWhiteSpace.TotalLength + directive.Info.TrailingWhiteSpace.TotalLength;
leadingLength = directive.Info.LeadingWhiteSpace.BlankLineLength;
trailingLength = directive.Info.TrailingWhiteSpace.BlankLineLength;
return;
}

Debug.Assert(startBeforeWhiteSpace > 0);
return 0;
leadingLength = 0;
trailingLength = 0;
}
}
18 changes: 18 additions & 0 deletions src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,24 @@ public static void CreateTempSubdirectory(string path)
static (sourceFile, textSpan, message, innerException) => throw new GracefulException(
$"{sourceFile.GetLocationString(textSpan)}: {FileBasedProgramsResources.DirectiveError}: {message}",
innerException);

public static SourceFile RemoveDirectivesFromFile(SourceFile sourceFile)
{
var editor = FileBasedAppSourceEditor.Load(sourceFile);

while (editor.Directives is [{ } directive, ..])
{
editor.Remove(directive);
}

return editor.SourceFile;
}

public static void RemoveDirectivesFromFile(SourceFile sourceFile, string targetFilePath)
{
var modifiedFile = RemoveDirectivesFromFile(sourceFile);
modifiedFile.WithPath(targetFilePath).Save();
}
}

internal sealed class RunFileBuildCacheEntry
Expand Down
34 changes: 0 additions & 34 deletions src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -753,38 +753,4 @@ static void WriteImport(TextWriter writer, string project, CSharpDirective.Sdk s
}
}
}

public static SourceFile RemoveDirectivesFromFile(ImmutableArray<CSharpDirective> directives, SourceFile sourceFile)

@jjonescz jjonescz Feb 24, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved these to VirtualProjectBuildingCommand because

  • they now need to use the FileBasedAppSourceEditor which isn't available in this project,
  • there is no need for these APIs to live in this shared project since they are not used by anyone but dotnet CLI (@tmat I assume you don't need these in dotnet-watch?).

{
if (directives.Length == 0)
{
return sourceFile;
}

#if DEBUG
var filteredDirectives = directives.Where(d => d.Info.SourceFile.Path == sourceFile.Path);
Debug.Assert(
filteredDirectives.OrderBy(static d => d.Info.Span.Start).SequenceEqual(filteredDirectives),
"Directives should be ordered by source location.");
#endif

var text = sourceFile.Text;

for (int i = directives.Length - 1; i >= 0; i--)
{
var directive = directives[i];
if (directive.Info.SourceFile.Path == sourceFile.Path)
{
text = text.Replace(directive.Info.Span, string.Empty);
}
}

return sourceFile.WithText(text);
}

public static void RemoveDirectivesFromFile(ImmutableArray<CSharpDirective> directives, SourceFile sourceFile, string targetFilePath)
{
var modifiedFile = RemoveDirectivesFromFile(directives, sourceFile);
modifiedFile.WithPath(targetFilePath).Save();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1645,7 +1645,6 @@ public void Directives_BlankLines()
""",
expectedProject: expectedProject,
expectedCSharp: """

Console.WriteLine();
""");

Expand Down Expand Up @@ -1968,7 +1967,7 @@ private static void Convert(

actualProject = projectWriter.ToString();

var convertedFile = VirtualProjectBuilder.RemoveDirectivesFromFile(directives, builder.EntryPointSourceFile);
var convertedFile = VirtualProjectBuildingCommand.RemoveDirectivesFromFile(builder.EntryPointSourceFile);
actualCSharp = convertedFile.Text != builder.EntryPointSourceFile.Text ? convertedFile.Text.ToString() : null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,51 @@ public void PreExistingWhiteSpace()
"""));
}

[Fact]
public void LeadingWhiteSpace()
{
Verify(
"""

#:package MyPackage@1.0.0
Console.WriteLine();
""",
(static editor => editor.Remove(editor.Directives.Single()),
"""

Console.WriteLine();
"""));

Verify(
"""
#:package MyPackage@1.0.0
Console.WriteLine();
""",
(static editor => editor.Remove(editor.Directives.Single()),
"""
Console.WriteLine();
"""));
}

[Fact]
public void WhiteSpaceOutsideLines()
{
Verify(
$"""
// trailing{" "}

#:package MyPackage@1.0.0

// leading
""",
(static editor => editor.Remove(editor.Directives.Single()),
$"""
// trailing{" "}

// leading
"""));
}

[Fact]
public void Comments()
{
Expand Down
Loading