diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index b57f73a5..e60546c0 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,19 +3,19 @@
"isRoot": true,
"tools": {
"powershell": {
- "version": "7.4.5",
+ "version": "7.4.6",
"commands": [
"pwsh"
]
},
"dotnet-coverage": {
- "version": "17.12.5",
+ "version": "17.12.6",
"commands": [
"dotnet-coverage"
]
},
"nbgv": {
- "version": "3.6.143",
+ "version": "3.6.146",
"commands": [
"nbgv"
]
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index b3336ca3..9626b31b 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -1,5 +1,5 @@
# Refer to https://hub.docker.com/_/microsoft-dotnet-sdk for available versions
-FROM mcr.microsoft.com/dotnet/sdk:8.0.400-jammy
+FROM mcr.microsoft.com/dotnet/sdk:8.0.402-jammy
# Installing mono makes `dotnet test` work without errors even for net472.
# But installing it takes a long time, so it's excluded by default.
diff --git a/.github/workflows/libtemplate-update.yml b/.github/workflows/libtemplate-update.yml
new file mode 100644
index 00000000..564d4af2
--- /dev/null
+++ b/.github/workflows/libtemplate-update.yml
@@ -0,0 +1,72 @@
+name: Library.Template update
+
+# PREREQUISITE: This workflow requires the repo to be configured to allow workflows to push commits and create pull requests.
+# Visit https://github.com/USER/REPO/settings/actions
+# Under "Workflow permissions", select "Read and write permissions" and check "Allow GitHub Actions to create ...pull requests"
+# Click Save.
+
+on:
+ schedule:
+ - cron: "0 3 * * Mon" # Sun @ 8 or 9 PM Mountain Time (depending on DST)
+ workflow_dispatch:
+
+jobs:
+ merge:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
+
+ - name: merge
+ shell: pwsh
+ run: |
+ $LibTemplateBranch = & ./azure-pipelines/Get-LibTemplateBasis.ps1 -ErrorIfNotRelated
+ if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
+ }
+
+ git fetch https://github.com/aarnott/Library.Template $LibTemplateBranch
+ if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
+ }
+ $LibTemplateCommit = git rev-parse FETCH_HEAD
+
+ if ((git rev-list FETCH_HEAD ^HEAD --count) -eq 0) {
+ Write-Host "There are no Library.Template updates to merge."
+ exit 0
+ }
+
+ git -c http.extraheader="AUTHORIZATION: bearer $env:GH_TOKEN" push origin -u FETCH_HEAD:refs/heads/auto/libtemplateUpdate
+ - name: pull request
+ shell: pwsh
+ run: |
+ # If there is already an active pull request, don't create a new one.
+ $existingPR = gh pr list -H auto/libtemplateUpdate --json url | ConvertFrom-Json
+ if ($existingPR) {
+ Write-Host "::warning::Skipping pull request creation because one already exists at $($existingPR[0].url)"
+ exit 0
+ }
+
+ $prTitle = "Merge latest Library.Template"
+ $prBody = "This merges the latest features and fixes from [Library.Template's branch](https://github.com/AArnott/Library.Template/tree/).
+
+
+ Merge conflicts?
+ Resolve merge conflicts locally by carrying out these steps:
+
+ ```
+ git fetch
+ git checkout auto/libtemplateUpdate
+ git merge origin/main
+ # resolve conflicts
+ git commit
+ git push
+ ```
+
+
+ ⚠️ Do **not** squash this pull request when completing it. You must *merge* it."
+
+ gh pr create -H auto/libtemplateUpdate -b $prBody -t $prTitle
+ env:
+ GH_TOKEN: ${{ github.token }}
diff --git a/.gitignore b/.gitignore
index 3f1c5ed9..cc2b1247 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,6 +37,9 @@ bld/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
+# Jetbrains Rider cache directory
+.idea/
+
# Visual Studio 2017 auto generated files
Generated\ Files/
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 871b8e12..8c881b6e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -5,7 +5,7 @@
true
true
- 2.0.165
+ 2.0.171
@@ -24,7 +24,7 @@
-
+
diff --git a/azure-pipelines/OptProf_part2.yml b/azure-pipelines/OptProf_part2.yml
index cb8b2c6d..5f9a5b1c 100644
--- a/azure-pipelines/OptProf_part2.yml
+++ b/azure-pipelines/OptProf_part2.yml
@@ -14,6 +14,8 @@ resources:
- pipeline: DartLab.OptProf
source: DartLab.OptProf
branch: main
+ tags:
+ - production
repositories:
- repository: DartLabTemplates
type: git
@@ -22,7 +24,7 @@ resources:
- repository: DartLabOptProfTemplates
type: git
name: DartLab.OptProf
- ref: refs/heads/main
+ ref: refs/tags/Production
parameters:
diff --git a/azure-pipelines/Publish-Legacy-Symbols.ps1 b/azure-pipelines/Prepare-Legacy-Symbols.ps1
similarity index 91%
rename from azure-pipelines/Publish-Legacy-Symbols.ps1
rename to azure-pipelines/Prepare-Legacy-Symbols.ps1
index 5c4035a1..ae0bc40c 100644
--- a/azure-pipelines/Publish-Legacy-Symbols.ps1
+++ b/azure-pipelines/Prepare-Legacy-Symbols.ps1
@@ -33,5 +33,3 @@ Get-ChildItem "$ArtifactStagingFolder\*.pdb" -Recurse |% {
Move-Item $legacyPdbPath $_ -Force
}
}
-
-Write-Host "##vso[artifact.upload containerfolder=symbols-legacy;artifactname=symbols-legacy;]$ArtifactStagingFolder"
diff --git a/azure-pipelines/build.yml b/azure-pipelines/build.yml
index f73003d8..35746ea6 100644
--- a/azure-pipelines/build.yml
+++ b/azure-pipelines/build.yml
@@ -4,6 +4,12 @@ parameters:
##### Feel free to adjust their default value as needed.
# Whether this repo uses OptProf to optimize the built binaries.
+# When enabling this, be sure to update these files:
+# - OptProf.targets: InstallationPath and match TestCase selection with what's in the VS repo.
+# - The project file(s) for the libraries to optimize must import OptProf.targets (for multi-targeted projects, only import it for ONE target).
+# - OptProf.yml: Search for LibraryName (or your library's name) and verify that those names are appropriate.
+# - OptProf_part2.yml: Search for LibraryName (or your library's name) and verify that those names are appropriate.
+# and create pipelines for OptProf.yml, OptProf_part2.yml
- name: EnableOptProf
type: boolean
default: false
@@ -94,7 +100,7 @@ parameters:
- name: macOSPool
type: object
default:
- vmImage: macOS-12
+ vmImage: macOS-14
jobs:
- job: Windows
@@ -270,6 +276,15 @@ jobs:
- macOS
pool: ${{ parameters.windowsPool }} # Use Windows agent because PublishSymbols task requires it (https://github.com/microsoft/azure-pipelines-tasks/issues/13821).
condition: succeededOrFailed()
+ ${{ if eq(variables['system.collectionId'], '011b8bdf-6d56-4f87-be0d-0092136884d9') }}:
+ templateContext:
+ outputParentDirectory: $(Build.ArtifactStagingDirectory)
+ outputs:
+ - output: pipelineArtifact
+ displayName: 📢 Publish symbols-legacy
+ targetPath: $(Build.ArtifactStagingDirectory)/symbols-legacy
+ artifactName: symbols-legacy
+ condition: succeededOrFailed()
steps:
- checkout: self
fetchDepth: 0 # avoid shallow clone so nbgv can do its work.
diff --git a/azure-pipelines/dotnet.yml b/azure-pipelines/dotnet.yml
index 196fd4b5..ef6152f2 100644
--- a/azure-pipelines/dotnet.yml
+++ b/azure-pipelines/dotnet.yml
@@ -8,7 +8,7 @@ parameters:
steps:
-- script: dotnet build -t:build,pack --no-restore -c $(BuildConfiguration) -warnaserror /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog"
+- script: dotnet build -t:build,pack --no-restore -c $(BuildConfiguration) -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904 /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog"
displayName: 🛠 dotnet build
- ${{ if not(parameters.IsOptProf) }}:
diff --git a/azure-pipelines/microbuild.after.yml b/azure-pipelines/microbuild.after.yml
index f5b921aa..e2107433 100644
--- a/azure-pipelines/microbuild.after.yml
+++ b/azure-pipelines/microbuild.after.yml
@@ -29,7 +29,6 @@ steps:
usePat: true
displayName: 📢 Publish to Artifact Services - ProfilingInputs
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
- continueOnError: true
- task: PublishBuildArtifacts@1
inputs:
diff --git a/azure-pipelines/publish-symbols.yml b/azure-pipelines/publish-symbols.yml
index ddf82352..9078ea25 100644
--- a/azure-pipelines/publish-symbols.yml
+++ b/azure-pipelines/publish-symbols.yml
@@ -63,5 +63,5 @@ steps:
SymbolServerType: TeamServices
displayName: 📢 Publish test symbols
-- powershell: azure-pipelines/Publish-Legacy-Symbols.ps1 -Path $(Pipeline.Workspace)/symbols/Windows
- displayName: 📢 Publish symbols for symbol archival
+- powershell: azure-pipelines/Prepare-Legacy-Symbols.ps1 -Path $(Pipeline.Workspace)/symbols/Windows
+ displayName: ⚙ Prepare symbols for symbol archival
diff --git a/global.json b/global.json
index b769fe51..f9835d26 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "8.0.400",
+ "version": "8.0.402",
"rollForward": "patch",
"allowPrerelease": false
},
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Errors.Designer.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Errors.Designer.cs
index 27949b96..1f985ed7 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Errors.Designer.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Errors.Designer.cs
@@ -206,7 +206,7 @@ internal static string InvalidModelItem {
///
/// Looks up a localized string similar to Names cannot:
- ///- contain any of the following characters: / ? : \ * " < > | # & %
+ ///- contain any of the following characters: / ? : \ * " < > |
///- contain control characters
///- be system reserved names, including 'CON', 'AUX', 'PRN', 'COM1' or 'LPT2'
///- be '.' or '..'.
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Errors.resx b/src/Microsoft.VisualStudio.SolutionPersistence/Errors.resx
index 6be5d561..a3755c1b 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Errors.resx
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Errors.resx
@@ -199,7 +199,7 @@
Names cannot:
-- contain any of the following characters: / ? : \ * " < > | # & %
+- contain any of the following characters: / ? : \ * " < > |
- contain control characters
- be system reserved names, including 'CON', 'AUX', 'PRN', 'COM1' or 'LPT2'
- be '.' or '..'
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Model/ConfigurationRuleFollower.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Model/ConfigurationRuleFollower.cs
index 85247146..6d65615a 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Model/ConfigurationRuleFollower.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Model/ConfigurationRuleFollower.cs
@@ -6,7 +6,7 @@ namespace Microsoft.VisualStudio.SolutionPersistence.Model;
///
/// Helper to process configuration rules.
///
-internal readonly struct ConfigurationRuleFollower(IReadOnlyList? configurationRules)
+internal readonly ref struct ConfigurationRuleFollower(IReadOnlyList? configurationRules)
{
private readonly IReadOnlyList? configurationRules = configurationRules;
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Model/ProjectTypeTable.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Model/ProjectTypeTable.cs
index 782e72ef..8681e20a 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Model/ProjectTypeTable.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Model/ProjectTypeTable.cs
@@ -41,7 +41,7 @@ private ProjectTypeTable(bool isBuiltIn, List projectTypes)
!this.fromExtension.TryAdd(GetExtension(type.Extension), type))
{
string projectType = type.GetDisplayName();
- throw new SolutionException(string.Format(Errors.DuplicateExtension_Args2, type.Extension, projectType));
+ throw new SolutionException(string.Format(Errors.DuplicateExtension_Args2, type.Extension, projectType), SolutionErrorType.DuplicateExtension);
}
if (!type.Name.IsNullOrEmpty())
@@ -49,14 +49,14 @@ private ProjectTypeTable(bool isBuiltIn, List projectTypes)
if (!this.fromName.TryAdd(type.Name, type))
{
string projectType = type.GetDisplayName();
- throw new SolutionException(string.Format(Errors.DuplicateName_Args2, type.Name, projectType));
+ throw new SolutionException(string.Format(Errors.DuplicateName_Args2, type.Name, projectType), SolutionErrorType.DuplicateName);
}
// If a name isn't provided, it is just to map an extension to a project type.
if (type.ProjectTypeId != Guid.Empty && !this.fromProjectTypeId.TryAdd(type.ProjectTypeId, type))
{
string projectType = type.GetDisplayName();
- throw new SolutionException(string.Format(Errors.DuplicateProjectTypeId_Args2, type.ProjectTypeId, projectType));
+ throw new SolutionException(string.Format(Errors.DuplicateProjectTypeId_Args2, type.ProjectTypeId, projectType), SolutionErrorType.DuplicateProjectTypeId);
}
}
@@ -64,7 +64,7 @@ private ProjectTypeTable(bool isBuiltIn, List projectTypes)
{
if (this.defaultRules is not null)
{
- throw new SolutionException(Errors.DuplicateDefaultProjectType);
+ throw new SolutionException(Errors.DuplicateDefaultProjectType, SolutionErrorType.DuplicateDefaultProjectType);
}
this.defaultRules ??= type.ConfigurationRules;
@@ -77,7 +77,7 @@ private ProjectTypeTable(bool isBuiltIn, List projectTypes)
{
if (this.GetBasedOnType(type) is null)
{
- throw new SolutionException(string.Format(Errors.InvalidProjectTypeReference_Args1, type.BasedOn));
+ throw new SolutionException(string.Format(Errors.InvalidProjectTypeReference_Args1, type.BasedOn), SolutionErrorType.InvalidProjectTypeReference);
}
// Check for loops in the BasedOn chain using Floyd's cycle-finding algorithm.
@@ -88,7 +88,7 @@ private ProjectTypeTable(bool isBuiltIn, List projectTypes)
if (object.ReferenceEquals(currentSlow, currentFast))
{
string projectType = type.GetDisplayName();
- throw new SolutionException(string.Format(Errors.InvalidLoop_Args1, projectType));
+ throw new SolutionException(string.Format(Errors.InvalidLoop_Args1, projectType), SolutionErrorType.InvalidLoop);
}
currentSlow = this.GetBasedOnType(currentSlow);
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionArgumentException.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionArgumentException.cs
new file mode 100644
index 00000000..e94134a9
--- /dev/null
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionArgumentException.cs
@@ -0,0 +1,65 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+
+namespace Microsoft.VisualStudio.SolutionPersistence.Model;
+
+///
+/// Represents an argument exception inside the solution.
+///
+public class SolutionArgumentException : ArgumentException
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Message to be shown with the exception.
+ /// Reason for the exception.
+ public SolutionArgumentException(string? message, SolutionErrorType type)
+ : base(message)
+ {
+ this.Type = type;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Message to be shown with the exception.
+ /// Exception that triggered this exception.
+ /// Reason for the exception.
+ public SolutionArgumentException(string? message, Exception? innerException, SolutionErrorType type)
+ : base(message, innerException)
+ {
+ this.Type = type;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Message to be shown with the exception.
+ /// Name of parameter that triggered this exception.
+ /// Reason for the exception.
+ public SolutionArgumentException(string? message, string? paramName, SolutionErrorType type)
+ : base(message, paramName)
+ {
+ this.Type = type;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Message to be shown with the exception.
+ /// Name of parameter that triggered this exception.
+ /// Exception that triggered this exception.
+ /// Reason for the exception.
+ public SolutionArgumentException(string? message, string? paramName, Exception? innerException, SolutionErrorType type)
+ : base(message, paramName, innerException)
+ {
+ this.Type = type;
+ }
+
+ ///
+ /// Gets reason why the exception was raised.
+ ///
+ public SolutionErrorType Type { get; init; }
+}
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionConfigurationMap.Rules.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionConfigurationMap.Rules.cs
index 907de597..d374a1fd 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionConfigurationMap.Rules.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionConfigurationMap.Rules.cs
@@ -31,7 +31,7 @@ internal sealed partial class SolutionConfigurationMap
///
/// The mappings to update.
/// The rules to run, scoped to their effect.
- private void ApplyRules(in SolutionToProjectMappings projectMappings, in ScopedRules scopedRules)
+ private void ApplyRules(in SolutionToProjectMappings projectMappings, scoped in ScopedRules scopedRules)
{
int iBuildTypeBegin = scopedRules.BuildTypeIndex == ScopedRules.All ? 0 : scopedRules.BuildTypeIndex;
int iBuildTypeEnd = scopedRules.BuildTypeIndex == ScopedRules.All ? this.BuildTypesCount : scopedRules.BuildTypeIndex + 1;
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionConfigurationMap.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionConfigurationMap.cs
index 3a84e19e..1334316f 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionConfigurationMap.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionConfigurationMap.cs
@@ -9,8 +9,8 @@ namespace Microsoft.VisualStudio.SolutionPersistence.Model;
internal sealed partial class SolutionConfigurationMap
{
private readonly SolutionModel solutionModel;
- private readonly Dictionary buildTypesIndex = [];
- private readonly Dictionary platformsIndex = [];
+ private readonly Dictionary buildTypesIndex;
+ private readonly Dictionary platformsIndex;
private readonly Dictionary perProjectCurrent = [];
@@ -19,11 +19,13 @@ internal sealed partial class SolutionConfigurationMap
internal SolutionConfigurationMap(SolutionModel solutionModel)
{
this.solutionModel = solutionModel;
+ this.buildTypesIndex = new Dictionary(solutionModel.BuildTypes.Count);
for (int i = 0; i < solutionModel.BuildTypes.Count; i++)
{
this.buildTypesIndex.Add(solutionModel.BuildTypes[i], i);
}
+ this.platformsIndex = new Dictionary(solutionModel.Platforms.Count);
for (int i = 0; i < solutionModel.Platforms.Count; i++)
{
this.platformsIndex.Add(PlatformNames.Canonical(solutionModel.Platforms[i]), i);
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionErrorType.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionErrorType.cs
new file mode 100644
index 00000000..6c6d6f20
--- /dev/null
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionErrorType.cs
@@ -0,0 +1,130 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Microsoft.VisualStudio.SolutionPersistence.Model;
+
+///
+/// Reasons the SolutionArgumentException was raised.
+///
+public enum SolutionErrorType
+{
+ ///
+ /// The cause of the error is not specified.
+ ///
+ Undefined,
+
+ ///
+ /// There was an error while trying to move a folder to a child folder.
+ ///
+ CannotMoveFolderToChildFolder,
+
+ ///
+ /// The default project type was duplicated.
+ ///
+ DuplicateDefaultProjectType,
+
+ ///
+ /// File has two extensions.
+ ///
+ DuplicateExtension,
+
+ ///
+ /// Item already exists in the solution.
+ ///
+ DuplicateItemRef,
+
+ ///
+ /// Name of item is duplicate.
+ ///
+ DuplicateName,
+
+ ///
+ /// A project with the same name already exists.
+ ///
+ DuplicateProjectName,
+
+ ///
+ /// A project with the same path already exists.
+ ///
+ DuplicateProjectPath,
+
+ ///
+ /// This project type is already specified.
+ ///
+ DuplicateProjectTypeId,
+
+ ///
+ /// Invalid syntax for solution configuration.
+ ///
+ InvalidConfiguration,
+
+ ///
+ /// Invalid encoding for solution.
+ ///
+ InvalidEncoding,
+
+ ///
+ /// Folder path doesn't follow correct format.
+ ///
+ InvalidFolderPath,
+
+ ///
+ /// Folder was not found.
+ ///
+ InvalidFolderReference,
+
+ ///
+ /// Item is not valid.
+ ///
+ InvalidItemRef,
+
+ ///
+ /// Found a circular dependency.
+ ///
+ InvalidLoop,
+
+ ///
+ /// Model does not belong to this solution.
+ ///
+ InvalidModelItem,
+
+ ///
+ /// Name of item is not valid.
+ ///
+ InvalidName,
+
+ ///
+ /// Project was not found.
+ ///
+ InvalidProjectReference,
+
+ ///
+ /// Project type was not found.
+ ///
+ InvalidProjectTypeReference,
+
+ ///
+ /// File version is not supported.
+ ///
+ InvalidVersion,
+
+ ///
+ /// Empty value for project attribute.
+ ///
+ MissingProjectValue,
+
+ ///
+ /// The file is not a solution file.
+ ///
+ NotSolution,
+
+ ///
+ /// This veersion is not supported.
+ ///
+ UnsupportedVersion,
+
+ ///
+ /// Invalid decorator element name.
+ ///
+ InvalidXmlDecoratorElementName,
+}
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionException.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionException.cs
index 767eb8d2..dc6e40cc 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionException.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionException.cs
@@ -26,6 +26,7 @@ public SolutionException()
public SolutionException(string message)
: base(message)
{
+ this.ErrorType = SolutionErrorType.Undefined;
}
///
@@ -36,6 +37,30 @@ public SolutionException(string message)
public SolutionException(string message, Exception inner)
: base(message, inner)
{
+ this.ErrorType = SolutionErrorType.Undefined;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The type of error associated to this exception.
+ public SolutionException(string message, SolutionErrorType errorType)
+ : base(message)
+ {
+ this.ErrorType = errorType;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception.
+ /// The type of error associated to this exception.
+ public SolutionException(string message, Exception inner, SolutionErrorType errorType)
+ : base(message, inner)
+ {
+ this.ErrorType = errorType;
}
#if NETFRAMEWORK
@@ -57,6 +82,11 @@ protected SolutionException(System.Runtime.Serialization.SerializationInfo info,
}
#endif
+ ///
+ /// Gets error type.
+ ///
+ public SolutionErrorType? ErrorType { get; init; }
+
///
/// Gets file the error occurred in if known.
///
@@ -84,19 +114,19 @@ public override void GetObjectData(System.Runtime.Serialization.SerializationInf
}
#endif
- internal static SolutionException Create(string message, XmlDecorator location)
+ internal static SolutionException Create(string message, XmlDecorator location, SolutionErrorType errorType = SolutionErrorType.Undefined)
{
return location?.XmlElement is IXmlLineInfo lineInfo && lineInfo.HasLineInfo() ?
- new SolutionException(message) { Line = lineInfo.LineNumber, Column = lineInfo.LinePosition, File = location.Root.FullPath } :
- new SolutionException(message) { File = location?.Root.FullPath };
+ new SolutionException(message, errorType) { Line = lineInfo.LineNumber, Column = lineInfo.LinePosition, File = location.Root.FullPath } :
+ new SolutionException(message, errorType) { File = location?.Root.FullPath };
}
- internal static SolutionException Create(Exception innerException, XmlDecorator location, string? message = null)
+ internal static SolutionException Create(Exception innerException, XmlDecorator location, string? message = null, SolutionErrorType errorType = SolutionErrorType.Undefined)
{
message ??= innerException.Message;
return location?.XmlElement is IXmlLineInfo lineInfo && lineInfo.HasLineInfo() ?
- new SolutionException(message, innerException) { Line = lineInfo.LineNumber, Column = lineInfo.LinePosition, File = location.Root.FullPath } :
- new SolutionException(message, innerException) { File = location?.Root.FullPath };
+ new SolutionException(message, innerException, errorType) { Line = lineInfo.LineNumber, Column = lineInfo.LinePosition, File = location.Root.FullPath } :
+ new SolutionException(message, innerException, errorType) { File = location?.Root.FullPath };
}
// Checks if an exception caught during serialization should be wrapped by a SolutionException to add position information.
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionFolderModel.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionFolderModel.cs
index 0023dcb4..541b0183 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionFolderModel.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionFolderModel.cs
@@ -62,7 +62,7 @@ public string Name
string testName = $"{this.Parent?.ItemRef ?? "/"}{value}/";
if (this.Solution.FindFolder(testName) is not null)
{
- throw new ArgumentException(string.Format(Errors.DuplicateItemRef_Args2, testName, "Folder"), nameof(value));
+ throw new SolutionArgumentException(string.Format(Errors.DuplicateItemRef_Args2, testName, "Folder"), nameof(value), SolutionErrorType.DuplicateItemRef);
}
string oldName = this.name;
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionItemModel.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionItemModel.cs
index c863b9cb..13b5d9c4 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionItemModel.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionItemModel.cs
@@ -62,7 +62,7 @@ public Guid Id
{
if (this.Solution.FindItemById(value) is not null)
{
- throw new ArgumentException(string.Format(Errors.DuplicateItemRef_Args2, value, this.GetType().Name), nameof(value));
+ throw new SolutionArgumentException(string.Format(Errors.DuplicateItemRef_Args2, value, this.GetType().Name), nameof(value), SolutionErrorType.DuplicateItemRef);
}
Guid? oldId = this.id ?? this.defaultId;
@@ -114,7 +114,7 @@ public void MoveToFolder(SolutionFolderModel? folder)
{
if (ReferenceEquals(parents, this))
{
- throw new ArgumentException(Errors.CannotMoveFolderToChildFolder, nameof(folder));
+ throw new SolutionArgumentException(Errors.CannotMoveFolderToChildFolder, nameof(folder), SolutionErrorType.CannotMoveFolderToChildFolder);
}
}
@@ -122,6 +122,13 @@ public void MoveToFolder(SolutionFolderModel? folder)
try
{
this.Parent = folder;
+
+ // Reevaulate the id.
+ if (this.id == this.DefaultId)
+ {
+ this.id = null;
+ }
+
if (this is SolutionProjectModel thisProject)
{
this.Solution.ValidateProjectName(thisProject);
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionModel.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionModel.cs
index 2312e453..5d94160d 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionModel.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionModel.cs
@@ -14,9 +14,9 @@ namespace Microsoft.VisualStudio.SolutionPersistence.Model;
public sealed class SolutionModel : PropertyContainerModel
{
#if NETFRAMEWORK
- private const string InvalidNameChars = @"?:&\/*""<>|#%";
+ private const string InvalidNameChars = @"?:\/*""<>|";
#else
- private static readonly SearchValues InvalidNameChars = SearchValues.Create(@"?:&\/*""<>|#%");
+ private static readonly SearchValues InvalidNameChars = SearchValues.Create(@"?:\/*""<>|");
#endif
private readonly VisualStudioProperties visualStudioProperties;
@@ -177,7 +177,7 @@ public SolutionFolderModel AddFolder(string path)
Argument.ThrowIfNullOrEmpty(path, nameof(path));
if (!path.StartsWith('/') || !path.EndsWith('/'))
{
- throw new ArgumentException(string.Format(Errors.InvalidFolderPath_Args1, path), nameof(path));
+ throw new SolutionArgumentException(string.Format(Errors.InvalidFolderPath_Args1, path), nameof(path), SolutionErrorType.InvalidFolderPath);
}
SolutionFolderModel? existingFolder = this.FindFolder(path);
@@ -193,7 +193,12 @@ public SolutionFolderModel AddFolder(string path)
string? parentItemRef = lastSlash > 0 ? folderPath.Slice(0, lastSlash + 1).ToString() : null;
StringSpan newName = lastSlash > 0 ? folderPath.Slice(lastSlash + 1) : folderPath.Slice(1);
- return this.AddFolder(newName, parentItemRef);
+ SolutionFolderModel folder = this.AddFolder(newName, parentItemRef);
+
+ // Ensure the project type is in the project type table, if it is not already.
+ this.solutionItemsById[folder.Id] = folder;
+
+ return folder;
}
///
@@ -213,7 +218,7 @@ public SolutionProjectModel AddProject(string filePath, string? projectTypeName
Guid projectTypeId =
Guid.TryParse(projectTypeName, out Guid projectTypeGuid) ? projectTypeGuid :
this.ProjectTypeTable.GetProjectTypeId(projectTypeName, Path.GetExtension(filePath.AsSpan())) ??
- throw new ArgumentException(string.Format(Errors.InvalidProjectTypeReference_Args1, projectTypeName), nameof(projectTypeName));
+ throw new SolutionArgumentException(string.Format(Errors.InvalidProjectTypeReference_Args1, projectTypeName), nameof(projectTypeName), SolutionErrorType.InvalidProjectReference);
return this.AddProject(filePath, projectTypeName ?? string.Empty, projectTypeId, folder);
}
@@ -337,7 +342,7 @@ public bool RemovePlatform(string platform)
Argument.ThrowIfNullOrEmpty(path, nameof(path));
if (!path.StartsWith('/') || !path.EndsWith('/'))
{
- throw new ArgumentException(string.Format(Errors.InvalidFolderPath_Args1, path), nameof(path));
+ throw new SolutionArgumentException(string.Format(Errors.InvalidFolderPath_Args1, path), nameof(path), SolutionErrorType.InvalidFolderPath);
}
return ModelHelper.FindByItemRef(this.solutionFolders, path);
@@ -386,13 +391,13 @@ internal static void ValidateName(StringSpan name)
{
if (char.IsControl(c) || InvalidNameChars.Contains(c))
{
- throw new ArgumentException(Errors.InvalidName, nameof(name));
+ throw new SolutionArgumentException(Errors.InvalidName, nameof(name), SolutionErrorType.InvalidName);
}
}
if (IsDosWord(name))
{
- throw new ArgumentException(Errors.InvalidName, nameof(name));
+ throw new SolutionArgumentException(Errors.InvalidName, nameof(name), SolutionErrorType.InvalidName);
}
static bool IsDosWord(scoped StringSpan name)
@@ -491,7 +496,7 @@ internal SolutionProjectModel AddProject(string filePath, string projectTypeName
// Project is already in the solution.
if (this.FindProject(project.FilePath) is not null)
{
- throw new ArgumentException(string.Format(Errors.DuplicateProjectPath_Arg1, project.ItemRef), nameof(filePath));
+ throw new SolutionArgumentException(string.Format(Errors.DuplicateProjectPath_Arg1, project.ItemRef), nameof(filePath), SolutionErrorType.DuplicateProjectPath);
}
this.ValidateProjectName(project);
@@ -602,7 +607,7 @@ internal void ValidateProjectName(SolutionProjectModel project)
if (existingProject.ActualDisplayName.Equals(displayName, StringComparison.OrdinalIgnoreCase))
{
- throw new ArgumentException(string.Format(Errors.DuplicateProjectName_Arg1, displayName));
+ throw new SolutionArgumentException(string.Format(Errors.DuplicateProjectName_Arg1, displayName), SolutionErrorType.DuplicateProjectName);
}
}
}
@@ -611,7 +616,7 @@ internal void ValidateInModel(SolutionItemModel? item)
{
if (item is not null && item.Solution != this)
{
- throw new ArgumentException(Errors.InvalidModelItem, nameof(item));
+ throw new SolutionArgumentException(Errors.InvalidModelItem, nameof(item), SolutionErrorType.InvalidModelItem);
}
}
@@ -629,8 +634,6 @@ private SolutionFolderModel AddFolder(StringSpan name, string? parentItemRef)
this.solutionFolders.Add(folder);
this.solutionItems.Add(folder);
- // Ensure the project type is in the project type table, if it is not already.
- this.solutionItemsById[folder.Id] = folder;
return folder;
}
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionProjectModel.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionProjectModel.cs
index 9a524261..9ca0f7db 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionProjectModel.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionProjectModel.cs
@@ -99,7 +99,7 @@ public string FilePath
{
if (this.Solution.FindProject(value) is not null)
{
- throw new ArgumentException(string.Format(Errors.DuplicateItemRef_Args2, value, "Project"), nameof(value));
+ throw new SolutionArgumentException(string.Format(Errors.DuplicateItemRef_Args2, value, "Project"), nameof(value), SolutionErrorType.DuplicateItemRef);
}
string oldPath = this.filePath!;
@@ -211,7 +211,7 @@ public void AddDependency(SolutionProjectModel dependency)
if (ReferenceEquals(dependency, this))
{
- throw new ArgumentException(string.Format(Errors.InvalidLoop_Args1, dependency.ItemRef), nameof(dependency));
+ throw new SolutionArgumentException(string.Format(Errors.InvalidLoop_Args1, dependency.ItemRef), nameof(dependency), SolutionErrorType.InvalidLoop);
}
this.dependencies ??= [];
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/PublicAPI/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.SolutionPersistence/PublicAPI/PublicAPI.Unshipped.txt
index 815c9200..d34f9569 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/PublicAPI/PublicAPI.Unshipped.txt
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/PublicAPI/PublicAPI.Unshipped.txt
@@ -1 +1,37 @@
-#nullable enable
\ No newline at end of file
+#nullable enable
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionArgumentException
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionArgumentException.SolutionArgumentException(string? message, Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType type) -> void
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionArgumentException.SolutionArgumentException(string? message, string? paramName, Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType type) -> void
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionArgumentException.SolutionArgumentException(string? message, string? paramName, System.Exception? innerException, Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType type) -> void
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionArgumentException.SolutionArgumentException(string? message, System.Exception? innerException, Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType type) -> void
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionArgumentException.Type.get -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionArgumentException.Type.init -> void
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.CannotMoveFolderToChildFolder = 1 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.DuplicateDefaultProjectType = 2 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.DuplicateExtension = 3 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.DuplicateItemRef = 4 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.DuplicateName = 5 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.DuplicateProjectName = 6 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.DuplicateProjectPath = 7 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.DuplicateProjectTypeId = 8 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.InvalidConfiguration = 9 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.InvalidEncoding = 10 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.InvalidFolderPath = 11 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.InvalidFolderReference = 12 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.InvalidItemRef = 13 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.InvalidLoop = 14 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.InvalidModelItem = 15 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.InvalidName = 16 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.InvalidProjectReference = 17 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.InvalidProjectTypeReference = 18 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.InvalidVersion = 19 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.InvalidXmlDecoratorElementName = 23 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.MissingProjectValue = 20 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.NotSolution = 21 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.Undefined = 0 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.UnsupportedVersion = 22 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionException.ErrorType.get -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType?
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionException.ErrorType.init -> void
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionException.SolutionException(string! message, Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType errorType) -> void
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionException.SolutionException(string! message, System.Exception! inner, Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType errorType) -> void
\ No newline at end of file
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/SlnV12/SlnFileV12Serializer.Reader.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/SlnV12/SlnFileV12Serializer.Reader.cs
index 86371121..e1df8ac1 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/SlnV12/SlnFileV12Serializer.Reader.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/SlnV12/SlnFileV12Serializer.Reader.cs
@@ -63,7 +63,7 @@ internal ValueTask ParseAsync(ISolutionSerializer serializer, str
this.lineNumber = 0;
if (!this.TryParseFormatLine())
{
- throw new SolutionException(Errors.NotSolution) { File = fullPath, Line = this.lineNumber };
+ throw new SolutionException(Errors.NotSolution, SolutionErrorType.NotSolution) { File = fullPath, Line = this.lineNumber };
}
// Some property bags need to be loaded after all projects have been resolved.
@@ -213,7 +213,7 @@ internal ValueTask ParseAsync(ISolutionSerializer serializer, str
}
catch (Exception ex) when (SolutionException.ShouldWrap(ex))
{
- throw new SolutionException(ex.Message, ex) { File = fullPath, Line = this.lineNumber };
+ throw new SolutionException(ex.Message, ex, SolutionErrorType.Undefined) { File = fullPath, Line = this.lineNumber };
}
return new ValueTask(solutionModel);
@@ -459,7 +459,7 @@ private bool TryParseFormatLine()
if (string.IsNullOrEmpty(fileVersionMaj) || !int.TryParse(fileVersionMaj, out int fileVer) || fileVer > CurrentFileVersion)
{
- throw new SolutionException(string.Format(Errors.UnsupportedVersion_Args1, fileVersionMaj)) { File = fullPath, Line = this.lineNumber };
+ throw new SolutionException(string.Format(Errors.UnsupportedVersion_Args1, fileVersionMaj), SolutionErrorType.UnsupportedVersion) { File = fullPath, Line = this.lineNumber };
}
return true;
@@ -564,7 +564,7 @@ private readonly void SolutionAssert([DoesNotReturnIf(false)] bool condition, st
return;
}
- throw new SolutionException(message) { File = fullPath, Line = this.lineNumber };
+ throw new SolutionException(message, SolutionErrorType.Undefined) { File = fullPath, Line = this.lineNumber };
}
}
}
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/SlnV12/SlnFileV12Serializer.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/SlnV12/SlnFileV12Serializer.cs
index ec2a49c3..da249536 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/SlnV12/SlnFileV12Serializer.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/SlnV12/SlnFileV12Serializer.cs
@@ -46,7 +46,7 @@ public override ISerializerModelExtension CreateModelExtension(SlnV12SerializerS
encoding.CodePage != Encoding.UTF8.CodePage &&
encoding.CodePage != Encoding.Unicode.CodePage)
{
- throw new ArgumentException(Errors.InvalidEncoding, nameof(settings));
+ throw new SolutionArgumentException(Errors.InvalidEncoding, nameof(settings), SolutionErrorType.InvalidEncoding);
}
// Make sure ASCII encoding always has exception fallback.
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/SlnV12/SlnV12Extensions.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/SlnV12/SlnV12Extensions.cs
index bb5b8823..055f2b5e 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/SlnV12/SlnV12Extensions.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/SlnV12/SlnV12Extensions.cs
@@ -265,8 +265,6 @@ static void SetProjectConfigurationPlatforms(SolutionModel solution, SolutionPro
{
ParseProjectConfigLine(solution, projectKey, projectValue);
}
-
- solution.DistillProjectConfigurations();
}
// Applies a .SLN configuration line to the current project configuration.
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/SlnXMLSerializer.Writer.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/SlnXMLSerializer.Writer.cs
index f34ab761..1207ab8e 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/SlnXMLSerializer.Writer.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/SlnXMLSerializer.Writer.cs
@@ -43,6 +43,8 @@ internal static async Task SaveAsync(
model.RemoveObsoleteProperties();
}
+ model.DistillProjectConfigurations();
+
// If this started as an XML document, merge the changes back into the original document.
SlnxFile root = modelExtension?.Root ?? CreateNewSlnFile(fullPath, xmlSerializerSettings, model.StringTable);
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/ItemRefList`1.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/ItemRefList`1.cs
index 68714648..36c75fea 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/ItemRefList`1.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/ItemRefList`1.cs
@@ -34,14 +34,14 @@ internal readonly void Add(T item)
// Missing Name attribute.
if (!item.IsValid() || item.ItemRef is null)
{
- throw SolutionException.Create(string.Format(Errors.InvalidItemRef_Args2, item.ItemRefAttribute, item.ElementName), item);
+ throw SolutionException.Create(string.Format(Errors.InvalidItemRef_Args2, item.ItemRefAttribute, item.ElementName), item, SolutionErrorType.InvalidItemRef);
}
else
{
if (!this.items.TryAdd(item.ItemRef, item))
{
// Duplicate Name attribute.
- throw SolutionException.Create(string.Format(Errors.DuplicateItemRef_Args2, item.ItemRef, item.ElementName), item);
+ throw SolutionException.Create(string.Format(Errors.DuplicateItemRef_Args2, item.ItemRef, item.ElementName), item, SolutionErrorType.DuplicateItemRef);
}
}
}
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/SlnxFile.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/SlnxFile.cs
index 3e362284..02a14020 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/SlnxFile.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/SlnxFile.cs
@@ -38,7 +38,7 @@ internal SlnxFile(
}
else
{
- throw new SolutionException(Errors.NotSolution) { File = this.FullPath };
+ throw new SolutionException(Errors.NotSolution, SolutionErrorType.NotSolution) { File = this.FullPath };
}
this.SerializationSettings = this.GetDefaultSerializationSettings(serializationSettings);
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlConfiguration.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlConfiguration.cs
index c2a7bfbf..8e4e5712 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlConfiguration.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlConfiguration.cs
@@ -48,13 +48,13 @@ internal string Project
if (string.IsNullOrEmpty(projectValue))
{
- throw SolutionException.Create(Errors.MissingProjectValue, this);
+ throw SolutionException.Create(Errors.MissingProjectValue, this, SolutionErrorType.MissingProjectValue);
}
if (!ModelHelper.TrySplitFullConfiguration(this.Root.StringTable, this.Solution, out string? solutionBuildType, out string? solutionPlatform) &&
!this.Solution.IsNullOrEmpty())
{
- throw SolutionException.Create(string.Format(Errors.InvalidConfiguration_Args1, this.Solution), this);
+ throw SolutionException.Create(string.Format(Errors.InvalidConfiguration_Args1, this.Solution), this, SolutionErrorType.InvalidConfiguration);
}
if (solutionBuildType is BuildTypeNames.All or null)
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlContainer.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlContainer.cs
index 3e078041..6de22d84 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlContainer.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlContainer.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.Xml;
+using Microsoft.VisualStudio.SolutionPersistence.Model;
namespace Microsoft.VisualStudio.SolutionPersistence.Serializer.Xml.XmlDecorators;
@@ -66,7 +67,7 @@ internal override void UpdateFromXml()
if (validateItemRef && !xmlDecorator.IsValid())
{
- throw new ArgumentException(string.Format(Errors.InvalidItemRef_Args2, itemRef, xmlDecorator.ElementName));
+ throw new SolutionArgumentException(string.Format(Errors.InvalidItemRef_Args2, itemRef, xmlDecorator.ElementName), SolutionErrorType.InvalidItemRef);
}
xmlDecorator.UpdateFromXml();
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlDecorator.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlDecorator.cs
index ab2a96a7..2ed5eb37 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlDecorator.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlDecorator.cs
@@ -3,6 +3,7 @@
using System.Diagnostics;
using System.Xml;
+using Microsoft.VisualStudio.SolutionPersistence.Model;
namespace Microsoft.VisualStudio.SolutionPersistence.Serializer.Xml.XmlDecorators;
@@ -24,7 +25,7 @@ private protected XmlDecorator(SlnxFile root, XmlElement element, Keyword elemen
this.ElementName = elementName;
if (this.ElementName != Keywords.ToKeyword(element.Name))
{
- throw new ArgumentException($"Expected element name {this.ElementName}, but got {element.Name}");
+ throw new SolutionArgumentException($"Expected element name {this.ElementName}, but got {element.Name}", SolutionErrorType.InvalidXmlDecoratorElementName);
}
}
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlProject.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlProject.cs
index 1aeb7639..abf40a74 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlProject.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlProject.cs
@@ -101,7 +101,7 @@ internal SolutionProjectModel AddToModel(SolutionModel solution)
}
else
{
- throw SolutionException.Create(string.Format(Errors.InvalidFolderReference_Args1, this.ParentFolder.Name), this);
+ throw SolutionException.Create(string.Format(Errors.InvalidFolderReference_Args1, this.ParentFolder.Name), this, SolutionErrorType.InvalidFolderReference);
}
}
@@ -152,7 +152,7 @@ internal void AddDependenciesToModel(SolutionModel solution, SolutionProjectMode
}
else
{
- throw SolutionException.Create(string.Format(Errors.InvalidProjectReference_Args1, dependencyItemRef), buildDependency);
+ throw SolutionException.Create(string.Format(Errors.InvalidProjectReference_Args1, dependencyItemRef), buildDependency, SolutionErrorType.InvalidProjectReference);
}
}
}
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlSolution.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlSolution.cs
index 275a5947..cb998ddc 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlSolution.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlSolution.cs
@@ -96,12 +96,12 @@ internal SolutionModel ToModel()
}
catch (Exception ex) when (SolutionException.ShouldWrap(ex))
{
- throw SolutionException.Create(ex, this, string.Format(Errors.InvalidVersion_Args1, fileVersion));
+ throw SolutionException.Create(ex, this, string.Format(Errors.InvalidVersion_Args1, fileVersion), SolutionErrorType.InvalidVersion);
}
if (this.Root.FileVersion.Major > SlnxFile.CurrentVersion)
{
- throw SolutionException.Create(string.Format(Errors.UnsupportedVersion_Args1, fileVersion), this);
+ throw SolutionException.Create(string.Format(Errors.UnsupportedVersion_Args1, fileVersion), this, SolutionErrorType.UnsupportedVersion);
}
}
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Utilities/ListBuilderStruct`1.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Utilities/ListBuilderStruct`1.cs
index 51c390a7..a08a65fb 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Utilities/ListBuilderStruct`1.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Utilities/ListBuilderStruct`1.cs
@@ -111,7 +111,7 @@ internal readonly T[] ToArray()
2 => [this.item0, this.item1],
3 => [this.item0, this.item1, this.item2],
4 => [this.item0, this.item1, this.item2, this.item3],
- _ => [this.item0, this.item1, this.item2, this.item3, .. this.items],
+ _ => [this.item0, this.item1, this.item2, this.item3, .. this.items!],
};
}
diff --git a/src/OptProf.targets b/src/OptProf.targets
new file mode 100644
index 00000000..d0167d7c
--- /dev/null
+++ b/src/OptProf.targets
@@ -0,0 +1,17 @@
+
+
+
+ IBC
+ Common7\IDE\PrivateAssemblies\$(TargetFileName)
+ /ExeConfig:"%VisualStudio.InstallationUnderTest.Path%\Common7\IDE\vsn.exe"
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Microsoft.VisualStudio.SolutionPersistence.Tests.csproj b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Microsoft.VisualStudio.SolutionPersistence.Tests.csproj
index 8da1c8cc..91d2e70f 100644
--- a/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Microsoft.VisualStudio.SolutionPersistence.Tests.csproj
+++ b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Microsoft.VisualStudio.SolutionPersistence.Tests.csproj
@@ -1,7 +1,8 @@
- net8.0;net472
+ net8.0
+ $(TargetFrameworks);net472
diff --git a/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/Folders.cs b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/Folders.cs
index 9d540a44..810af520 100644
--- a/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/Folders.cs
+++ b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/Folders.cs
@@ -137,7 +137,7 @@ public void MoveProjectToFolder()
Assert.Equal("/This/Is/A/Nested/Folder/", wanderingProject.Parent.ItemRef);
// Try moving project to folder with existing project
- ArgumentException ex = Assert.Throws(() => wanderingProject.MoveToFolder(folderA));
+ ArgumentException ex = Assert.Throws(() => wanderingProject.MoveToFolder(folderA));
Assert.Equal(string.Format(Errors.DuplicateProjectName_Arg1, wanderingProject.ActualDisplayName), ex.Message);
Assert.Equal("/This/Is/A/Nested/Folder/", wanderingProject.Parent.ItemRef);
@@ -172,7 +172,7 @@ public void MoveFolder()
Assert.Equal("/This/Is/A/", folderFolder.Parent.ItemRef);
// Try to move folder under itself.
- ArgumentException ex = Assert.Throws(() => folderThis.MoveToFolder(folderNested));
+ ArgumentException ex = Assert.Throws(() => folderThis.MoveToFolder(folderNested));
Assert.StartsWith(Errors.CannotMoveFolderToChildFolder, ex.Message);
}
@@ -192,7 +192,7 @@ public void ChangeFolderName()
// Try case exact
{
- ArgumentException ex = Assert.Throws(() => folderB.Name = "A");
+ ArgumentException ex = Assert.Throws(() => folderB.Name = "A");
Assert.StartsWith(string.Format(Errors.DuplicateItemRef_Args2, "/A/", "Folder"), ex.Message);
Assert.Equal("/B/Nested/Deep/", folderNestedB.Path);
@@ -200,7 +200,7 @@ public void ChangeFolderName()
// Try case insensitive
{
- ArgumentException ex = Assert.Throws(() => folderB.Name = "a");
+ ArgumentException ex = Assert.Throws(() => folderB.Name = "a");
Assert.StartsWith(string.Format(Errors.DuplicateItemRef_Args2, "/a/", "Folder"), ex.Message);
Assert.Equal("/B/Nested/Deep/", folderNestedB.Path);
diff --git a/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/ManipulateXmlKitchenSink.cs b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/ManipulateXmlKitchenSink.cs
index 349717c3..b3fdd143 100644
--- a/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/ManipulateXmlKitchenSink.cs
+++ b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/ManipulateXmlKitchenSink.cs
@@ -41,7 +41,7 @@ static void CreateModifiedModel(SolutionModel solution)
SolutionProjectModel? project = solution.FindProject(Path.Join("other", "Project4.nativeproj"));
Assert.NotNull(project);
- project.AddProjectConfigurationRule(new ConfigurationRule(BuildDimension.Platform, "*", "Z80", "Z80"));
+ project.AddProjectConfigurationRule(new ConfigurationRule(BuildDimension.Platform, "*", "Arm64", "Z80"));
SolutionProjectModel? project3 = solution.FindProject("Project3.csproj");
Assert.NotNull(project3);
diff --git a/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/Project.cs b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/Project.cs
index 4b38af67..d63dc4ee 100644
--- a/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/Project.cs
+++ b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/Project.cs
@@ -28,26 +28,26 @@ public void AddProject()
// Verify error if same project is added again.
{
- Exception ex = Assert.Throws(() => solution.AddProject(projectPath, projectTypeName: null));
+ Exception ex = Assert.Throws(() => solution.AddProject(projectPath, projectTypeName: null));
Assert.StartsWith(string.Format(Errors.DuplicateItemRef_Args2, projectPath, "Project"), ex.Message);
}
// Verify error if same project is added to folder.
{
- Exception ex = Assert.Throws(() => solution.AddProject(projectPath, projectTypeName: null, folder: folder));
+ Exception ex = Assert.Throws(() => solution.AddProject(projectPath, projectTypeName: null, folder: folder));
Assert.StartsWith(string.Format(Errors.DuplicateItemRef_Args2, projectPath, "Project"), ex.Message);
}
// Verify error if same project is added with different case..
{
string projectPathUpper = projectPath.ToUpperInvariant();
- Exception ex = Assert.Throws(() => solution.AddProject(projectPathUpper, projectTypeName: null));
+ Exception ex = Assert.Throws(() => solution.AddProject(projectPathUpper, projectTypeName: null));
Assert.StartsWith(string.Format(Errors.DuplicateItemRef_Args2, projectPathUpper, "Project"), ex.Message);
}
// Try chaging a path to an existing project.
{
- Exception ex = Assert.Throws(() => anotherProject.FilePath = project.FilePath);
+ Exception ex = Assert.Throws(() => anotherProject.FilePath = project.FilePath);
Assert.StartsWith(string.Format(Errors.DuplicateItemRef_Args2, projectPath, "Project"), ex.Message);
}
}
diff --git a/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/RoundTripClassicSln.cs b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/RoundTripClassicSln.cs
index e4e7efcb..5e6c80f4 100644
--- a/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/RoundTripClassicSln.cs
+++ b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/RoundTripClassicSln.cs
@@ -39,6 +39,9 @@ public class RoundTripClassicSln
[Fact]
public Task MissingConfigurationsAsync() => TestRoundTripSerializerAsync(SlnAssets.ClassicSlnMissingConfigurations);
+ [Fact]
+ public Task FolderIdAsync() => TestRoundTripSerializerAsync(SlnAssets.LoadResource("FolderId.sln"));
+
[Theory]
[MemberData(nameof(ClassicSlnFiles))]
public Task AllClassicSolutionAsync(ResourceName sampleFile)
diff --git a/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/Validation.cs b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/Validation.cs
index c240433b..9e260333 100644
--- a/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/Validation.cs
+++ b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/Validation.cs
@@ -88,11 +88,8 @@ public void ConfigurationName()
">",
"?",
"*",
- "%",
":",
"|",
- "&",
- "%",
"con",
"com1",
"lpt9",
@@ -170,7 +167,7 @@ public void SolutionFolders()
string invalidNameError = Errors.InvalidName;
// Don't allow invalid characters
- Assert.StartsWith(invalidNameError, Assert.ThrowsAny(() => solution.AddFolder("/Foo#/")).Message);
+ Assert.StartsWith(invalidNameError, Assert.ThrowsAny(() => solution.AddFolder("/Foo")).Message);
// Don't allow reserved names
Assert.StartsWith(invalidNameError, Assert.ThrowsAny(() => solution.AddFolder("/LPT4/")).Message);
diff --git a/test/Microsoft.VisualStudio.SolutionPersistence.Tests/SlnAssets/FolderId.sln.txt b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/SlnAssets/FolderId.sln.txt
new file mode 100644
index 00000000..6b973cf6
--- /dev/null
+++ b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/SlnAssets/FolderId.sln.txt
@@ -0,0 +1,19 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Foo", "Foo", "{A340195B-735B-34A1-F5D5-445C9F08470D}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Foo", "Foo", "{F508AC4D-2574-0F7F-5175-5C248303655F}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Foo", "Foo", "{45DA1D9E-8A3F-8D58-BB91-EFD6A5B29A17}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Foo", "Foo", "{6647119C-3B2A-3090-A1B3-560120CC3977}"
+EndProject
+Global
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {F508AC4D-2574-0F7F-5175-5C248303655F} = {A340195B-735B-34A1-F5D5-445C9F08470D}
+ {45DA1D9E-8A3F-8D58-BB91-EFD6A5B29A17} = {F508AC4D-2574-0F7F-5175-5C248303655F}
+ {6647119C-3B2A-3090-A1B3-560120CC3977} = {45DA1D9E-8A3F-8D58-BB91-EFD6A5B29A17}
+ EndGlobalSection
+EndGlobal
diff --git a/test/Microsoft.VisualStudio.SolutionPersistence.Tests/SlnAssets/FolderId.slnx.xml b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/SlnAssets/FolderId.slnx.xml
new file mode 100644
index 00000000..5b1136f5
--- /dev/null
+++ b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/SlnAssets/FolderId.slnx.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/test/Microsoft.VisualStudio.SolutionPersistence.Tests/SlnAssets/SlnxWhitespace/KitchenSink-AddConfigurations.slnx.xml b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/SlnAssets/SlnxWhitespace/KitchenSink-AddConfigurations.slnx.xml
index d27565f4..5c6d6a3a 100644
--- a/test/Microsoft.VisualStudio.SolutionPersistence.Tests/SlnAssets/SlnxWhitespace/KitchenSink-AddConfigurations.slnx.xml
+++ b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/SlnAssets/SlnxWhitespace/KitchenSink-AddConfigurations.slnx.xml
@@ -52,7 +52,7 @@
-
+
diff --git a/tools/MergeFrom-Template.ps1 b/tools/MergeFrom-Template.ps1
index c0d13dda..3f721c6a 100644
--- a/tools/MergeFrom-Template.ps1
+++ b/tools/MergeFrom-Template.ps1
@@ -43,7 +43,7 @@ if ($LASTEXITCODE -ne 0) {
$LibTemplateUrl = 'https://github.com/aarnott/Library.Template'
Spawn-Tool 'git' ('fetch', $LibTemplateUrl, $remoteBranch)
-$SourceCommit = git rev-parse FETCH_HEAD
+$SourceCommit = Spawn-Tool 'git' ('rev-parse', 'FETCH_HEAD')
$BaseBranch = Spawn-Tool 'git' ('branch', '--show-current')
$SourceCommitUrl = "$LibTemplateUrl/commit/$SourceCommit"