Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ internal readonly ref struct ConfigurationRuleFollower(IReadOnlyList<Configurati
return value == "*" ? solutionBuildType : value.NullIfEmpty();
}

internal readonly string? GetProjectPlatform(string solutionBuildType, string solutionPlatform)
internal readonly string? GetProjectPlatform(string? solutionBuildType = null, string? solutionPlatform = null)
{
string value = this.GetDimensionValue(BuildDimension.Platform, solutionBuildType, solutionPlatform);
return value == "*" ? solutionPlatform : value.NullIfEmpty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,9 @@ internal static ConfigurationRule CreateNoBuildRule()
{
return new ConfigurationRule(BuildDimension.Build, solutionBuildType: string.Empty, solutionPlatform: string.Empty, projectValue: bool.FalseString);
}

internal static ConfigurationRule CreateNoPlatformsRule()
{
return new ConfigurationRule(BuildDimension.Platform, solutionBuildType: string.Empty, solutionPlatform: string.Empty, projectValue: PlatformNames.Missing);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@ namespace Microsoft.VisualStudio.SolutionPersistence.Model;
internal static class PlatformNames
{
internal const string All = "*";

// Some project types do not support platforms, so this either indicates
// the project type doesn't support platforms or that the platform mapping is missing.
internal const string Missing = "?";

// Used if the project type doesn't support platforms.
internal const string Default = nameof(Default);

internal const string AnyCPU = nameof(AnyCPU);
internal const string AnySpaceCPU = "Any CPU";
internal const string Win32 = nameof(Win32);
Expand Down Expand Up @@ -35,6 +41,8 @@ internal static bool TryGetKnown(StringSpan platform, [NotNullWhen(true)] out st
{
All => All,
Missing => Missing,
Default => Default,
AnyCPU => AnyCPU,
AnySpaceCPU => AnySpaceCPU,
Win32 => Win32,
x64 => x64,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public sealed class ProjectType(Guid projectTypeId, IReadOnlyList<ConfigurationR
/// Gets rules to determine the default configurations for projects of this type.
/// </summary>
/// <remarks>
/// If a project type should not build, it should have a single rule with Build set to false.
/// If a project type should not build, it should have a single rule with Build set to <see langword="false"/>.
/// </remarks>
public IReadOnlyList<ConfigurationRule> ConfigurationRules { get; } = rules;

Expand All @@ -51,8 +51,8 @@ public sealed class ProjectType(Guid projectTypeId, IReadOnlyList<ConfigurationR
public string? Extension { get; init; }

/// <summary>
/// Gets references a base project type to inherit its configuration rules and project type id.
/// This uses the Name or Extension of the base project type to find it.
/// Gets a references to a base project type to inherit its configuration rules and project type id.
/// This uses the <see cref="Name"/> or <see cref="Extension"/> of the base project type to find it.
/// </summary>
public string? BasedOn { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ internal sealed partial class ProjectTypeTable
{
internal static readonly ConfigurationRule[] NoBuildRules = [ModelHelper.CreateNoBuildRule()];

internal static readonly ConfigurationRule NoPlatformsRule = ModelHelper.CreateNoPlatformsRule();

internal static readonly Guid VCXProj = new Guid("8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942");
internal static readonly Guid SolutionFolder = new Guid("2150E333-8FDC-42A3-9474-1A3956D46DE8");

Expand Down Expand Up @@ -87,6 +89,7 @@ internal sealed partial class ProjectTypeTable
new ProjectType(new Guid("00D1A9C2-B5F0-4AF3-8072-F6C62B433612"), NoBuildRules) { Name = "SQL", Extension = ".sqlproj" },
new ProjectType(new Guid("0C603C2C-620A-423B-A800-4F3E2F6281F1"), NoBuildRules) { Name = "U-SQL-DB", Extension = ".usqldbproj" },
new ProjectType(new Guid("182E2583-ECAD-465B-BB50-91101D7C24CE"), NoBuildRules) { Name = "U-SQL", Extension = ".usqlproj" },
new ProjectType(new Guid("F14B399A-7131-4C87-9E4B-1186C45EF12D"), [NoPlatformsRule]) { Name = "SSRS", Extension = ".rptproj" },

// Azure project types
new ProjectType(new Guid("A07B5EB6-E848-4116-A8D0-A826331D98C6"), NoBuildRules) { Name = "Fabric", Extension = ".sfproj" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,10 @@ internal void AddString(string str)
{
_ = this.GetString(str);
}

// Used to test the string table.
internal bool Contains(string str)
{
return this.strings.Contains(str);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,10 @@ static void SetProjectConfigurationPlatforms(SolutionModel solution, SolutionPro
project.AddProjectConfigurationRule(new ConfigurationRule(BuildDimension.BuildType, buildType, platform, BuildTypeNames.Missing));
project.AddProjectConfigurationRule(new ConfigurationRule(BuildDimension.Platform, buildType, platform, PlatformNames.Missing));

// In the old .sln file the default configuration is not to build unless there is a build line.
// This rule will get overwritten by the build line if it exists.
// In the old .sln file the default configuration is not to build/deploy unless there is a build/deploy line.
// This rule will get overwritten by the build/deploy line if it exists.
project.AddProjectConfigurationRule(new ConfigurationRule(BuildDimension.Build, buildType, platform, bool.FalseString));
project.AddProjectConfigurationRule(new ConfigurationRule(BuildDimension.Deploy, buildType, platform, bool.FalseString));
}
}
}
Expand Down Expand Up @@ -336,6 +337,11 @@ void ParseProjectConfigLine(SolutionModel solutionModel, string name, string val
projectModel.AddProjectConfigurationRule(new ConfigurationRule(BuildDimension.BuildType, solutionBuildType, solutionPlatform, projectBuildType));
projectModel.AddProjectConfigurationRule(new ConfigurationRule(BuildDimension.Platform, solutionBuildType, solutionPlatform, projectPlatform));
}
else if (!value.IsNullOrEmpty())
{
// If the project configuration does not have a platform, just set the build type.
projectModel.AddProjectConfigurationRule(new ConfigurationRule(BuildDimension.BuildType, solutionBuildType, solutionPlatform, value));
}

break;
case ConfigLineType.Build:
Expand Down Expand Up @@ -454,18 +460,15 @@ not SectionName.ExtensibilityGlobals and
continue;
}

bool isMissing = mapping.BuildType == BuildTypeNames.Missing || mapping.Platform == PlatformNames.Missing;

// Default project mapping in SLN was to use "Any CPU"
string platform = mapping.Platform;
if (platform == PlatformNames.AnyCPU)
{
platform = PlatformNames.AnySpaceCPU;
}
string platform =
mapping.Platform == PlatformNames.AnyCPU ? PlatformNames.AnySpaceCPU :
mapping.Platform;

string prjCfgPlatString = $"{mapping.BuildType}|{platform}";
// If just the platform is missing, the project doesn't support platforms and only the build type should be written.
string prjCfgPlatString = platform == PlatformNames.Missing ? mapping.BuildType : $"{mapping.BuildType}|{platform}";

if (!isMissing)
if (mapping.BuildType != BuildTypeNames.Missing)
{
WriteProperty(propertyBag, projectId, entry.SlnKey, ActiveCfgSuffix, prjCfgPlatString);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ internal enum Keyword
Extension,
BasedOn,
IsBuildable,
SupportsPlatform,

// Configuration properties
Configuration,
Expand Down Expand Up @@ -87,6 +88,7 @@ static Keywords()
new(nameof(Keyword.Extension), Keyword.Extension),
new(nameof(Keyword.BasedOn), Keyword.BasedOn),
new(nameof(Keyword.IsBuildable), Keyword.IsBuildable),
new(nameof(Keyword.SupportsPlatform), Keyword.SupportsPlatform),
new(nameof(Keyword.Configuration), Keyword.Configuration),
new(nameof(Keyword.Dimension), Keyword.Dimension),
new(nameof(Keyword.BuildType), Keyword.BuildType),
Expand Down Expand Up @@ -125,6 +127,8 @@ internal static StringTable WithSolutionConstants(this StringTable stringTable)
stringTable.AddString(BuildTypeNames.Debug);
stringTable.AddString(BuildTypeNames.Release);
stringTable.AddString(PlatformNames.All);
stringTable.AddString(PlatformNames.Missing);
stringTable.AddString(PlatformNames.Default);
stringTable.AddString(PlatformNames.AnyCPU);
stringTable.AddString(PlatformNames.AnySpaceCPU);
stringTable.AddString(PlatformNames.Win32);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@
<xs:element name="Folder" type="Folder" minOccurs="0" maxOccurs="unbounded" />
<xs:group ref="PropertiesGroup" minOccurs="0" maxOccurs="unbounded"/>
</xs:choice>
<xs:attribute name="Description"/>
<xs:attribute name="SolutionId" />
<xs:attribute name="VisualStudioVersion" />
<xs:attribute name="MinimalVisualStudioVersion" />
<xs:attribute name="Description" type="xs:string" />
<xs:attribute name="Version" type="xs:string" />
</xs:complexType>
</xs:element>

Expand Down Expand Up @@ -38,7 +36,8 @@
<xs:attribute name="Name" type="xs:string" />
<xs:attribute name="Extension" type="xs:string" />
<xs:attribute name="BasedOn" type="xs:string" />
<xs:attribute name="IsBuildable" type="xs:string" default="True" />
<xs:attribute name="IsBuildable" type="xs:boolean" default="true" use="optional" />
<xs:attribute name="SupportsPlatform" type="xs:boolean" default="true" use="optional" />
</xs:complexType>

<xs:complexType name="Folder">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ public readonly struct SlnxSerializerSettings(SlnxSerializerSettings settings)
{
/// <summary>
/// Gets a value indicating whether to keep whitespace when writing the solution file.
/// If this is true, the solution file will be written with the same whitespace as the original file.
/// Default is true.
/// If this is <see langword="true"/>, the solution file will be written with the same whitespace as the original file.
/// Default is <see langword="true"/>.
/// </summary>
public bool? PreserveWhitespace { get; init; } = settings.PreserveWhitespace;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ internal string ConvertToUserPath(string projectPath)
/// Update the Xml DOM with changes from the model.
/// </summary>
/// <returns>
/// true if any changes were made to the XML.
/// <see langword="true"/> if any changes were made to the XML.
/// </returns>
internal bool ApplyModel(SolutionModel model)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ internal List<string> DebugChildNodes
/// <param name="decoratorItems">The list of existing decorator items in the XML.</param>
/// <param name="decoratorElementName">The element name for the decorator, can be dynamic by using getDecoratorElementName.</param>
/// <param name="applyModelToXml">Applies the model item changes to the decorator.</param>
/// <returns>true if the XML was changed.</returns>
/// <returns><see langword="true"/> if the XML was changed.</returns>
internal bool ApplyModelItemsToXml<TModelItem, TDecorator>(
List<(string ItemRef, TModelItem Item)>? modelItems,
ref ItemRefList<TDecorator> decoratorItems,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ internal override void UpdateFromXml()

/// <summary>
/// Wraps the given element with a new decorator and adds it to the cache.
/// If this is a new element, pass itemRef and validateItemRef to true.
/// If this is a new element, pass itemRef and validateItemRef to <see langword="true"/>.
/// </summary>
private XmlDecorator? CreateChildDecorator(XmlElement xmlElement, string? itemRef = null, bool validateItemRef = false)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,36 +18,61 @@ internal sealed class XmlProjectType(SlnxFile root, XmlElement element) :

public Keyword ItemRefAttribute => Keyword.TypeId;

/// <inheritdoc cref="ProjectType.ProjectTypeId"/>
internal Guid TypeId
{
get => this.GetXmlAttributeGuid(Keyword.TypeId);
set => this.UpdateXmlAttributeGuid(Keyword.TypeId, value);
}

/// <inheritdoc cref="ProjectType.Name"/>
internal string? Name
{
get => this.GetXmlAttribute(Keyword.Name);
set => this.UpdateXmlAttribute(Keyword.Name, value);
}

/// <inheritdoc cref="ProjectType.Extension"/>
internal string? Extension
{
get => this.GetXmlAttribute(Keyword.Extension);
set => this.UpdateXmlAttribute(Keyword.Extension, value);
}

/// <inheritdoc cref="ProjectType.BasedOn"/>
internal string? BasedOn
{
get => this.GetXmlAttribute(Keyword.BasedOn);
set => this.UpdateXmlAttribute(Keyword.BasedOn, value);
}

/// <summary>
/// Gets or sets a value indicating whether the project type is buildable.
/// </summary>
/// <remarks>
/// Default is <see langword="true"/>.
/// When <see langword="false"/> automatically sets configuration rules to never build.
/// </remarks>
internal bool IsBuildable
{
get => this.GetXmlAttributeBool(Keyword.IsBuildable, defaultValue: true);
set => this.UpdateXmlAttributeBool(Keyword.IsBuildable, value, defaultValue: true);
}

/// <summary>
/// Gets or sets a value indicating whether the project type supports platform configurations.
/// </summary>
/// <remarks>
/// Default is <see langword="true"/>.
/// When <see langword="false"/> automatically adds configuration rule to remove platform mappings.
/// This setting is ignored if <see cref="IsBuildable"/> is <see langword="false"/>.
/// </remarks>
internal bool SupportsPlatform
{
get => this.GetXmlAttributeBool(Keyword.SupportsPlatform, defaultValue: true);
set => this.UpdateXmlAttributeBool(Keyword.SupportsPlatform, value, defaultValue: true);
}

private protected override bool AllowEmptyItemRef => true;

/// <summary>
Expand Down Expand Up @@ -124,7 +149,10 @@ internal override bool IsValid()

internal ProjectType ToModel()
{
ConfigurationRule[] rules = this.IsBuildable ? [.. this.configurationRules.ToModel()] : ProjectTypeTable.NoBuildRules;
ConfigurationRule[] rules =
!this.IsBuildable ? ProjectTypeTable.NoBuildRules :
!this.SupportsPlatform ? [ProjectTypeTable.NoPlatformsRule, .. this.configurationRules.ToModel()] :
/*default*/ [.. this.configurationRules.ToModel()];

return new ProjectType(this.TypeId, rules)
{
Expand Down Expand Up @@ -164,14 +192,34 @@ internal bool ApplyModelToXml(ProjectType modelProjectType)

ConfigurationRuleFollower rules = new ConfigurationRuleFollower(modelProjectType.ConfigurationRules);
bool isBuildable = rules.GetIsBuildable() ?? true;
bool supportsPlatform = rules.GetProjectPlatform() != PlatformNames.Missing;

if (this.IsBuildable != isBuildable)
{
this.IsBuildable = isBuildable;
modified = true;
}

modified |= this.configurationRules.ApplyModelToXml(this, isBuildable ? modelProjectType.ConfigurationRules : []);
if (this.SupportsPlatform != supportsPlatform)
{
this.SupportsPlatform = supportsPlatform;
modified = true;
}

// Determine which rules to serizlize. Remove rules implied by IsBuildable and SupportsPlatform.
IReadOnlyList<ConfigurationRule>? rulesToApply =
!isBuildable ? [] :
!supportsPlatform ? RemovePlatformRules(modelProjectType.ConfigurationRules) :
modelProjectType.ConfigurationRules;

modified |= this.configurationRules.ApplyModelToXml(this, rulesToApply);
return modified;

// Remove any platform rules from the list.
static List<ConfigurationRule> RemovePlatformRules(IReadOnlyList<ConfigurationRule> rules) =>
rules.WhereToList(
predicate: static (rule, _) => rule.Dimension != BuildDimension.Platform,
selector: static (rule, _) => rule,
(object?)null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ public class RoundTripClassicSln
[Fact]
public Task FolderIdAsync() => TestRoundTripSerializerAsync(SlnAssets.LoadResource("FolderId.sln"));

[Fact]
public Task ReportProjectAsync() => TestRoundTripSerializerAsync(SlnAssets.LoadResource("Report Project.sln"));

[Theory]
[MemberData(nameof(ClassicSlnFiles))]
public Task AllClassicSolutionAsync(ResourceName sampleFile)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public class RoundTripClassicSlnThruSlnxStream
[Fact]
public Task MissingConfigurationsThruSlnxStreamAsync() => TestRoundTripSerializerAsync(SlnAssets.ClassicSlnMissingConfigurations, SlnAssets.XmlSlnxMissingConfigurations);

[Fact]
public Task ReportProjectThruSlnxStreamAsync() => TestRoundTripSerializerAsync(SlnAssets.LoadResource("Report Project.sln"), SlnAssets.LoadResource("Report Project.slnx"));

/// <summary>
/// Round trip a .SLN file through the slnx serializer.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ public class RoundTripXmlSlnx
[Fact]
public Task VersionMinAsync() => TestRoundTripSerializerAsync(SlnAssets.XmlSlnxVersionMin);

[Fact]
public Task ReportProject() => TestRoundTripSerializerAsync(SlnAssets.LoadResource("Report Project.slnx"));

[Theory]
[MemberData(nameof(XmlSlnxFiles))]
public Task AllXmlSolutionAsync(ResourceName sampleFile)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ public Task CommentsAsync()
[Fact]
public Task TraditionalAsync() => TestRoundTripSerializerAsync(SlnAssets.XmlSlnxTraditional);

[Fact]
public Task ReportProjectAsync() => TestRoundTripSerializerAsync(SlnAssets.LoadResource("Report Project.slnx"));

private static async Task TestRoundTripSerializerAsync(ResourceStream slnStream)
{
// Open the Model from stream.
Expand Down
Loading