Skip to content

Commit cba9a0b

Browse files
authored
Fix fallback project manager in the presence of linked files or shared projects (#9582)
* Add telemetry reporter dependency to the fallback project manager * Make sure we never bring down project loading if there is a problem with the fallback bits * Only support fallback documents that are in the project cone * Make things more efficient, and cross platform * Validate target paths * Fix tests linux and mac (even though the fallback project manager never runs on linux or mac)
1 parent f0a326f commit cba9a0b

File tree

4 files changed

+103
-41
lines changed

4 files changed

+103
-41
lines changed

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/FallbackProjectManager.cs

Lines changed: 60 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using System.Collections.Immutable;
66
using System.Composition;
77
using System.IO;
8+
using Microsoft.AspNetCore.Razor.Telemetry;
9+
using Microsoft.AspNetCore.Razor.Utilities;
810
using Microsoft.CodeAnalysis.Razor.Workspaces;
911

1012
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem;
@@ -16,49 +18,58 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem;
1618
/// </summary>
1719
[Shared]
1820
[Export(typeof(FallbackProjectManager))]
19-
internal sealed class FallbackProjectManager
21+
[method: ImportingConstructor]
22+
internal sealed class FallbackProjectManager(
23+
ProjectConfigurationFilePathStore projectConfigurationFilePathStore,
24+
LanguageServerFeatureOptions languageServerFeatureOptions,
25+
ProjectSnapshotManagerAccessor projectSnapshotManagerAccessor,
26+
ITelemetryReporter telemetryReporter)
2027
{
21-
private readonly ProjectConfigurationFilePathStore _projectConfigurationFilePathStore;
22-
private readonly LanguageServerFeatureOptions _languageServerFeatureOptions;
23-
private readonly ProjectSnapshotManagerAccessor _projectSnapshotManagerAccessor;
28+
private readonly ProjectConfigurationFilePathStore _projectConfigurationFilePathStore = projectConfigurationFilePathStore;
29+
private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions;
30+
private readonly ProjectSnapshotManagerAccessor _projectSnapshotManagerAccessor = projectSnapshotManagerAccessor;
31+
private readonly ITelemetryReporter _telemetryReporter = telemetryReporter;
2432

2533
private ImmutableHashSet<ProjectId> _fallbackProjectIds = ImmutableHashSet<ProjectId>.Empty;
2634

27-
[ImportingConstructor]
28-
public FallbackProjectManager(
29-
ProjectConfigurationFilePathStore projectConfigurationFilePathStore,
30-
LanguageServerFeatureOptions languageServerFeatureOptions,
31-
ProjectSnapshotManagerAccessor projectSnapshotManagerAccessor)
32-
{
33-
_projectConfigurationFilePathStore = projectConfigurationFilePathStore;
34-
_languageServerFeatureOptions = languageServerFeatureOptions;
35-
_projectSnapshotManagerAccessor = projectSnapshotManagerAccessor;
36-
}
37-
3835
internal void DynamicFileAdded(ProjectId projectId, ProjectKey razorProjectKey, string projectFilePath, string filePath)
3936
{
40-
if (_fallbackProjectIds.Contains(projectId))
37+
try
4138
{
42-
// If this is a fallback project, then Roslyn may not track documents in the project, so these dynamic file notifications
43-
// are the only way to know about files in the project.
44-
AddFallbackDocument(razorProjectKey, filePath, projectFilePath);
39+
if (_fallbackProjectIds.Contains(projectId))
40+
{
41+
// If this is a fallback project, then Roslyn may not track documents in the project, so these dynamic file notifications
42+
// are the only way to know about files in the project.
43+
AddFallbackDocument(razorProjectKey, filePath, projectFilePath);
44+
}
45+
else if (_projectSnapshotManagerAccessor.Instance.GetLoadedProject(razorProjectKey) is null)
46+
{
47+
// We have been asked to provide dynamic file info, which means there is a .razor or .cshtml file in the project
48+
// but for some reason our project system doesn't know about the project. In these cases (often when people don't
49+
// use the Razor or Web SDK) we spin up a fallback experience for them
50+
AddFallbackProject(projectId, filePath);
51+
}
4552
}
46-
else if (_projectSnapshotManagerAccessor.Instance.GetLoadedProject(razorProjectKey) is null)
53+
catch (Exception ex)
4754
{
48-
// We have been asked to provide dynamic file info, which means there is a .razor or .cshtml file in the project
49-
// but for some reason our project system doesn't know about the project. In these cases (often when people don't
50-
// use the Razor or Web SDK) we spin up a fallback experience for them
51-
AddFallbackProject(projectId, filePath);
55+
_telemetryReporter.ReportFault(ex, "Error while trying to add fallback document to project");
5256
}
5357
}
5458

5559
internal void DynamicFileRemoved(ProjectId projectId, string projectFilePath, string filePath)
5660
{
57-
if (_fallbackProjectIds.Contains(projectId))
61+
try
62+
{
63+
if (_fallbackProjectIds.Contains(projectId))
64+
{
65+
// If this is a fallback project, then Roslyn may not track documents in the project, so these dynamic file notifications
66+
// are the only way to know about files in the project.
67+
RemoveFallbackDocument(projectId, filePath, projectFilePath);
68+
}
69+
}
70+
catch (Exception ex)
5871
{
59-
// If this is a fallback project, then Roslyn may not track documents in the project, so these dynamic file notifications
60-
// are the only way to know about files in the project.
61-
RemoveFallbackDocument(projectId, filePath, projectFilePath);
72+
_telemetryReporter.ReportFault(ex, "Error while trying to remove fallback document from project");
6273
}
6374
}
6475

@@ -101,15 +112,27 @@ private void AddFallbackProject(ProjectId projectId, string filePath)
101112
private void AddFallbackDocument(ProjectKey projectKey, string filePath, string projectFilePath)
102113
{
103114
var hostDocument = CreateHostDocument(filePath, projectFilePath);
115+
if (hostDocument is null)
116+
{
117+
return;
118+
}
119+
104120
var textLoader = new FileTextLoader(filePath, defaultEncoding: null);
105121
_projectSnapshotManagerAccessor.Instance.DocumentAdded(projectKey, hostDocument, textLoader);
106122
}
107123

108-
private static HostDocument CreateHostDocument(string filePath, string projectFilePath)
124+
private static HostDocument? CreateHostDocument(string filePath, string projectFilePath)
109125
{
110-
var targetPath = filePath.StartsWith(projectFilePath, FilePathComparison.Instance)
111-
? filePath[projectFilePath.Length..]
112-
: filePath;
126+
// The compiler only supports paths that are relative to the project root, so filter our files
127+
// that don't match
128+
var projectPath = FilePathNormalizer.GetNormalizedDirectoryName(projectFilePath);
129+
var normalizedFilePath = FilePathNormalizer.Normalize(filePath);
130+
if (!normalizedFilePath.StartsWith(projectPath, FilePathComparison.Instance))
131+
{
132+
return null;
133+
}
134+
135+
var targetPath = filePath[projectPath.Length..];
113136
var hostDocument = new HostDocument(filePath, targetPath);
114137
return hostDocument;
115138
}
@@ -129,6 +152,11 @@ private void RemoveFallbackDocument(ProjectId projectId, string filePath, string
129152
}
130153

131154
var hostDocument = CreateHostDocument(filePath, projectFilePath);
155+
if (hostDocument is null)
156+
{
157+
return;
158+
}
159+
132160
_projectSnapshotManagerAccessor.Instance.DocumentRemoved(razorProjectKey, hostDocument);
133161
}
134162

src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/FallbackProjectManagerTest.cs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using System.Linq;
66
using Microsoft.AspNetCore.Razor.Language;
77
using Microsoft.AspNetCore.Razor.LanguageServer;
8+
using Microsoft.AspNetCore.Razor.Telemetry;
9+
using Microsoft.AspNetCore.Razor.Utilities;
810
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
911
using Moq;
1012
using Xunit;
@@ -30,7 +32,7 @@ public FallbackProjectManagerTest(ITestOutputHelper testOutputHelper)
3032

3133
var projectSnapshotManagerAccessor = Mock.Of<ProjectSnapshotManagerAccessor>(a => a.Instance == _projectSnapshotManager, MockBehavior.Strict);
3234

33-
_fallbackProjectManger = new FallbackProjectManager(_projectConfigurationFilePathStore, languageServerFeatureOptions, projectSnapshotManagerAccessor);
35+
_fallbackProjectManger = new FallbackProjectManager(_projectConfigurationFilePathStore, languageServerFeatureOptions, projectSnapshotManagerAccessor, NoOpTelemetryReporter.Instance);
3436
}
3537

