Skip to content

Commit 8ee1499

Browse files
committed
Check msbuild globs before adding file to workspace
1 parent 29d1c10 commit 8ee1499

File tree

3 files changed

+67
-15
lines changed

3 files changed

+67
-15
lines changed

src/SharpIDE.Application/Features/Analysis/ProjectLoader/CustomMsBuildProjectLoader.Worker.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ private async Task<TResult> DoOperationAndReportProgressAsync<TResult>(ProjectLo
111111
return result;
112112
}
113113

114-
public async Task<ImmutableArray<ProjectInfo>> LoadAsync(CancellationToken cancellationToken)
114+
public async Task<(ImmutableArray<ProjectInfo>, Dictionary<ProjectId, ProjectFileInfo>)> LoadAsync(CancellationToken cancellationToken)
115115
{
116116
var results = ImmutableArray.CreateBuilder<ProjectInfo>();
117117
var processedPaths = new HashSet<string>(PathUtilities.Comparer);
@@ -150,7 +150,7 @@ public async Task<ImmutableArray<ProjectInfo>> LoadAsync(CancellationToken cance
150150
}
151151
}
152152

153-
return results.ToImmutableAndClear();
153+
return (results.ToImmutableAndClear(), _projectIdToFileInfoMap);
154154
}
155155

156156
private async Task<ImmutableArray<ProjectFileInfo>> LoadProjectFileInfosAsync(string projectPath, DiagnosticReportingOptions reportingOptions, CancellationToken cancellationToken)

