Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Document.FilePath to be set to null #74290

Merged
merged 1 commit into from
Jul 8, 2024
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
18 changes: 7 additions & 11 deletions src/Workspaces/Core/Portable/Workspace/Solution/Document.cs
Original file line number Diff line number Diff line change
Expand Up @@ -383,41 +383,37 @@ async Task<SemanticModel> GetSemanticModelWorkerAsync()
/// Creates a new instance of this document updated to have the source code kind specified.
/// </summary>
public Document WithSourceCodeKind(SourceCodeKind kind)
=> this.Project.Solution.WithDocumentSourceCodeKind(this.Id, kind).GetDocument(this.Id)!;
=> this.Project.Solution.WithDocumentSourceCodeKind(this.Id, kind).GetRequiredDocument(Id);

/// <summary>
/// Creates a new instance of this document updated to have the text specified.
/// </summary>
public Document WithText(SourceText text)
=> this.Project.Solution.WithDocumentText(this.Id, text, PreservationMode.PreserveIdentity).GetDocument(this.Id)!;
=> this.Project.Solution.WithDocumentText(this.Id, text, PreservationMode.PreserveIdentity).GetRequiredDocument(Id);

/// <summary>
/// Creates a new instance of this document updated to have a syntax tree rooted by the specified syntax node.
/// </summary>
public Document WithSyntaxRoot(SyntaxNode root)
=> this.Project.Solution.WithDocumentSyntaxRoot(this.Id, root, PreservationMode.PreserveIdentity).GetDocument(this.Id)!;
=> this.Project.Solution.WithDocumentSyntaxRoot(this.Id, root, PreservationMode.PreserveIdentity).GetRequiredDocument(Id);

/// <summary>
/// Creates a new instance of this document updated to have the specified name.
/// </summary>
public Document WithName(string name)
=> this.Project.Solution.WithDocumentName(this.Id, name).GetDocument(this.Id)!;
=> this.Project.Solution.WithDocumentName(this.Id, name).GetRequiredDocument(Id);

/// <summary>
/// Creates a new instance of this document updated to have the specified folders.
/// </summary>
public Document WithFolders(IEnumerable<string> folders)
=> this.Project.Solution.WithDocumentFolders(this.Id, folders).GetDocument(this.Id)!;
=> this.Project.Solution.WithDocumentFolders(this.Id, folders).GetRequiredDocument(Id);

/// <summary>
/// Creates a new instance of this document updated to have the specified file path.
/// </summary>
/// <param name="filePath"></param>
// TODO (https://github.com/dotnet/roslyn/issues/37125): Solution.WithDocumentFilePath will throw if
// filePath is null, but it's odd because we *do* support null file paths. Why can't you switch a
// document back to null?
public Document WithFilePath(string filePath)
=> this.Project.Solution.WithDocumentFilePath(this.Id, filePath).GetDocument(this.Id)!;
public Document WithFilePath(string? filePath)
=> this.Project.Solution.WithDocumentFilePath(this.Id, filePath).GetRequiredDocument(Id);

/// <summary>
/// Get the text changes between this document and a prior version of the same document.
Expand Down
59 changes: 23 additions & 36 deletions src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -350,15 +350,7 @@ public DocumentState UpdateParseOptions(ParseOptions options, bool onlyPreproces

private DocumentState SetParseOptions(ParseOptions options, bool onlyPreprocessorDirectiveChange)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}

if (!SupportsSyntaxTree)
{
throw new InvalidOperationException();
}
Contract.ThrowIfFalse(SupportsSyntaxTree);

ITreeAndVersionSource? newTreeSource = null;

Expand Down Expand Up @@ -413,42 +405,37 @@ public DocumentState UpdateSourceCodeKind(SourceCodeKind kind)
return this.SetParseOptions(this.ParseOptions.WithKind(kind), onlyPreprocessorDirectiveChange: false);
}