3638
[Fact]
@@ -71,6 +73,7 @@ public void DynamicFileAdded_UnknownProject_Adds()
7173

7274
var documentFilePath = Assert.Single(project.DocumentFilePaths);
7375
Assert.Equal(SomeProjectFile1.FilePath, documentFilePath);
76+
Assert.Equal(SomeProjectFile1.TargetPath, project.GetDocument(documentFilePath)!.TargetPath);
7477
}
7578

7679
[Fact]
@@ -109,14 +112,44 @@ public void DynamicFileAdded_TrackedProject_AddsDocuments()
109112

110113
_fallbackProjectManger.DynamicFileAdded(projectId, SomeProject.Key, SomeProject.FilePath, SomeProjectFile2.FilePath);
111114

112-
_fallbackProjectManger.DynamicFileAdded(projectId, SomeProject.Key, SomeProject.FilePath, SomeProjectComponentFile1.FilePath);
115+
_fallbackProjectManger.DynamicFileAdded(projectId, SomeProject.Key, SomeProject.FilePath, SomeProjectNestedComponentFile3.FilePath);
113116

114117
var project = Assert.Single(_projectSnapshotManager.GetProjects());
115118

116119
Assert.Collection(project.DocumentFilePaths.OrderBy(f => f), // DocumentFilePaths comes from a dictionary, so no sort guarantee
117120
f => Assert.Equal(SomeProjectFile1.FilePath, f),
118-
f => Assert.Equal(SomeProjectComponentFile1.FilePath, f),
119-
f => Assert.Equal(SomeProjectFile2.FilePath, f));
121+
f => Assert.Equal(SomeProjectFile2.FilePath, f),
122+
f => Assert.Equal(SomeProjectNestedComponentFile3.FilePath, f));
123+
124+
Assert.Equal(SomeProjectFile1.TargetPath, project.GetDocument(SomeProjectFile1.FilePath)!.TargetPath);
125+
Assert.Equal(SomeProjectFile2.TargetPath, project.GetDocument(SomeProjectFile2.FilePath)!.TargetPath);
126+
// The test data is created with a "\" so when the test runs on linux, and direct string comparison wouldn't work
127+
Assert.True(FilePathNormalizer.FilePathsEquivalent(SomeProjectNestedComponentFile3.TargetPath, project.GetDocument(SomeProjectNestedComponentFile3.FilePath)!.TargetPath));
128+
}
129+
130+
[Fact]
131+
public void DynamicFileAdded_TrackedProject_IgnoresDocumentFromOutsideCone()
132+
{
133+
var projectId = ProjectId.CreateNewId();
134+
var projectInfo = ProjectInfo.Create(projectId, VersionStamp.Default, "DisplayName", "AssemblyName", LanguageNames.CSharp, filePath: SomeProject.FilePath)
135+
.WithCompilationOutputInfo(new CompilationOutputInfo().WithAssemblyPath(Path.Combine(SomeProject.IntermediateOutputPath, "SomeProject.dll")))
136+
.WithDefaultNamespace("RootNamespace");
137+
138+
Workspace.TryApplyChanges(Workspace.CurrentSolution.AddProject(projectInfo));
139+
140+
_fallbackProjectManger.DynamicFileAdded(projectId, SomeProject.Key, SomeProject.FilePath, SomeProjectFile1.FilePath);
141+
142+
// These two represent linked files, or shared project items
143+
_fallbackProjectManger.DynamicFileAdded(projectId, SomeProject.Key, SomeProject.FilePath, AnotherProjectFile2.FilePath);
144+
145+
_fallbackProjectManger.DynamicFileAdded(projectId, SomeProject.Key, SomeProject.FilePath, AnotherProjectComponentFile1.FilePath);
146+
147+
var project = Assert.Single(_projectSnapshotManager.GetProjects());
148+
149+
Assert.Collection(project.DocumentFilePaths,
150+
f => Assert.Equal(SomeProjectFile1.FilePath, f));
151+
152+
Assert.Equal(SomeProjectFile1.TargetPath, project.GetDocument(SomeProjectFile1.FilePath)!.TargetPath);
120153
}
121154

