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(() => 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"