diff --git a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs index 11107ceac1..67aaf6309e 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs @@ -2027,12 +2027,75 @@ namespace Microsoft.PowerShell.Commands Array.Clear(tmpNestedModules, 0, tmpNestedModules.Length); } - // Set the private data member for the module if the manifest contains - // this member - object privateData = null; - if (data.Contains("PrivateData")) + // Set the private data member for the module if the manifest contains this member + object privateData = data["PrivateData"]; + + // Validate the 'ExperimentalFeatures' member of the manifest + List expFeatureList = null; + if (privateData is Hashtable hashData && hashData["PSData"] is Hashtable psData) { - privateData = data["PrivateData"]; + if (!GetScalarFromData(psData, moduleManifestPath, "ExperimentalFeatures", manifestProcessingFlags, out Hashtable[] features)) + { + containedErrors = true; + if (bailOnFirstError) return null; + } + + if (features != null && features.Length > 0) + { + bool nameMissingOrEmpty = false; + var invalidNames = new List(); + string moduleName = ModuleIntrinsics.GetModuleName(moduleManifestPath); + expFeatureList = new List(features.Length); + + foreach (var feature in features) + { + string featureName = feature["Name"] as string; + if (String.IsNullOrEmpty(featureName)) + { + nameMissingOrEmpty = true; + continue; + } + + if (ExperimentalFeature.IsModuleFeatureName(featureName, moduleName)) + { + string featureDescription = feature["Description"] as string; + expFeatureList.Add(new ExperimentalFeature(featureName, featureDescription, moduleName, + ExperimentalFeature.HasEnabled(featureName))); + } + else + { + invalidNames.Add(featureName); + } + } + + if (nameMissingOrEmpty) + { + if (writingErrors) + { + WriteError(new ErrorRecord(new ArgumentException(Modules.ExperimentalFeatureNameMissingOrEmpty), + "Modules_ExperimentalFeatureNameMissingOrEmpty", + ErrorCategory.InvalidData, null)); + } + + containedErrors = true; + if (bailOnFirstError) { return null; } + } + + if (invalidNames.Count > 0) + { + if (writingErrors) + { + string invalidNameStr = String.Join(',', invalidNames); + string errorMsg = StringUtil.Format(Modules.InvalidExperimentalFeatureName, invalidNameStr); + WriteError(new ErrorRecord(new ArgumentException(errorMsg), + "Modules_InvalidExperimentalFeatureName", + ErrorCategory.InvalidData, null)); + } + + containedErrors = true; + if (bailOnFirstError) { return null; } + } + } } // Process all of the exports... @@ -2413,6 +2476,12 @@ namespace Microsoft.PowerShell.Commands manifestInfo.PowerShellVersion = powerShellVersion; manifestInfo.ProcessorArchitecture = requiredProcessorArchitecture; manifestInfo.Prefix = resolvedCommandPrefix; + + if (expFeatureList != null) + { + manifestInfo.ExperimentalFeatures = new ReadOnlyCollection(expFeatureList); + } + if (assemblyList != null) { foreach (var a in assemblyList) @@ -3009,6 +3078,7 @@ namespace Microsoft.PowerShell.Commands newManifestInfo.LicenseUri = manifestInfo.LicenseUri; newManifestInfo.IconUri = manifestInfo.IconUri; newManifestInfo.RepositorySourceLocation = manifestInfo.RepositorySourceLocation; + newManifestInfo.ExperimentalFeatures = manifestInfo.ExperimentalFeatures; // If we are in module discovery, then fix the path. if (ss == null) diff --git a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs index d0e13d9a6a..3bf5c0d9a0 100644 --- a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs +++ b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs @@ -301,49 +301,37 @@ namespace System.Management.Automation ProjectUri = null; IconUri = null; - var privateDataHashTable = _privateData as Hashtable; - if (privateDataHashTable != null) + if (_privateData is Hashtable hashData && hashData["PSData"] is Hashtable psData) { - var psData = privateDataHashTable["PSData"] as Hashtable; - if (psData != null) + var tagsValue = psData["Tags"]; + if (tagsValue is object[] tags && tags.Length > 0) { - object tagsValue = psData["Tags"]; - if (tagsValue != null) + foreach (var tagString in tags.OfType()) { - var tags = tagsValue as object[]; - if (tags != null && tags.Any()) - { - foreach (var tagString in tags.OfType()) - { - AddToTags(tagString); - } - } - else - { - AddToTags(tagsValue.ToString()); - } + AddToTags(tagString); } - - var licenseUri = psData["LicenseUri"] as string; - if (licenseUri != null) - { - LicenseUri = GetUriFromString(licenseUri); - } - - var projectUri = psData["ProjectUri"] as string; - if (projectUri != null) - { - ProjectUri = GetUriFromString(projectUri); - } - - var iconUri = psData["IconUri"] as string; - if (iconUri != null) - { - IconUri = GetUriFromString(iconUri); - } - - ReleaseNotes = psData["ReleaseNotes"] as string; } + else if (tagsValue is string tag) + { + AddToTags(tag); + } + + if (psData["LicenseUri"] is string licenseUri) + { + LicenseUri = GetUriFromString(licenseUri); + } + + if (psData["ProjectUri"] is string projectUri) + { + ProjectUri = GetUriFromString(projectUri); + } + + if (psData["IconUri"] is string iconUri) + { + IconUri = GetUriFromString(iconUri); + } + + ReleaseNotes = psData["ReleaseNotes"] as string; } } @@ -360,6 +348,11 @@ namespace System.Management.Automation return uri; } + /// + /// Get the experimental features declared in this module. + /// + public IEnumerable ExperimentalFeatures { get; internal set; } = Utils.EmptyReadOnlyCollection(); + /// /// Tags of this module. /// diff --git a/src/System.Management.Automation/resources/Modules.resx b/src/System.Management.Automation/resources/Modules.resx index af889e1556..11bf9e4776 100644 --- a/src/System.Management.Automation/resources/Modules.resx +++ b/src/System.Management.Automation/resources/Modules.resx @@ -606,4 +606,10 @@ This prerequisite is valid for the PowerShell Desktop edition only. + + A non-empty string value should be specified for an experimental feature declared in the module manifest. + + + One or more invalid experimental feature names found: {0}. A module experimental feature name should follow this convention: 'ModuleName.FeatureName'. +