122155
[Fact]

src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Shared/TestProjectData.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ static TestProjectData()
2828
SomeProjectFile1 = new HostDocument(Path.Combine(someProjectPath, "File1.cshtml"), "File1.cshtml", FileKinds.Legacy);
2929
SomeProjectFile2 = new HostDocument(Path.Combine(someProjectPath, "File2.cshtml"), "File2.cshtml", FileKinds.Legacy);
3030
SomeProjectImportFile = new HostDocument(Path.Combine(someProjectPath, "_Imports.cshtml"), "_Imports.cshtml", FileKinds.Legacy);
31-
SomeProjectNestedFile3 = new HostDocument(Path.Combine(someProjectPath, "Nested", "File3.cshtml"), "Nested\\File1.cshtml", FileKinds.Legacy);
32-
SomeProjectNestedFile4 = new HostDocument(Path.Combine(someProjectPath, "Nested", "File4.cshtml"), "Nested\\File2.cshtml", FileKinds.Legacy);
31+
SomeProjectNestedFile3 = new HostDocument(Path.Combine(someProjectPath, "Nested", "File3.cshtml"), "Nested\\File3.cshtml", FileKinds.Legacy);
32+
SomeProjectNestedFile4 = new HostDocument(Path.Combine(someProjectPath, "Nested", "File4.cshtml"), "Nested\\File4.cshtml", FileKinds.Legacy);
3333
SomeProjectNestedImportFile = new HostDocument(Path.Combine(someProjectPath, "Nested", "_Imports.cshtml"), "Nested\\_Imports.cshtml", FileKinds.Legacy);
3434
SomeProjectComponentFile1 = new HostDocument(Path.Combine(someProjectPath, "File1.razor"), "File1.razor", FileKinds.Component);
3535
SomeProjectComponentFile2 = new HostDocument(Path.Combine(someProjectPath, "File2.razor"), "File2.razor", FileKinds.Component);
3636
SomeProjectComponentImportFile1 = new HostDocument(Path.Combine(someProjectPath, "_Imports.razor"), "_Imports.razor", FileKinds.Component);
37-
SomeProjectNestedComponentFile3 = new HostDocument(Path.Combine(someProjectPath, "Nested", "File3.razor"), "Nested\\File1.razor", FileKinds.Component);
38-
SomeProjectNestedComponentFile4 = new HostDocument(Path.Combine(someProjectPath, "Nested", "File4.razor"), "Nested\\File2.razor", FileKinds.Component);
37+
SomeProjectNestedComponentFile3 = new HostDocument(Path.Combine(someProjectPath, "Nested", "File3.razor"), "Nested\\File3.razor", FileKinds.Component);
38+
SomeProjectNestedComponentFile4 = new HostDocument(Path.Combine(someProjectPath, "Nested", "File4.razor"), "Nested\\File4.razor", FileKinds.Component);
3939
SomeProjectCshtmlComponentFile5 = new HostDocument(Path.Combine(someProjectPath, "File5.cshtml"), "File5.cshtml", FileKinds.Component);
4040