src/SharpIDE.Application/Features/Analysis/ProjectLoader/CustomMsBuildProjectLoader.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace SharpIDE.Application.Features.Analysis.ProjectLoader;
1313
// https://github.com/dotnet/roslyn/blob/main/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.cs
1414
public partial class CustomMsBuildProjectLoader(Workspace workspace, ImmutableDictionary<string, string>? properties = null) : MSBuildProjectLoader(workspace, properties)
1515
{
16-
public async Task<ImmutableArray<ProjectInfo>> LoadProjectInfosAsync(
16+
public async Task<(ImmutableArray<ProjectInfo>, Dictionary<ProjectId, ProjectFileInfo>)> LoadProjectInfosAsync(
1717
List<string> projectFilePaths,
1818
ProjectMap? projectMap = null,
1919
IProgress<ProjectLoadProgress>? progress = null,
@@ -64,7 +64,7 @@ public async Task<ImmutableArray<ProjectInfo>> LoadProjectInfosAsync(
6464
/// <param name="progress">An optional <see cref="IProgress{T}"/> that will receive updates as the solution is loaded.</param>
6565
/// <param name="msbuildLogger">An optional <see cref="ILogger"/> that will log MSBuild results.</param>
6666
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> to allow cancellation of this operation.</param>
67-
public new async Task<SolutionInfo> LoadSolutionInfoAsync(
67+
public new async Task<(SolutionInfo, Dictionary<ProjectId, ProjectFileInfo>)> LoadSolutionInfoAsync(
6868
string solutionFilePath,
6969
IProgress<ProjectLoadProgress>? progress = null,
7070
ILogger? msbuildLogger = null,
@@ -109,13 +109,15 @@ public async Task<ImmutableArray<ProjectInfo>> LoadProjectInfosAsync(
109109
discoveredProjectOptions: reportingOptions,
110110
preferMetadataForReferencesOfDiscoveredProjects: false);
111111

112-
var projectInfos = await worker.LoadAsync(cancellationToken).ConfigureAwait(false);
112+
var (projectInfos, projectFileInfos) = await worker.LoadAsync(cancellationToken).ConfigureAwait(false);
113113

114114
// construct workspace from loaded project infos
115-
return SolutionInfo.Create(
115+
var solutionInfo = SolutionInfo.Create(
116116
SolutionId.CreateNewId(debugName: absoluteSolutionPath),
117117
version: default,
118118
absoluteSolutionPath,
119119
projectInfos);
120+
121+
return (solutionInfo, projectFileInfos);
120122
}
121123
}

src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
using Microsoft.CodeAnalysis.Rename;
2323
using Microsoft.CodeAnalysis.Shared.Extensions;
2424
using Microsoft.CodeAnalysis.Text;
25+
using Microsoft.Extensions.FileSystemGlobbing;
2526
using Microsoft.Extensions.Logging;
2627
using NuGet.Frameworks;
2728
using Roslyn.LanguageServer.Protocol;
@@ -53,6 +54,9 @@ public class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService buildSe
5354
private static HashSet<CodeFixProvider> _codeFixProviders = [];
5455
private static HashSet<CodeRefactoringProvider> _codeRefactoringProviders = [];
5556

57+
// Primarily used for getting the globs for a project
58+
private Dictionary<ProjectId, ProjectFileInfo> _projectFileInfoMap = new();
59+
5660
private TaskCompletionSource _solutionLoadedTcs = null!;
5761
private SharpIdeSolutionModel? _sharpIdeSolutionModel;
5862
public void StartSolutionAnalysis(SharpIdeSolutionModel solutionModel)
@@ -110,7 +114,8 @@ public async Task Analyse(SharpIdeSolutionModel solutionModel, CancellationToken
110114

111115
// MsBuildProjectLoader doesn't do a restore which is absolutely required for resolving PackageReferences, if they have changed. I am guessing it just reads from project.assets.json
112116
await _buildService.MsBuildAsync(_sharpIdeSolutionModel.FilePath, BuildType.Restore, cancellationToken);
113-
var solutionInfo = await _msBuildProjectLoader!.LoadSolutionInfoAsync(_sharpIdeSolutionModel.FilePath, cancellationToken: cancellationToken);
117+
var (solutionInfo, projectFileInfos) = await _msBuildProjectLoader!.LoadSolutionInfoAsync(_sharpIdeSolutionModel.FilePath, cancellationToken: cancellationToken);
118+
_projectFileInfoMap = projectFileInfos;
114119
_workspace.ClearSolution();
115120
var solution = _workspace.AddSolution(solutionInfo);
116121
}
@@ -185,7 +190,8 @@ public async Task ReloadSolution(CancellationToken cancellationToken = default)
185190
var __ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.MSBuildProjectLoader.LoadSolutionInfoAsync");
186191
// This call is the expensive part - MSBuild is slow. There doesn't seem to be any incrementalism for solutions.
187192
// The best we could do to speed it up is do .LoadProjectInfoAsync for the single project, and somehow munge that into the existing solution
188-
var newSolutionInfo = await _msBuildProjectLoader.LoadSolutionInfoAsync(_sharpIdeSolutionModel!.FilePath, cancellationToken: cancellationToken);
193+
var (newSolutionInfo, projectFileInfos) = await _msBuildProjectLoader.LoadSolutionInfoAsync(_sharpIdeSolutionModel!.FilePath, cancellationToken: cancellationToken);
194+
_projectFileInfoMap = projectFileInfos;
189195
__?.Dispose();
190196

191197
var ___ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.Workspace.OnSolutionReloaded");
@@ -219,7 +225,11 @@ public async Task ReloadProject(SharpIdeProjectModel projectModel, CancellationT
219225
// This will get all projects necessary to build this group of projects, regardless of whether those projects are actually affected by the original project change
220226
// We can potentially optimise this, but given this is the expensive part, lets just proceed with reloading them all in the solution
221227
// We potentially lose performance because Workspace/Solution caches are dropped, but lets not prematurely optimise
222-
var loadedProjectInfos = await _msBuildProjectLoader.LoadProjectInfosAsync(projectPathsToReload, null, cancellationToken: cancellationToken);
228+
var (loadedProjectInfos, projectFileInfos) = await _msBuildProjectLoader.LoadProjectInfosAsync(projectPathsToReload, null, cancellationToken: cancellationToken);
229+
foreach (var (projectId, projectFileInfo) in projectFileInfos)
230+
{
231+
_projectFileInfoMap[projectId] = projectFileInfo;
232+
}
223233
__?.Dispose();
224234

225235
var ___ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.Workspace.UpdateSolution");
@@ -960,12 +970,46 @@ public async Task AddDocument(SharpIdeFile fileModel, string content)
960970
Guard.Against.Null(fileModel, nameof(fileModel));
961971
Guard.Against.Null(content, nameof(content));
962972

963-
var project = GetProjectForSharpIdeFile(fileModel);
973+
var sharpIdeProject = GetSharpIdeProjectForSharpIdeFile(fileModel);
974+
var probableProject = GetProjectForSharpIdeProjectModel(sharpIdeProject);
975+
// This file probably belongs to this project, but we need to check its path against the globs for the project to make sure
976+
var projectFileInfo = _projectFileInfoMap.GetValueOrDefault(probableProject.Id);
977+
Guard.Against.Null(projectFileInfo);
978+
var matchers = projectFileInfo.FileGlobs.Select(glob =>
979+
{
980+
var matcher = new Matcher();
981+
matcher.AddIncludePatterns(glob.Includes);
982+
matcher.AddExcludePatterns(glob.Excludes);
983+
matcher.AddExcludePatterns(glob.Removes);
984+
return matcher;
985+
});
986+
987+
var belongsToProject = false;
988+
// Check if the file path matches any of the globs in the project file.
989+
foreach (var matcher in matchers)
990+
{
991+
// CPS re-creates the msbuild globs from the includes/excludes/removes and the project XML directory and
992+
// ignores the MSBuildGlob.FixedDirectoryPart. We'll do the same here and match using the project directory as the relative path.
993+
// See https://devdiv.visualstudio.com/DevDiv/_git/CPS?path=/src/Microsoft.VisualStudio.ProjectSystem/Build/MsBuildGlobFactory.cs
994+
var relativeDirectory = sharpIdeProject.DirectoryPath;
995+
996+
var matches = matcher.Match(relativeDirectory, fileModel.Path);
997+
if (matches.HasMatches)
998+
{
999+
belongsToProject = true;
1000+
break;
1001+
}
1002+
}
1003+
1004+
if (belongsToProject is false)
1005+
{
1006+
return;
1007+
}
9641008

9651009
var existingDocument = fileModel switch
9661010
{
967-
{ IsRazorFile: true } => project.AdditionalDocuments.SingleOrDefault(s => s.FilePath == fileModel.Path),
968-
{ IsCsharpFile: true } => project.Documents.SingleOrDefault(s => s.FilePath == fileModel.Path),
1011+
{ IsRazorFile: true } => probableProject.AdditionalDocuments.SingleOrDefault(s => s.FilePath == fileModel.Path),
1012+
{ IsCsharpFile: true } => probableProject.Documents.SingleOrDefault(s => s.FilePath == fileModel.Path),
9691013
_ => throw new InvalidOperationException("AddDocument failed: File is not a workspace file")
9701014
};
9711015
if (existingDocument is not null)
@@ -977,8 +1021,8 @@ public async Task AddDocument(SharpIdeFile fileModel, string content)
9771021

9781022
var newSolution = fileModel switch
9791023
{
980-
{ IsRazorFile: true } => _workspace.CurrentSolution.AddAdditionalDocument(DocumentId.CreateNewId(project.Id), fileModel.Name, sourceText, filePath: fileModel.Path),
981-
{ IsCsharpFile: true } => _workspace.CurrentSolution.AddDocument(DocumentId.CreateNewId(project.Id), fileModel.Name, sourceText, filePath: fileModel.Path),
1024+
{ IsRazorFile: true } => _workspace.CurrentSolution.AddAdditionalDocument(DocumentId.CreateNewId(probableProject.Id), fileModel.Name, sourceText, filePath: fileModel.Path),
1025+
{ IsCsharpFile: true } => _workspace.CurrentSolution.AddDocument(DocumentId.CreateNewId(probableProject.Id), fileModel.Name, sourceText, filePath: fileModel.Path),
9821026
_ => throw new InvalidOperationException("AddDocument failed: File is not in workspace")
9831027
};
9841028

@@ -1037,9 +1081,15 @@ public async Task<string> GetOutputDllPathForProject(SharpIdeProjectModel projec
10371081
return outputPath;
10381082
}
10391083

1040-
private static Project GetProjectForSharpIdeFile(SharpIdeFile sharpIdeFile)
1084+
private static SharpIdeProjectModel GetSharpIdeProjectForSharpIdeFile(SharpIdeFile sharpIdeFile)
10411085
{
10421086
var sharpIdeProjectModel = ((IChildSharpIdeNode)sharpIdeFile).GetNearestProjectNode()!;
1087+
return sharpIdeProjectModel;
1088+
}
1089+
1090+
private static Project GetProjectForSharpIdeFile(SharpIdeFile sharpIdeFile)
1091+
{
1092+
var sharpIdeProjectModel = GetSharpIdeProjectForSharpIdeFile(sharpIdeFile);
10431093
var project = GetProjectForSharpIdeProjectModel(sharpIdeProjectModel);
10441094
return project;
10451095
}

0 commit comments

Comments
 (0)