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

Support preserving formatting when opening a project #1036

Merged
merged 12 commits into from
Sep 16, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
48 changes: 38 additions & 10 deletions src/XMakeBuildEngine/Construction/ProjectRootElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ public class ProjectRootElement : ProjectElementContainer
/// </summary>
private static readonly ProjectRootElementCache.OpenProjectRootElement s_openLoaderDelegate = OpenLoader;

private static readonly ProjectRootElementCache.OpenProjectRootElement s_openLoaderPreserveWhitespaceDelegate = OpenLoaderPreserveWhitespace;

/// <summary>
/// The default encoding to use / assume for a new project.
/// </summary>
Expand Down Expand Up @@ -200,7 +202,8 @@ private ProjectRootElement(ProjectRootElementCache projectRootElementCache)
/// Assumes path is already normalized.
/// May throw InvalidProjectFileException.
/// </summary>
private ProjectRootElement(string path, ProjectRootElementCache projectRootElementCache, BuildEventContext buildEventContext)
private ProjectRootElement(string path, ProjectRootElementCache projectRootElementCache, BuildEventContext buildEventContext,
bool preserveWhitespace)
: base()
{
ErrorUtilities.VerifyThrowArgumentLength(path, "path");
Expand All @@ -214,7 +217,7 @@ private ProjectRootElement(string path, ProjectRootElementCache projectRootEleme
_versionOnDisk = _version;
_timeLastChangedUtc = DateTime.UtcNow;

XmlDocumentWithLocation document = LoadDocument(path);
XmlDocumentWithLocation document = LoadDocument(path, preserveWhitespace);

ProjectParser.Parse(document, this);

Expand Down Expand Up @@ -995,13 +998,19 @@ public static ProjectRootElement Open(string path)
/// May throw InvalidProjectFileException.
/// </summary>
public static ProjectRootElement Open(string path, ProjectCollection projectCollection)
{
return Open(path, projectCollection,
preserveWhitespace: false);
}

public static ProjectRootElement Open(string path, ProjectCollection projectCollection, bool preserveWhitespace)
{
ErrorUtilities.VerifyThrowArgumentLength(path, "path");
ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection");

path = FileUtilities.NormalizePath(path);

return Open(path, projectCollection.ProjectRootElementCache, true /*Is explicitly loaded*/);
return Open(path, projectCollection.ProjectRootElementCache, true /*Is explicitly loaded*/, preserveWhitespace);
}

/// <summary>
Expand Down Expand Up @@ -1735,11 +1744,14 @@ internal static ProjectRootElement Create(ProjectRootElementCache projectRootEle
/// Uses the specified project root element cache.
/// May throw InvalidProjectFileException.
/// </summary>
internal static ProjectRootElement Open(string path, ProjectRootElementCache projectRootElementCache, bool isExplicitlyLoaded)
internal static ProjectRootElement Open(string path, ProjectRootElementCache projectRootElementCache, bool isExplicitlyLoaded,
bool preserveWhitespace)
{
ErrorUtilities.VerifyThrowInternalRooted(path);

ProjectRootElement projectRootElement = projectRootElementCache.Get(path, s_openLoaderDelegate, isExplicitlyLoaded);
ProjectRootElement projectRootElement = projectRootElementCache.Get(path,
preserveWhitespace ? s_openLoaderPreserveWhitespaceDelegate : s_openLoaderDelegate,
isExplicitlyLoaded);

return projectRootElement;
}
Expand Down Expand Up @@ -1771,7 +1783,8 @@ internal static ProjectRootElement OpenProjectOrSolution(string fullPath, IDicti

ProjectRootElement projectRootElement = projectRootElementCache.Get(
fullPath,
(path, cache) => CreateProjectFromPath(path, globalProperties, toolsVersion, loggingService, cache, buildEventContext),
(path, cache) => CreateProjectFromPath(path, globalProperties, toolsVersion, loggingService, cache, buildEventContext,
preserveWhitespace: false),
isExplicitlyLoaded);

return projectRootElement;
Expand Down Expand Up @@ -1869,11 +1882,24 @@ protected override ProjectElement CreateNewInstance(ProjectRootElement owner)
/// <param name="path">The path to the file to load.</param>
/// <param name="projectRootElementCache">The cache to load the PRE into.</param>
private static ProjectRootElement OpenLoader(string path, ProjectRootElementCache projectRootElementCache)
{
return OpenLoader(path, projectRootElementCache,
preserveWhitespace: false);
}

private static ProjectRootElement OpenLoaderPreserveWhitespace(string path, ProjectRootElementCache projectRootElementCache)
{
return OpenLoader(path, projectRootElementCache,
preserveWhitespace: true);
}

private static ProjectRootElement OpenLoader(string path, ProjectRootElementCache projectRootElementCache, bool preserveWhitespace)
{
return new ProjectRootElement(
path,
projectRootElementCache,
new BuildEventContext(0, BuildEventContext.InvalidNodeId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId));
new BuildEventContext(0, BuildEventContext.InvalidNodeId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId),
preserveWhitespace);
}

/// <summary>
Expand All @@ -1890,7 +1916,8 @@ private static ProjectRootElement CreateProjectFromPath
string toolsVersion,
ILoggingService loggingService,
ProjectRootElementCache projectRootElementCache,
BuildEventContext buildEventContext
BuildEventContext buildEventContext,
bool preserveWhitespace
)
{
ErrorUtilities.VerifyThrowInternalRooted(projectFile);
Expand All @@ -1903,7 +1930,7 @@ BuildEventContext buildEventContext
}

// OK it's a regular project file, load it normally.
return new ProjectRootElement(projectFile, projectRootElementCache, buildEventContext);
return new ProjectRootElement(projectFile, projectRootElementCache, buildEventContext, preserveWhitespace);
}
catch (InvalidProjectFileException)
{
Expand All @@ -1924,11 +1951,12 @@ BuildEventContext buildEventContext
/// Does NOT add to the ProjectRootElementCache. Caller should add after verifying subsequent MSBuild parsing succeeds.
/// </summary>
/// <param name="fullPath">The full path to the document to load.</param>
private XmlDocumentWithLocation LoadDocument(string fullPath)
private XmlDocumentWithLocation LoadDocument(string fullPath, bool preserveWhitespace)
{
ErrorUtilities.VerifyThrowInternalRooted(fullPath);

XmlDocumentWithLocation document = new XmlDocumentWithLocation();
document.PreserveWhitespace = preserveWhitespace;
#if (!STANDALONEBUILD)
using (new CodeMarkerStartEnd(CodeMarkerEvent.perfMSBuildProjectLoadFromFileBegin, CodeMarkerEvent.perfMSBuildProjectLoadFromFileEnd))
#endif
Expand Down
4 changes: 3 additions & 1 deletion src/XMakeBuildEngine/Definition/Toolset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1011,7 +1011,9 @@ private void LoadAndRegisterFromTasksFile(string searchPath, string[] defaultTas
}
else
{
projectRootElement = ProjectRootElement.Open(defaultTasksFile, projectRootElementCache, false /*The tasks file is not a explicitly loaded file*/);
projectRootElement = ProjectRootElement.Open(defaultTasksFile, projectRootElementCache,
false /*The tasks file is not a explicitly loaded file*/,
preserveWhitespace: false);
}

foreach (ProjectElement elementXml in projectRootElement.Children)
Expand Down
2 changes: 2 additions & 0 deletions src/XMakeBuildEngine/Evaluation/ProjectRootElementCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ internal ProjectRootElement Get(string projectFile, OpenProjectRootElement openP
// use: it checks the file content as well as the timestamp. That's better than completely disabling
// the cache as we get test coverage of the rest of the cache code.
XmlDocument document = new XmlDocument();
// TODO: does PreserveWhitespace need to be an option here?
//document.PreserveWhitespace = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like it'd need to match what gets inserted into the cache, which would depend on how it was loaded, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated this so that it won't use the cached version if it doesn't match the preserveFormatting setting requested.

using (XmlTextReader xtr = new XmlTextReader(projectRootElement.FullPath))
{
xtr.DtdProcessing = DtdProcessing.Ignore;
Expand Down
69 changes: 69 additions & 0 deletions src/XMakeBuildEngine/UnitTests/Evaluation/Preprocessor_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -832,5 +832,74 @@ public void ProjectMetadata()

Helpers.VerifyAssertLineByLine(expected, writer.ToString());
}

// TODO: Test preprocessor formatting (project.SaveLogicalProject)
// TODO: Test loading project from string instead of file
// ProjectRootElement xml = ProjectRootElement.Create(XmlReader.Create(new StringReader(content)));
// TODO: Add test for whether single quotes get changed to double quotes
// TODO: Rename preserveWhitespace parameters (and variable names, etc) to preserveFormatting
// TODO: If preserveFormatting is true, don't change single quotes to double quotes

// TODO: Not sure this actually belongs in the preprocessor tests

[Fact]
public void ProjectCommentFormatting()
{
string content = ObjectModelHelpers.CleanupFileContents(@"
<Project DefaultTargets=`Build` ToolsVersion=`msbuilddefaulttoolsversion` xmlns=`msbuildnamespace`>
<ItemGroup>
<ProjectReference Include=`..\CLREXE\CLREXE.vcxproj`><!-- Comment -->
<Project>{3699f81b-2d03-46c5-abd7-e88a4c946f28}</Project>
</ProjectReference>
</ItemGroup>
</Project>");

VerifyWhitespacePreserved(content);
}

[Fact]
public void ProjectQuoteFormatting()
{
string content = ObjectModelHelpers.CleanupFileContents(@"
<Project DefaultTargets='Build' ToolsVersion='msbuilddefaulttoolsversion' xmlns='msbuildnamespace'>
<ItemGroup>
<ProjectReference Include='..\CLREXE\CLREXE.vcxproj'>
<Project>{3699f81b-2d03-46c5-abd7-e88a4c946f28}</Project>
</ProjectReference>
</ItemGroup>
</Project>");

VerifyWhitespacePreserved(content);
}

void VerifyWhitespacePreserved(string projectContents)
{
string directory = null;

try
{
directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(directory);

string file = Path.Combine(directory, "test.proj");
File.WriteAllText(file, projectContents);

ProjectRootElement xml = ProjectRootElement.Open(file, ProjectCollection.GlobalProjectCollection,
preserveWhitespace: true);
Project project = new Project(xml);
StringWriter writer = new StringWriter();
project.Save(writer);

string expected = @"<?xml version=""1.0"" encoding=""utf-16""?>" +
projectContents;
string actual = writer.ToString();

Helpers.VerifyAssertLineByLine(expected, actual);
}
finally
{
Directory.Delete(directory, true);
}
}
}
}