// TODO: https://github.com/dotnet/roslyn/issues/37125
// if FilePath is null, then this will change the name of the underlying tree, but we aren't producing a new tree in that case.
public DocumentState UpdateName(string name)
=> UpdateAttributes(Attributes.With(name: name));

public DocumentState UpdateFilePath(string? path)
=> UpdateAttributes(Attributes.With(filePath: path));

public DocumentState UpdateFolders(IReadOnlyList<string> folders)
=> UpdateAttributes(Attributes.With(folders: folders));

private DocumentState UpdateAttributes(DocumentInfo.DocumentAttributes attributes)
{
Debug.Assert(attributes != Attributes);

return new DocumentState(
LanguageServices,
Services,
attributes,
_options,
TextAndVersionSource,
LoadTextOptions,
_treeSource);
}

public DocumentState UpdateFilePath(string? filePath)
private DocumentState UpdateAttributes(DocumentInfo.DocumentAttributes newAttributes)
{
var newAttributes = Attributes.With(filePath: filePath);
Debug.Assert(newAttributes != Attributes);

// TODO: it's overkill to fully reparse the tree if we had the tree already; all we have to do is update the
// file path and diagnostic options for that tree.
var newTreeSource = SupportsSyntaxTree ?
CreateLazyFullyParsedTree(
TextAndVersionSource,
LoadTextOptions,
newAttributes.SyntaxTreeFilePath,
_options,
LanguageServices) : null;
ITreeAndVersionSource? newTreeSource;

if (newAttributes.SyntaxTreeFilePath != Attributes.SyntaxTreeFilePath)
{
// TODO: it's overkill to fully reparse the tree if we had the tree already; all we have to do is update the
// file path and diagnostic options for that tree.
newTreeSource = SupportsSyntaxTree ?
CreateLazyFullyParsedTree(
TextAndVersionSource,
LoadTextOptions,
newAttributes.SyntaxTreeFilePath,
_options,
LanguageServices) : null;
}
else
{
newTreeSource = _treeSource;
}

return new DocumentState(
LanguageServices,
Expand Down
11 changes: 1 addition & 10 deletions src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1169,18 +1169,9 @@ public Solution WithDocumentFolders(DocumentId documentId, IEnumerable<string>?
/// <summary>
/// Creates a new solution instance with the document specified updated to have the specified file path.
/// </summary>
public Solution WithDocumentFilePath(DocumentId documentId, string filePath)
public Solution WithDocumentFilePath(DocumentId documentId, string? filePath)
{
CheckContainsDocument(documentId);

// TODO (https://github.com/dotnet/roslyn/issues/37125):
// We *do* support null file paths. Why can't you switch a document back to null?
// See DocumentState.GetSyntaxTreeFilePath
if (filePath == null)
{
throw new ArgumentNullException(nameof(filePath));
}

return WithCompilationState(_compilationState.WithDocumentFilePath(documentId, filePath));
}

Expand Down
5 changes: 1 addition & 4 deletions src/Workspaces/Core/Portable/Workspace/Workspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1088,10 +1088,7 @@ protected internal void OnDocumentInfoChanged(DocumentId documentId, DocumentInf

if (oldAttributes.FilePath != newInfo.FilePath)
{
// TODO (https://github.com/dotnet/roslyn/issues/37125): Solution.WithDocumentFilePath will throw if
// filePath is null, but it's odd because we *do* support null file paths. The suppression here is to silence it
// but should be removed when the bug is fixed.
newSolution = newSolution.WithDocumentFilePath(documentId, newInfo.FilePath!);
newSolution = newSolution.WithDocumentFilePath(documentId, newInfo.FilePath);
}

if (oldAttributes.SourceCodeKind != newInfo.SourceCodeKind)
Expand Down
71 changes: 66 additions & 5 deletions src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,19 +226,19 @@ public void WithDocumentFilePath()
var path = "new path";

var newSolution1 = solution.WithDocumentFilePath(documentId, path);
Assert.Equal(path, newSolution1.GetDocument(documentId)!.FilePath);
Assert.Equal(path, newSolution1.GetRequiredDocument(documentId).FilePath);
AssertEx.Equal(new[] { documentId }, newSolution1.GetDocumentIdsWithFilePath(path));

var newSolution2 = newSolution1.WithDocumentFilePath(documentId, path);
Assert.Same(newSolution1, newSolution2);

// empty path (TODO https://github.com/dotnet/roslyn/issues/37125):
var newSolution3 = solution.WithDocumentFilePath(documentId, "");
Assert.Equal("", newSolution3.GetDocument(documentId)!.FilePath);
Assert.Equal("", newSolution3.GetRequiredDocument(documentId).FilePath);
Assert.Empty(newSolution3.GetDocumentIdsWithFilePath(""));

// TODO: https://github.com/dotnet/roslyn/issues/37125
Assert.Throws<ArgumentNullException>(() => solution.WithDocumentFilePath(documentId, filePath: null!));
var newSolution4 = solution.WithDocumentFilePath(documentId, null);
Assert.Null(newSolution4.GetRequiredDocument(documentId).FilePath);
Assert.Empty(newSolution4.GetDocumentIdsWithFilePath(null));

Assert.Throws<ArgumentNullException>(() => solution.WithDocumentFilePath(null!, path));
Assert.Throws<InvalidOperationException>(() => solution.WithDocumentFilePath(s_unrelatedDocumentId, path));
Expand Down Expand Up @@ -1341,6 +1341,67 @@ public async Task ChangingPreprocessorDirectivesMayReparse(string source, bool e
Assert.Equal(expectReuse, oldRoot.IsIncrementallyIdenticalTo(newTree.GetRoot()));
}

[Theory]
[InlineData(null, "test.cs")]
[InlineData("test.cs", null)]
[InlineData("", null)]
[InlineData("test.cs", "")]
public async Task ChangingFilePathReparses(string oldPath, string newPath)
{
var projectId = ProjectId.CreateNewId();
var documentId = DocumentId.CreateNewId(projectId);

using var workspace = CreateWorkspace();
var document = workspace.CurrentSolution
.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp)
.AddDocument(documentId, name: "Test.cs", text: "// File", filePath: oldPath)
.GetRequiredDocument(documentId);

var oldTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None);
var newDocument = document.WithFilePath(newPath);
var newTree = await newDocument.GetRequiredSyntaxTreeAsync(CancellationToken.None);

Assert.False(oldTree.GetRoot().IsIncrementallyIdenticalTo(newTree.GetRoot()));
}

[Fact]
public async Task ChangingName_ReparsesWhenPathIsNull()
{
var projectId = ProjectId.CreateNewId();
var documentId = DocumentId.CreateNewId(projectId);

using var workspace = CreateWorkspace();
var document = workspace.CurrentSolution
.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp)
.AddDocument(documentId, name: "name1", text: "// File", filePath: null)
.GetRequiredDocument(documentId);

var oldTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None);
var newDocument = document.WithName("name2");
var newTree = await newDocument.GetRequiredSyntaxTreeAsync(CancellationToken.None);

Assert.False(oldTree.GetRoot().IsIncrementallyIdenticalTo(newTree.GetRoot()));
}

[Fact]
public async Task ChangingName_NoReparse()
{
var projectId = ProjectId.CreateNewId();
var documentId = DocumentId.CreateNewId(projectId);

using var workspace = CreateWorkspace();
var document = workspace.CurrentSolution
.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp)
.AddDocument(documentId, name: "name1", text: "// File", filePath: "")
.GetRequiredDocument(documentId);

var oldTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None);
var newDocument = document.WithName("name2");
var newTree = await newDocument.GetRequiredSyntaxTreeAsync(CancellationToken.None);

Assert.True(oldTree.GetRoot().IsIncrementallyIdenticalTo(newTree.GetRoot()));
}

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