diff --git a/src/Buildalyzer.Workspaces/AnalyzerManagerExtensions.cs b/src/Buildalyzer.Workspaces/AnalyzerManagerExtensions.cs index a668684..f8cc625 100644 --- a/src/Buildalyzer.Workspaces/AnalyzerManagerExtensions.cs +++ b/src/Buildalyzer.Workspaces/AnalyzerManagerExtensions.cs @@ -4,7 +4,6 @@ using Microsoft.Build.Construction; using Microsoft.CodeAnalysis; using Microsoft.Extensions.Logging; - namespace Buildalyzer.Workspaces; public static class AnalyzerManagerExtensions @@ -39,7 +38,7 @@ public static AdhocWorkspace GetWorkspace(this IAnalyzerManager manager) AdhocWorkspace workspace = manager.CreateWorkspace(); if (!string.IsNullOrEmpty(manager.SolutionFilePath)) { - SolutionInfo solutionInfo = SolutionInfo.Create(SolutionId.CreateNewId(), VersionStamp.Default, manager.SolutionFilePath); + Microsoft.CodeAnalysis.SolutionInfo solutionInfo = Microsoft.CodeAnalysis.SolutionInfo.Create(SolutionId.CreateNewId(), VersionStamp.Default, manager.SolutionFilePath); workspace.AddSolution(solutionInfo); // Sort the projects so the order that they're added to the workspace in the same order as the solution file diff --git a/src/Buildalyzer.Workspaces/AnalyzerResultExtensions.cs b/src/Buildalyzer.Workspaces/AnalyzerResultExtensions.cs index 34f8f05..993d3a2 100644 --- a/src/Buildalyzer.Workspaces/AnalyzerResultExtensions.cs +++ b/src/Buildalyzer.Workspaces/AnalyzerResultExtensions.cs @@ -62,7 +62,7 @@ public static Project AddToWorkspace(this IAnalyzerResult analyzerResult, Worksp analyzerResult.Manager.WorkspaceProjectReferences[projectId.Id] = analyzerResult.ProjectReferences.ToArray(); // Create and add the project, but only if it's a support Roslyn project type - ProjectInfo projectInfo = GetProjectInfo(analyzerResult, workspace, projectId); + Microsoft.CodeAnalysis.ProjectInfo projectInfo = GetProjectInfo(analyzerResult, workspace, projectId); if (projectInfo is null) { // Something went wrong (maybe not a support project type), so don't add this project @@ -137,14 +137,14 @@ public static Project AddToWorkspace(this IAnalyzerResult analyzerResult, Worksp return workspace.CurrentSolution.GetProject(projectId); } - private static ProjectInfo GetProjectInfo(IAnalyzerResult analyzerResult, Workspace workspace, ProjectId projectId) + private static Microsoft.CodeAnalysis.ProjectInfo GetProjectInfo(IAnalyzerResult analyzerResult, Workspace workspace, ProjectId projectId) { string projectName = Path.GetFileNameWithoutExtension(analyzerResult.ProjectFilePath); if (!TryGetSupportedLanguageName(analyzerResult.ProjectFilePath, out string languageName)) { return null; } - return ProjectInfo.Create( + return Microsoft.CodeAnalysis.ProjectInfo.Create( projectId, VersionStamp.Create(), projectName, diff --git a/src/Buildalyzer/AnalyzerManager.cs b/src/Buildalyzer/AnalyzerManager.cs index 7d4eb56..c95008d 100644 --- a/src/Buildalyzer/AnalyzerManager.cs +++ b/src/Buildalyzer/AnalyzerManager.cs @@ -1,6 +1,7 @@ extern alias StructuredLogger; using System.Collections.Concurrent; using System.IO; +using Buildalyzer.IO; using Buildalyzer.Logging; using Microsoft.Build.Construction; using Microsoft.Extensions.Logging; @@ -35,36 +36,44 @@ public class AnalyzerManager : IAnalyzerManager internal ConcurrentDictionary WorkspaceProjectReferences = new ConcurrentDictionary(); #pragma warning restore SA1401 // Fields should be private - public string SolutionFilePath { get; } + [Obsolete("Use SolutionInfo.Path instead.")] + public string? SolutionFilePath => SolutionInfo?.Path.ToString(); - public SolutionFile SolutionFile { get; } + [Obsolete("Use SolutionInfo instead.")] + public SolutionFile? SolutionFile => SolutionInfo?.File; - public AnalyzerManager(AnalyzerManagerOptions options = null) + public SolutionInfo? SolutionInfo { get; } + + public AnalyzerManager(AnalyzerManagerOptions? options = null) : this(null, options) { } - public AnalyzerManager(string solutionFilePath, AnalyzerManagerOptions options = null) + public AnalyzerManager(string? solutionFilePath, AnalyzerManagerOptions? options = null) { options ??= new AnalyzerManagerOptions(); LoggerFactory = options.LoggerFactory; - if (!string.IsNullOrEmpty(solutionFilePath)) + var path = IOPath.Parse(solutionFilePath); + + if (path.HasValue && path.File().Exists) { - SolutionFilePath = NormalizePath(solutionFilePath); - SolutionFile = SolutionFile.Parse(SolutionFilePath); + SolutionInfo = SolutionInfo.Load(path, Filter); + + var lookup = SolutionInfo.File.ProjectsInOrder.ToDictionary(p => Guid.Parse(p.ProjectGuid), p => p); - // Initialize all the projects in the solution - foreach (ProjectInSolution projectInSolution in SolutionFile.ProjectsInOrder) + // init projects. + foreach (var proj in SolutionInfo) { - if (!SupportedProjectTypes.Contains(projectInSolution.ProjectType) - || (options?.ProjectFilter != null && !options.ProjectFilter(projectInSolution))) - { - continue; - } - GetProject(projectInSolution.AbsolutePath, projectInSolution); + var file = lookup[proj.Guid]; + var analyzer = new ProjectAnalyzer(this, proj.Path.ToString(), file); + _projects.TryAdd(proj.Path.ToString(), analyzer); } } + + bool Filter(ProjectInSolution p) + => SupportedProjectTypes.Contains(p.ProjectType) + && (options?.ProjectFilter?.Invoke(p) ?? true); } public void SetGlobalProperty(string key, string value) @@ -83,7 +92,17 @@ public void SetEnvironmentVariable(string key, string value) EnvironmentVariables[key] = value; } - public IProjectAnalyzer GetProject(string projectFilePath) => GetProject(projectFilePath, null); + public IProjectAnalyzer GetProject(string projectFilePath) + { + Guard.NotNull(projectFilePath); + projectFilePath = NormalizePath(projectFilePath); + + if (!File.Exists(projectFilePath)) + { + throw new FileNotFoundException("Could not load hte project file.", projectFilePath); + } + return _projects.GetOrAdd(projectFilePath, new ProjectAnalyzer(this, projectFilePath, null)); + } /// public IAnalyzerResults Analyze(string binLogPath, IEnumerable buildLoggers = null) @@ -104,22 +123,6 @@ public IAnalyzerResults Analyze(string binLogPath, IEnumerable path == null ? null : Path.GetFullPath(path.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar)); } \ No newline at end of file diff --git a/src/Buildalyzer/Buildalyzer.csproj b/src/Buildalyzer/Buildalyzer.csproj index 7326814..7b07075 100644 --- a/src/Buildalyzer/Buildalyzer.csproj +++ b/src/Buildalyzer/Buildalyzer.csproj @@ -11,6 +11,8 @@ diff --git a/src/Buildalyzer/IAnalyzerManager.cs b/src/Buildalyzer/IAnalyzerManager.cs index 065292f..b5f8207 100644 --- a/src/Buildalyzer/IAnalyzerManager.cs +++ b/src/Buildalyzer/IAnalyzerManager.cs @@ -9,9 +9,14 @@ public interface IAnalyzerManager IReadOnlyDictionary Projects { get; } - SolutionFile SolutionFile { get; } + /// + SolutionInfo? SolutionInfo { get; } - string SolutionFilePath { get; } + [Obsolete("Use SolutionInfo instead.")] + SolutionFile? SolutionFile { get; } + + [Obsolete("Use SolutionInfo.Path instead.")] + string? SolutionFilePath { get; } /// /// Analyzes an MSBuild binary log file. diff --git a/src/Buildalyzer/IO/IOPath.cs b/src/Buildalyzer/IO/IOPath.cs index 17d0572..81b5901 100644 --- a/src/Buildalyzer/IO/IOPath.cs +++ b/src/Buildalyzer/IO/IOPath.cs @@ -23,6 +23,9 @@ namespace Buildalyzer.IO; private IOPath(string path) => _path = path; + /// Returns true if the path is not empty. + public bool HasValue => _path is { Length: > 0 }; + /// Creates a based on the path. [Pure] public DirectoryInfo Directory() => new(ToString()); diff --git a/src/Buildalyzer/ProjectAnalyzer.cs b/src/Buildalyzer/ProjectAnalyzer.cs index a5429b0..0dc3423 100644 --- a/src/Buildalyzer/ProjectAnalyzer.cs +++ b/src/Buildalyzer/ProjectAnalyzer.cs @@ -31,7 +31,7 @@ public class ProjectAnalyzer : IProjectAnalyzer public string SolutionDirectory { get; } - public ProjectInSolution ProjectInSolution { get; } + public ProjectInSolution? ProjectInSolution { get; } /// public Guid ProjectGuid { get; } @@ -50,7 +50,7 @@ public class ProjectAnalyzer : IProjectAnalyzer public bool IgnoreFaultyImports { get; set; } = true; // The project file path should already be normalized - internal ProjectAnalyzer(AnalyzerManager manager, string projectFilePath, ProjectInSolution projectInSolution) + internal ProjectAnalyzer(AnalyzerManager manager, string projectFilePath, ProjectInSolution? projectInSolution) { Manager = manager; Logger = Manager.LoggerFactory?.CreateLogger(); diff --git a/src/Buildalyzer/ProjectInfo.cs b/src/Buildalyzer/ProjectInfo.cs new file mode 100644 index 0000000..810793a --- /dev/null +++ b/src/Buildalyzer/ProjectInfo.cs @@ -0,0 +1,40 @@ +using Buildalyzer.Construction; +using Buildalyzer.IO; +using Microsoft.Build.Construction; + +namespace Buildalyzer; + +/// Represents info about the MS Build solution file. +[DebuggerDisplay("{DebuggerDisplay}")] +public sealed class ProjectInfo +{ + private ProjectInfo(IOPath path, Guid guid) + { + Path = path; + File = new ProjectFile(path.ToString()); + Guid = guid; + } + + /// The GUID of the project. + public Guid Guid { get; } + + /// The path to the solution. + public IOPath Path { get; } + + public IProjectFile File { get; } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string DebuggerDisplay => $"{Path.File().Name}, TFM = {string.Join(", ", File.TargetFrameworks)}"; + + /// Loads the from disk. + /// + /// The path to load from. + /// + [Pure] + public static ProjectInfo Load(IOPath path) + => new(path, ProjectGuid.Create(path.File().Name)); + + [Pure] + internal static ProjectInfo New(ProjectInSolution proj) + => new(IOPath.Parse(proj.AbsolutePath), Guid.Parse(proj.ProjectGuid)); +} diff --git a/src/Buildalyzer/SolutionInfo.cs b/src/Buildalyzer/SolutionInfo.cs new file mode 100644 index 0000000..a73058c --- /dev/null +++ b/src/Buildalyzer/SolutionInfo.cs @@ -0,0 +1,64 @@ +#pragma warning disable CA1710 // Identifiers should have correct suffix: being a collection is not its main purpose. + +using Buildalyzer.IO; +using Microsoft.Build.Construction; + +namespace Buildalyzer; + +/// Represents info about the MS Build solution file. +[DebuggerTypeProxy(typeof(Diagnostics.CollectionDebugView))] +[DebuggerDisplay("{Path.File().Name}, Count = {Count}")] +public sealed class SolutionInfo : IReadOnlyCollection +{ + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly Dictionary _lookup; + + private SolutionInfo(IOPath path, SolutionFile file, Predicate? filter) + { + Path = path; + File = file; + Projects = file.ProjectsInOrder + .Where(p => (filter?.Invoke(p) ?? true) && System.IO.File.Exists(p.AbsolutePath)) + .Select(ProjectInfo.New) + .ToImmutableArray(); + + _lookup = Projects.ToDictionary(p => p.Guid, p => p); + } + + /// The path to the solution. + public IOPath Path { get; } + + /// The representation of the solution. + internal SolutionFile File { get; } + + /// + public IReadOnlyList Configurations => File.SolutionConfigurations; + + /// The projects in the solution. + public ImmutableArray Projects { get; } + + /// Tries to get a project based on its . + public ProjectInfo? this[Guid projectGuid] => _lookup[projectGuid]; + + /// + public int Count => Projects.Length; + + /// + [Pure] + public IEnumerator GetEnumerator() => ((IReadOnlyCollection)Projects).GetEnumerator(); + + /// + [Pure] + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// Loads the from disk. + /// + /// The path to load from. + /// + /// + /// The project to include. + /// + [Pure] + public static SolutionInfo Load(IOPath path, Predicate? filter = null) + => new(path, SolutionFile.Parse(path.ToString()), filter); +}