Skip to content

Commit

Permalink
Simplify API for solution and project load into AdhocWorkspace (dotne…
Browse files Browse the repository at this point in the history
  • Loading branch information
tintoy committed Nov 26, 2017
1 parent 6864ff2 commit 4f800e2
Show file tree
Hide file tree
Showing 2 changed files with 228 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public sealed class ExtractMetadataWorker : IDisposable
private readonly Dictionary<string, string> _msbuildProperties;

//Lacks UT for shared workspace
private AdhocWorkspace _workspace;
private Lazy<AdhocWorkspace> _workspace = new Lazy<AdhocWorkspace>(() => new AdhocWorkspace());

internal const string IndexFileName = ".manifest";

Expand Down Expand Up @@ -886,9 +886,9 @@ private Solution GetSolution(string path)
{
Logger.LogVerbose($"Loading solution {path}", file: path);

_workspace = MSBuildWorkspace.FromSolutionFile(path, _msbuildProperties);
_workspace.Value.OpenSolution(path, _msbuildProperties);

return _workspace.CurrentSolution;
return _workspace.Value.CurrentSolution;
}
catch (Exception e)
{
Expand All @@ -905,9 +905,7 @@ private Project GetProject(ConcurrentDictionary<string, Project> cache, string p
{
Logger.LogVerbose($"Loading project {s}", file: s);

return _workspace.CurrentSolution.Projects.FirstOrDefault(
project => project.FilePath == path
);
return _workspace.Value.OpenProject(path);
}
catch (AggregateException e)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,46 +21,56 @@ namespace Microsoft.DocAsCode.Metadata.ManagedReference.MSBuildWorkspaces
public static class MSBuildWorkspace
{
/// <summary>
/// Generate a Roslyn <see cref="AdHocWorkspace"/> from a solution (.sln) file.
/// Open a solution (.sln) file, replacing the workspace's existing solution.
/// </summary>
/// <param name="workspace">
/// The target <see cref="AdhocWorkspace"/>.
/// </param>
/// <param name="solutionPath">
/// The full path to the solution file.
/// </param>
/// <param name="msbuildProperties">
/// An optional <see cref="IDictionary{TKey, TValue}"/> containing global MSBuild properties used to configure the underlying project collection.
/// </param>
/// <returns>
/// The configured <see cref="AdhocWorkspace"/>.
/// The loaded <see cref="Solution"/>.
/// </returns>
public static AdhocWorkspace FromSolutionFile(string solutionPath, IDictionary<string, string> msbuildProperties = null)
public static Solution OpenSolution(this AdhocWorkspace workspace, string solutionPath, IDictionary<string, string> msbuildProperties = null)
{
if (workspace == null)
throw new ArgumentNullException(nameof(workspace));

if (string.IsNullOrWhiteSpace(solutionPath))
throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(solutionPath)}.", nameof(solutionPath));

return FromSolutionFile(
return workspace.OpenSolution(
new FileInfo(solutionPath)
);
}

/// <summary>
/// Generate a Roslyn <see cref="AdHocWorkspace"/> from a solution (.sln) file.
/// Open a solution (.sln) file, replacing the workspace's existing solution.
/// </summary>
/// <param name="workspace">
/// The target <see cref="AdhocWorkspace"/>.
/// </param>
/// <param name="solutionFile">
/// A <see cref="FileInfo"/> representing the solution file.
/// </param>
/// <param name="msbuildProperties">
/// An optional <see cref="IDictionary{TKey, TValue}"/> containing global MSBuild properties used to configure the underlying project collection.
/// </param>
/// <returns>
/// The configured <see cref="AdhocWorkspace"/>.
/// The loaded <see cref="Solution"/>.
/// </returns>
public static AdhocWorkspace FromSolutionFile(FileInfo solutionFile, IDictionary<string, string> msbuildProperties = null)
public static Solution OpenSolution(this AdhocWorkspace workspace, FileInfo solutionFile, IDictionary<string, string> msbuildProperties = null)
{
if (workspace == null)
throw new ArgumentNullException(nameof(workspace));

if (solutionFile == null)
throw new ArgumentNullException(nameof(solutionFile));

AdhocWorkspace workspace = new AdhocWorkspace();

Solution solution = workspace.AddSolution(SolutionInfo.Create(
SolutionId.CreateNewId(),
VersionStamp.Create(),
Expand All @@ -72,81 +82,227 @@ public static AdhocWorkspace FromSolutionFile(FileInfo solutionFile, IDictionary
MSB.ProjectCollection projectCollection = MSBuildHelper.CreateProjectCollection(
solutionDirectory: solutionFile.Directory.FullName
);
if (msbuildProperties != null)
using (projectCollection)
{
foreach (string propertyName in msbuildProperties.Keys)
if (msbuildProperties != null)
{
foreach (string propertyName in msbuildProperties.Keys)
{
string propertyValue = msbuildProperties[propertyName];
projectCollection.SetGlobalProperty(propertyName, propertyValue);
}
}

foreach (var solutionProject in msbuildSolution.ProjectsInOrder)
{
string propertyValue = msbuildProperties[propertyName];
projectCollection.SetGlobalProperty(propertyName, propertyValue);
FileInfo projectFile = new FileInfo(solutionProject.AbsolutePath);
solution = solution.LoadMSBuildProject(projectFile, projectCollection);
}

workspace.TryApplyChanges(solution);
}

foreach (var solutionProject in msbuildSolution.ProjectsInOrder)
{
var msbuildProject = projectCollection.LoadProject(solutionProject.AbsolutePath);
return workspace.CurrentSolution;
}

/// <summary>
/// Load a project into the workspace.
/// </summary>
/// <param name="workspace">
/// The target <see cref="AdhocWorkspace"/>.
/// </param>
/// <param name="projectFilePath">
/// The full path to the project file.
/// </param>
/// <param name="msbuildProperties">
/// An optional <see cref="IDictionary{TKey, TValue}"/> containing global MSBuild properties used to configure the underlying project collection.
/// </param>
/// <returns>
/// The loaded <see cref="Project"/>.
/// </returns>
public static Project OpenProject(this AdhocWorkspace workspace, string projectFilePath, IDictionary<string, string> msbuildProperties = null)
{
if (workspace == null)
throw new ArgumentNullException(nameof(workspace));

string language;
if (solutionProject.AbsolutePath.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase))
language = "C#";
else if (solutionProject.AbsolutePath.EndsWith(".vbproj", StringComparison.OrdinalIgnoreCase))
language = "VB";
else
continue;
if (string.IsNullOrWhiteSpace(projectFilePath))
throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(projectFilePath)}.", nameof(projectFilePath));

var projectId = ProjectId.CreateNewId();
return workspace.OpenProject(
new FileInfo(projectFilePath),
msbuildProperties
);
}

List<DocumentInfo> projectDocuments = new List<DocumentInfo>();
foreach (MSB.ProjectItem item in msbuildProject.GetItems("Compile"))
{
string itemPath = item.GetMetadataValue("FullPath");

projectDocuments.Add(
DocumentInfo.Create(
DocumentId.CreateNewId(projectId),
name: Path.GetFileName(itemPath),
filePath: itemPath,
loader: TextLoader.From(
TextAndVersion.Create(
SourceText.From(File.ReadAllText(itemPath)),
VersionStamp.Create()
)
)
)
);
}
/// <summary>
/// Load a project into the workspace.
/// </summary>
/// <param name="workspace">
/// The target <see cref="AdhocWorkspace"/>.
/// </param>
/// <param name="projectFile">
/// The full path to the <see cref="AdhocWorkspace"/>.
/// </param>
/// <param name="msbuildProperties">
/// An optional <see cref="IDictionary{TKey, TValue}"/> containing global MSBuild properties used to configure the underlying project collection.
/// </param>
/// <returns>
/// The loaded <see cref="Project"/>.
/// </returns>
public static Project OpenProject(this AdhocWorkspace workspace, FileInfo projectFile, IDictionary<string, string> msbuildProperties = null)
{
if (workspace == null)
throw new ArgumentNullException(nameof(workspace));

List<MetadataReference> references = new List<MetadataReference>();
foreach (string assemblyPath in ResolveReferences(msbuildProject))
{
references.Add(
MetadataReference.CreateFromFile(assemblyPath, MetadataReferenceProperties.Assembly)
);
}
if (projectFile == null)
throw new ArgumentNullException(nameof(projectFile));

// If the project is already loaded, reuse the existing instance.
Project existingProject = workspace.CurrentSolution.Projects.FirstOrDefault(
project => String.Equals(projectFile.FullName, project.FilePath, StringComparison.OrdinalIgnoreCase)
);
if (existingProject != null)
return existingProject;

solution = solution.AddProject(ProjectInfo.Create(
projectId,
VersionStamp.Create(),
name: Path.GetFileNameWithoutExtension(msbuildProject.FullPath),
assemblyName: Path.GetFileNameWithoutExtension(msbuildProject.FullPath),
language: language,
filePath: msbuildProject.FullPath,
outputFilePath: msbuildProject.GetPropertyValue("TargetPath"),
documents: projectDocuments,
metadataReferences: references
));

var project = solution.GetProject(projectId);
solution = solution.WithProjectCompilationOptions(projectId, project.CompilationOptions.WithSpecificDiagnosticOptions(
new Dictionary<string, ReportDiagnostic>
MSB.ProjectCollection projectCollection = MSBuildHelper.CreateProjectCollection(
solutionDirectory: projectFile.Directory.FullName
);
using (projectCollection)
{
if (msbuildProperties != null)
{
foreach (string propertyName in msbuildProperties.Keys)
{
["CS1701"] = ReportDiagnostic.Suppress
string propertyValue = msbuildProperties[propertyName];
projectCollection.SetGlobalProperty(propertyName, propertyValue);
}
));
}

Project project;
Solution solution = workspace.CurrentSolution;
solution = solution.LoadMSBuildProject(projectFile, projectCollection, out project);

workspace.TryApplyChanges(solution);

return project;
}
}

/// <summary>
/// Load an MSBuild project and use its contents to populate the workspace's <see cref="Solution"/>.
/// </summary>
/// <param name="solution">
/// The <see cref="AdhocWorkspace"/>'s <see cref="Solution"/>.
/// </param>
/// <param name="projectFile">
/// A <see cref="FileInfo"/> representing the MSBuild project file.
/// </param>
/// <param name="projectCollection">
/// The <see cref="MSB.ProjectCollection"/> that the project will be loaded into.
/// </param>
/// <returns>
/// The updated <see cref="Solution"/>.
/// </returns>
static Solution LoadMSBuildProject(this Solution solution, FileInfo projectFile, MSB.ProjectCollection projectCollection) => solution.LoadMSBuildProject(projectFile, projectCollection, out _);

/// <summary>
/// Load an MSBuild project and use its contents to populate the workspace's <see cref="Solution"/>.
/// </summary>
/// <param name="solution">
/// The <see cref="AdhocWorkspace"/>'s <see cref="Solution"/>.
/// </param>
/// <param name="projectFile">
/// A <see cref="FileInfo"/> representing the MSBuild project file.
/// </param>
/// <param name="projectCollection">
/// The <see cref="MSB.ProjectCollection"/> that the project will be loaded into.
/// </param>
/// <param name="project">
/// Receives the loaded project (or <c>null</c> if the project is not of a supported type).
/// </param>
/// <returns>
/// The updated <see cref="Solution"/>.
/// </returns>
static Solution LoadMSBuildProject(this Solution solution, FileInfo projectFile, MSB.ProjectCollection projectCollection, out Project project)
{
if (solution == null)
throw new ArgumentNullException(nameof(solution));

if (projectFile == null)
throw new ArgumentNullException(nameof(projectFile));

if (projectCollection == null)
throw new ArgumentNullException(nameof(projectCollection));

var msbuildProject = projectCollection.LoadProject(projectFile.FullName);

string language;
if (String.Equals(projectFile.Extension, ".csproj", StringComparison.OrdinalIgnoreCase))
language = "C#";
if (String.Equals(projectFile.Extension, ".csproj", StringComparison.OrdinalIgnoreCase))
language = "VB";
else
{
// Unsupported project type.
project = null;

return solution;
}

var projectId = ProjectId.CreateNewId();

// Add source files in a single batch.
List<DocumentInfo> projectDocuments = new List<DocumentInfo>();
foreach (MSB.ProjectItem item in msbuildProject.GetItems("Compile"))
{
string itemPath = item.GetMetadataValue("FullPath");

projectDocuments.Add(
DocumentInfo.Create(
DocumentId.CreateNewId(projectId),
name: Path.GetFileName(itemPath),
filePath: itemPath,
loader: TextLoader.From(
TextAndVersion.Create(
SourceText.From(File.ReadAllText(itemPath)),
VersionStamp.Create()
)
)
)
);
}

List<MetadataReference> references = new List<MetadataReference>();
foreach (string assemblyPath in ResolveReferences(msbuildProject))
{
references.Add(
MetadataReference.CreateFromFile(assemblyPath, MetadataReferenceProperties.Assembly)
);
}

return workspace;
solution = solution.AddProject(ProjectInfo.Create(
projectId,
VersionStamp.Create(),
name: Path.GetFileNameWithoutExtension(msbuildProject.FullPath),
assemblyName: Path.GetFileNameWithoutExtension(msbuildProject.FullPath),
language: language,
filePath: msbuildProject.FullPath,
outputFilePath: msbuildProject.GetPropertyValue("TargetPath"),
documents: projectDocuments,
metadataReferences: references
));

project = solution.GetProject(projectId);
solution = solution.WithProjectCompilationOptions(projectId, project.CompilationOptions.WithSpecificDiagnosticOptions(
new Dictionary<string, ReportDiagnostic>
{
["CS1701"] = ReportDiagnostic.Suppress
}
));

project = solution.GetProject(projectId); // Yes, really.

return solution;
}

/// <summary>
Expand Down

0 comments on commit 4f800e2

Please sign in to comment.