4141
var anotherProjectPath = Path.Combine(baseDirectory, "AnotherProject");

src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/DefaultRazorDynamicFileInfoProviderTest.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Threading.Tasks;
99
using Microsoft.AspNetCore.Razor.Language;
1010
using Microsoft.AspNetCore.Razor.LanguageServer;
11+
using Microsoft.AspNetCore.Razor.Telemetry;
1112
using Microsoft.CodeAnalysis;
1213
using Microsoft.CodeAnalysis.Razor;
1314
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
@@ -56,7 +57,7 @@ public DefaultRazorDynamicFileInfoProviderTest(ITestOutputHelper testOutput)
5657
var filePathService = new FilePathService(languageServerFeatureOptions);
5758
var projectSnapshotManagerAccessor = Mock.Of<ProjectSnapshotManagerAccessor>(a => a.Instance == _projectSnapshotManager, MockBehavior.Strict);
5859
var projectConfigurationFilePathStore = Mock.Of<ProjectConfigurationFilePathStore>(MockBehavior.Strict);
59-
var fallbackProjectManager = new FallbackProjectManager(projectConfigurationFilePathStore, languageServerFeatureOptions, projectSnapshotManagerAccessor);
60+
var fallbackProjectManager = new FallbackProjectManager(projectConfigurationFilePathStore, languageServerFeatureOptions, projectSnapshotManagerAccessor, NoOpTelemetryReporter.Instance);
6061

6162
_provider = new DefaultRazorDynamicFileInfoProvider(_documentServiceFactory, _editorFeatureDetector, filePathService, projectSnapshotManagerAccessor, fallbackProjectManager);
6263
_testAccessor = _provider.GetTestAccessor();

0 commit comments

Comments
 (0)