Update the ICommandPredictor to provide more feedback and also make feedback easier to be corelated (#14649)

This commit is contained in:
Dongbo Wang
2021-02-04 15:34:17 -08:00
committed by GitHub
parent 61fea6c342
commit e803d3bc8d
6 changed files with 215 additions and 62 deletions
@@ -27,15 +27,22 @@ namespace System.Management.Automation.Subsystem
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the mini-session id that represents a specific invocation to the <see cref="ICommandPredictor.GetSuggestion"/> API of the predictor.
/// When it's not specified, it's considered by a client that the predictor doesn't expect feedback.
/// </summary>
public uint? Session { get; }
/// <summary>
/// Gets the suggestions.
/// </summary>
public IReadOnlyList<PredictiveSuggestion> Suggestions { get; }
internal PredictionResult(Guid id, string name, List<PredictiveSuggestion> suggestions)
internal PredictionResult(Guid id, string name, uint? session, List<PredictiveSuggestion> suggestions)
{
Id = id;
Name = name;
Session = session;
Suggestions = suggestions;
}
}
@@ -48,22 +55,24 @@ namespace System.Management.Automation.Subsystem
/// <summary>
/// Collect the predictive suggestions from registered predictors using the default timeout.
/// </summary>
/// <param name="client">Represents the client that initiates the call.</param>
/// <param name="ast">The <see cref="Ast"/> object from parsing the current command line input.</param>
/// <param name="astTokens">The <see cref="Token"/> objects from parsing the current command line input.</param>
/// <returns>A list of <see cref="PredictionResult"/> objects.</returns>
public static Task<List<PredictionResult>?> PredictInput(Ast ast, Token[] astTokens)
public static Task<List<PredictionResult>?> PredictInput(string client, Ast ast, Token[] astTokens)
{
return PredictInput(ast, astTokens, millisecondsTimeout: 20);
return PredictInput(client, ast, astTokens, millisecondsTimeout: 20);
}
/// <summary>
/// Collect the predictive suggestions from registered predictors using the specified timeout.
/// </summary>
/// <param name="client">Represents the client that initiates the call.</param>
/// <param name="ast">The <see cref="Ast"/> object from parsing the current command line input.</param>
/// <param name="astTokens">The <see cref="Token"/> objects from parsing the current command line input.</param>
/// <param name="millisecondsTimeout">The milliseconds to timeout.</param>
/// <returns>A list of <see cref="PredictionResult"/> objects.</returns>
public static async Task<List<PredictionResult>?> PredictInput(Ast ast, Token[] astTokens, int millisecondsTimeout)
public static async Task<List<PredictionResult>?> PredictInput(string client, Ast ast, Token[] astTokens, int millisecondsTimeout)
{
Requires.Condition(millisecondsTimeout > 0, nameof(millisecondsTimeout));
@@ -85,8 +94,8 @@ namespace System.Management.Automation.Subsystem
state =>
{
var predictor = (ICommandPredictor)state!;
List<PredictiveSuggestion>? texts = predictor.GetSuggestion(context, cancellationSource.Token);
return texts?.Count > 0 ? new PredictionResult(predictor.Id, predictor.Name, texts) : null;
SuggestionPackage pkg = predictor.GetSuggestion(client, context, cancellationSource.Token);
return pkg.SuggestionEntries?.Count > 0 ? new PredictionResult(predictor.Id, predictor.Name, pkg.Session, pkg.SuggestionEntries) : null;
},
predictor,
cancellationSource.Token,
@@ -99,7 +108,7 @@ namespace System.Management.Automation.Subsystem
Task.Delay(millisecondsTimeout, cancellationSource.Token)).ConfigureAwait(false);
cancellationSource.Cancel();
var results = new List<PredictionResult>(predictors.Count);
var resultList = new List<PredictionResult>(predictors.Count);
foreach (Task<PredictionResult?> task in tasks)
{
if (task.IsCompletedSuccessfully)
@@ -107,19 +116,20 @@ namespace System.Management.Automation.Subsystem
PredictionResult? result = task.Result;
if (result != null)
{
results.Add(result);
resultList.Add(result);
}
}
}
return results;
return resultList;
}
/// <summary>
/// Allow registered predictors to do early processing when a command line is accepted.
/// </summary>
/// <param name="client">Represents the client that initiates the call.</param>
/// <param name="history">History command lines provided as references for prediction.</param>
public static void OnCommandLineAccepted(IReadOnlyList<string> history)
public static void OnCommandLineAccepted(string client, IReadOnlyList<string> history)
{
Requires.NotNull(history, nameof(history));
@@ -134,7 +144,7 @@ namespace System.Management.Automation.Subsystem
if (predictor.SupportEarlyProcessing)
{
ThreadPool.QueueUserWorkItem<ICommandPredictor>(
state => state.StartEarlyProcessing(history),
state => state.StartEarlyProcessing(client, history),
predictor,
preferLocal: false);
}
@@ -142,11 +152,43 @@ namespace System.Management.Automation.Subsystem
}
/// <summary>
/// Send feedback to predictors about their last suggestions.
/// Send feedback to a predictor when one or more suggestions from it were displayed to the user.
/// </summary>
/// <param name="client">Represents the client that initiates the call.</param>
/// <param name="predictorId">The identifier of the predictor whose prediction result was accepted.</param>
/// <param name="session">The mini-session where the displayed suggestions came from.</param>
/// <param name="countOrIndex">
/// When the value is greater than 0, it's the number of displayed suggestions from the list returned in <paramref name="session"/>, starting from the index 0.
/// When the value is less than or equal to 0, it means a single suggestion from the list got displayed, and the index is the absolute value.
/// </param>
public static void OnSuggestionDisplayed(string client, Guid predictorId, uint session, int countOrIndex)
{
var predictors = SubsystemManager.GetSubsystems<ICommandPredictor>();
if (predictors.Count == 0)
{
return;
}
foreach (ICommandPredictor predictor in predictors)
{
if (predictor.AcceptFeedback && predictor.Id == predictorId)
{
ThreadPool.QueueUserWorkItem<ICommandPredictor>(
state => state.OnSuggestionDisplayed(client, session, countOrIndex),
predictor,
preferLocal: false);
}
}
}
/// <summary>
/// Send feedback to a predictor when a suggestion from it was accepted.
/// </summary>
/// <param name="client">Represents the client that initiates the call.</param>
/// <param name="predictorId">The identifier of the predictor whose prediction result was accepted.</param>
/// <param name="session">The mini-session where the accepted suggestion came from.</param>
/// <param name="suggestionText">The accepted suggestion text.</param>
public static void OnSuggestionAccepted(Guid predictorId, string suggestionText)
public static void OnSuggestionAccepted(string client, Guid predictorId, uint session, string suggestionText)
{
Requires.NotNullOrEmpty(suggestionText, nameof(suggestionText));
@@ -161,7 +203,7 @@ namespace System.Management.Automation.Subsystem
if (predictor.AcceptFeedback && predictor.Id == predictorId)
{
ThreadPool.QueueUserWorkItem<ICommandPredictor>(
state => state.OnSuggestionAccepted(suggestionText),
state => state.OnSuggestionAccepted(client, session, suggestionText),
predictor,
preferLocal: false);
}
@@ -40,22 +40,37 @@ namespace System.Management.Automation.Subsystem
/// A command line was accepted to execute.
/// The predictor can start processing early as needed with the latest history.
/// </summary>
/// <param name="clientId">Represents the client that initiates the call.</param>
/// <param name="history">History command lines provided as references for prediction.</param>
void StartEarlyProcessing(IReadOnlyList<string> history);
void StartEarlyProcessing(string clientId, IReadOnlyList<string> history);
/// <summary>
/// The suggestion given by the predictor was accepted.
/// </summary>
/// <param name="acceptedSuggestion">The accepted suggestion text.</param>
void OnSuggestionAccepted(string acceptedSuggestion);
/// <summary>
/// Get the predictive suggestions.
/// Get the predictive suggestions. It indicates the start of a suggestion rendering session.
/// </summary>
/// <param name="clientId">Represents the client that initiates the call.</param>
/// <param name="context">The <see cref="PredictionContext"/> object to be used for prediction.</param>
/// <param name="cancellationToken">The cancellation token to cancel the prediction.</param>
/// <returns>A list of predictive suggestions.</returns>
List<PredictiveSuggestion>? GetSuggestion(PredictionContext context, CancellationToken cancellationToken);
/// <returns>An instance of <see cref="SuggestionPackage"/>.</returns>
SuggestionPackage GetSuggestion(string clientId, PredictionContext context, CancellationToken cancellationToken);
/// <summary>
/// One or more suggestions provided by the predictor were displayed to the user.
/// </summary>
/// <param name="clientId">Represents the client that initiates the call.</param>
/// <param name="session">The mini-session where the displayed suggestions came from.</param>
/// <param name="countOrIndex">
/// When the value is greater than 0, it's the number of displayed suggestions from the list returned in <paramref name="session"/>, starting from the index 0.
/// When the value is less than or equal to 0, it means a single suggestion from the list got displayed, and the index is the absolute value.
/// </param>
void OnSuggestionDisplayed(string clientId, uint session, int countOrIndex);
/// <summary>
/// The suggestion provided by the predictor was accepted.
/// </summary>
/// <param name="clientId">Represents the client that initiates the call.</param>
/// <param name="session">Represents the mini-session where the accepted suggestion came from.</param>
/// <param name="acceptedSuggestion">The accepted suggestion text.</param>
void OnSuggestionAccepted(string clientId, uint session, string acceptedSuggestion);
}
/// <summary>
@@ -160,4 +175,47 @@ namespace System.Management.Automation.Subsystem
ToolTip = toolTip;
}
}
/// <summary>
/// A package returned from <see cref="ICommandPredictor.GetSuggestion"/>.
/// </summary>
public struct SuggestionPackage
{
/// <summary>
/// Gets the mini-session that represents a specific invocation to <see cref="ICommandPredictor.GetSuggestion"/>.
/// When it's not specified, it's considered by a client that the predictor doesn't expect feedback.
/// </summary>
public uint? Session { get; }
/// <summary>
/// Gets the suggestion entries returned from that mini-session.
/// </summary>
public List<PredictiveSuggestion>? SuggestionEntries { get; }
/// <summary>
/// Initializes a new instance of the <see cref="SuggestionPackage"/> struct without providing a session id.
/// Note that, when a session id is not specified, it's considered by a client that the predictor doesn't expect feedback.
/// </summary>
/// <param name="suggestionEntries">The suggestions to return.</param>
public SuggestionPackage(List<PredictiveSuggestion> suggestionEntries)
{
Requires.NotNullOrEmpty(suggestionEntries, nameof(suggestionEntries));
Session = null;
SuggestionEntries = suggestionEntries;
}
/// <summary>
/// Initializes a new instance of the <see cref="SuggestionPackage"/> struct with the mini-session id and the suggestions.
/// </summary>
/// <param name="session">The mini-session where suggestions came from.</param>
/// <param name="suggestionEntries">The suggestions to return.</param>
public SuggestionPackage(uint session, List<PredictiveSuggestion> suggestionEntries)
{
Requires.NotNullOrEmpty(suggestionEntries, nameof(suggestionEntries));
Session = session;
SuggestionEntries = suggestionEntries;
}
}
}
@@ -2312,6 +2312,14 @@ namespace System.Management.Automation.Internal
}
}
internal static void NotNullOrEmpty(ICollection value, string paramName)
{
if (value == null || value.Count == 0)
{
throw new ArgumentNullException(paramName);
}
}
internal static void Condition([DoesNotReturnIf(false)] bool precondition, string paramName)
{
if (!precondition)
+54 -27
View File
@@ -20,9 +20,9 @@ namespace PSTests.Sequential
public List<string> AcceptedSuggestions { get; }
public static readonly MyPredictor SlowPredictor;
public List<string> DisplayedSuggestions { get; }
public static readonly MyPredictor FastPredictor;
public static readonly MyPredictor SlowPredictor, FastPredictor;
static MyPredictor()
{
@@ -48,6 +48,7 @@ namespace PSTests.Sequential
History = new List<string>();
AcceptedSuggestions = new List<string>();
DisplayedSuggestions = new List<string>();
}
public Guid Id => _id;
@@ -60,17 +61,25 @@ namespace PSTests.Sequential
bool ICommandPredictor.AcceptFeedback => true;
public void StartEarlyProcessing(IReadOnlyList<string> history)
public void StartEarlyProcessing(string clientId, IReadOnlyList<string> history)
{
History.AddRange(history);
foreach (string item in history)
{
History.Add($"{clientId}-{item}");
}
}
public void OnSuggestionAccepted(string acceptedSuggestion)
public void OnSuggestionDisplayed(string clientId, uint session, int countOrIndex)
{
AcceptedSuggestions.Add(acceptedSuggestion);
DisplayedSuggestions.Add($"{clientId}-{session}-{countOrIndex}");
}
public List<PredictiveSuggestion> GetSuggestion(PredictionContext context, CancellationToken cancellationToken)
public void OnSuggestionAccepted(string clientId, uint session, string acceptedSuggestion)
{
AcceptedSuggestions.Add($"{clientId}-{session}-{acceptedSuggestion}");
}
public SuggestionPackage GetSuggestion(string clientId, PredictionContext context, CancellationToken cancellationToken)
{
if (_delay)
{
@@ -81,24 +90,31 @@ namespace PSTests.Sequential
// You can get the user input from the AST.
var userInput = context.InputAst.Extent.Text;
return new List<PredictiveSuggestion> {
new PredictiveSuggestion($"{userInput} TEST-1 from {Name}"),
new PredictiveSuggestion($"{userInput} TeSt-2 from {Name}"),
var entries = new List<PredictiveSuggestion>
{
new PredictiveSuggestion($"'{userInput}' from '{clientId}' - TEST-1 from {Name}"),
new PredictiveSuggestion($"'{userInput}' from '{clientId}' - TeSt-2 from {Name}"),
};
return new SuggestionPackage(56, entries);
}
}
public static class CommandPredictionTests
{
private const string Client = "PredictionTest";
private const uint Session = 56;
[Fact]
public static void PredictInput()
{
const string Input = "Hello world";
MyPredictor slow = MyPredictor.SlowPredictor;
MyPredictor fast = MyPredictor.FastPredictor;
Ast ast = Parser.ParseInput("Hello world", out Token[] tokens, out _);
Ast ast = Parser.ParseInput(Input, out Token[] tokens, out _);
// Returns null when no predictor implementation registered
List<PredictionResult> results = CommandPrediction.PredictInput(ast, tokens).Result;
List<PredictionResult> results = CommandPrediction.PredictInput(Client, ast, tokens).Result;
Assert.Null(results);
try
@@ -111,32 +127,35 @@ namespace PSTests.Sequential
// cannot finish before the specified timeout.
// The specified timeout is exaggerated to make the test reliable.
// xUnit must spin up a lot tasks, which makes the test unreliable when the time difference between 'delay' and 'timeout' is small.
results = CommandPrediction.PredictInput(ast, tokens, millisecondsTimeout: 1000).Result;
results = CommandPrediction.PredictInput(Client, ast, tokens, millisecondsTimeout: 1000).Result;
Assert.Single(results);
PredictionResult res = results[0];
Assert.Equal(fast.Id, res.Id);
Assert.Equal(Session, res.Session);
Assert.Equal(2, res.Suggestions.Count);
Assert.Equal($"Hello world TEST-1 from {fast.Name}", res.Suggestions[0].SuggestionText);
Assert.Equal($"Hello world TeSt-2 from {fast.Name}", res.Suggestions[1].SuggestionText);
Assert.Equal($"'{Input}' from '{Client}' - TEST-1 from {fast.Name}", res.Suggestions[0].SuggestionText);
Assert.Equal($"'{Input}' from '{Client}' - TeSt-2 from {fast.Name}", res.Suggestions[1].SuggestionText);
// Expect the results from both 'slow' and 'fast' predictors
// Same here -- the specified timeout is exaggerated to make the test reliable.
// xUnit must spin up a lot tasks, which makes the test unreliable when the time difference between 'delay' and 'timeout' is small.
results = CommandPrediction.PredictInput(ast, tokens, millisecondsTimeout: 4000).Result;
results = CommandPrediction.PredictInput(Client, ast, tokens, millisecondsTimeout: 4000).Result;
Assert.Equal(2, results.Count);
PredictionResult res1 = results[0];
Assert.Equal(slow.Id, res1.Id);
Assert.Equal(Session, res1.Session);
Assert.Equal(2, res1.Suggestions.Count);
Assert.Equal($"Hello world TEST-1 from {slow.Name}", res1.Suggestions[0].SuggestionText);
Assert.Equal($"Hello world TeSt-2 from {slow.Name}", res1.Suggestions[1].SuggestionText);
Assert.Equal($"'{Input}' from '{Client}' - TEST-1 from {slow.Name}", res1.Suggestions[0].SuggestionText);
Assert.Equal($"'{Input}' from '{Client}' - TeSt-2 from {slow.Name}", res1.Suggestions[1].SuggestionText);
PredictionResult res2 = results[1];
Assert.Equal(fast.Id, res2.Id);
Assert.Equal(Session, res2.Session);
Assert.Equal(2, res2.Suggestions.Count);
Assert.Equal($"Hello world TEST-1 from {fast.Name}", res2.Suggestions[0].SuggestionText);
Assert.Equal($"Hello world TeSt-2 from {fast.Name}", res2.Suggestions[1].SuggestionText);
Assert.Equal($"'{Input}' from '{Client}' - TEST-1 from {fast.Name}", res2.Suggestions[0].SuggestionText);
Assert.Equal($"'{Input}' from '{Client}' - TeSt-2 from {fast.Name}", res2.Suggestions[1].SuggestionText);
}
finally
{
@@ -160,8 +179,10 @@ namespace PSTests.Sequential
var history = new[] { "hello", "world" };
var ids = new HashSet<Guid> { slow.Id, fast.Id };
CommandPrediction.OnCommandLineAccepted(history);
CommandPrediction.OnSuggestionAccepted(slow.Id, "Yeah");
CommandPrediction.OnCommandLineAccepted(Client, history);
CommandPrediction.OnSuggestionDisplayed(Client, slow.Id, Session, 2);
CommandPrediction.OnSuggestionDisplayed(Client, fast.Id, Session, -1);
CommandPrediction.OnSuggestionAccepted(Client, slow.Id, Session, "Yeah");
// The calls to 'StartEarlyProcessing' and 'OnSuggestionAccepted' are queued in thread pool,
// so we wait a bit to make sure the calls are done.
@@ -171,15 +192,21 @@ namespace PSTests.Sequential
}
Assert.Equal(2, slow.History.Count);
Assert.Equal(history[0], slow.History[0]);
Assert.Equal(history[1], slow.History[1]);
Assert.Equal($"{Client}-{history[0]}", slow.History[0]);
Assert.Equal($"{Client}-{history[1]}", slow.History[1]);
Assert.Equal(2, fast.History.Count);
Assert.Equal(history[0], fast.History[0]);
Assert.Equal(history[1], fast.History[1]);
Assert.Equal($"{Client}-{history[0]}", fast.History[0]);
Assert.Equal($"{Client}-{history[1]}", fast.History[1]);
Assert.Single(slow.DisplayedSuggestions);
Assert.Equal($"{Client}-{Session}-2", slow.DisplayedSuggestions[0]);
Assert.Single(fast.DisplayedSuggestions);
Assert.Equal($"{Client}-{Session}--1", fast.DisplayedSuggestions[0]);
Assert.Single(slow.AcceptedSuggestions);
Assert.Equal("Yeah", slow.AcceptedSuggestions[0]);
Assert.Equal($"{Client}-{Session}-Yeah", slow.AcceptedSuggestions[0]);
Assert.Empty(fast.AcceptedSuggestions);
}
+6 -4
View File
@@ -95,10 +95,12 @@ namespace PSTests.Sequential
Assert.Null(impl.FunctionsToDefine);
Assert.Equal(SubsystemKind.CommandPredictor, impl.Kind);
var predCxt = PredictionContext.Create("Hello world");
var results = impl.GetSuggestion(predCxt, CancellationToken.None);
Assert.Equal($"Hello world TEST-1 from {impl.Name}", results[0].SuggestionText);
Assert.Equal($"Hello world TeSt-2 from {impl.Name}", results[1].SuggestionText);
const string Client = "SubsystemTest";
const string Input = "Hello world";
var predCxt = PredictionContext.Create(Input);
var results = impl.GetSuggestion(Client, predCxt, CancellationToken.None);
Assert.Equal($"'{Input}' from '{Client}' - TEST-1 from {impl.Name}", results.SuggestionEntries[0].SuggestionText);
Assert.Equal($"'{Input}' from '{Client}' - TeSt-2 from {impl.Name}", results.SuggestionEntries[1].SuggestionText);
// Now validate the all-subsystem-implementation collection.
ReadOnlyCollection<ICommandPredictor> impls = SubsystemManager.GetSubsystems<ICommandPredictor>();
+23 -7
View File
@@ -2030,24 +2030,27 @@ function CopyReferenceAssemblies
switch ($assemblyName) {
{ $_ -in $supportedRefList } {
$refDll = Join-Path -Path $refBinPath -ChildPath "$assemblyName.dll"
Copy-Item $refDll -Destination $refNugetPath -Force
Write-Log "Copied file $refDll to $refNugetPath"
$refDoc = Join-Path -Path $refBinPath -ChildPath "$assemblyName.xml"
Copy-Item $refDll, $refDoc -Destination $refNugetPath -Force
Write-Log "Copied file '$refDll' and '$refDoc' to '$refNugetPath'"
}
"Microsoft.PowerShell.SDK" {
foreach ($asmFileName in $assemblyFileList) {
$refFile = Join-Path -Path $refBinPath -ChildPath $asmFileName
if (Test-Path -Path $refFile) {
Copy-Item $refFile -Destination $refNugetPath -Force
Write-Log "Copied file $refFile to $refNugetPath"
$refDoc = Join-Path -Path $refBinPath -ChildPath ([System.IO.Path]::ChangeExtension($asmFileName, "xml"))
Copy-Item $refFile, $refDoc -Destination $refNugetPath -Force
Write-Log "Copied file '$refFile' and '$refDoc' to '$refNugetPath'"
}
}
}
default {
$ref_SMA = Join-Path -Path $refBinPath -ChildPath System.Management.Automation.dll
Copy-Item $ref_SMA -Destination $refNugetPath -Force
Write-Log "Copied file $ref_SMA to $refNugetPath"
$ref_doc = Join-Path -Path $refBinPath -ChildPath System.Management.Automation.xml
Copy-Item $ref_SMA, $ref_doc -Destination $refNugetPath -Force
Write-Log "Copied file '$ref_SMA' and '$ref_doc' to '$refNugetPath'"
}
}
}
@@ -2229,8 +2232,13 @@ function New-ReferenceAssembly
throw "$assemblyName.dll was not found at: $Linux64BinPath"
}
$dllXmlDoc = Join-Path $Linux64BinPath "$assemblyName.xml"
if (-not (Test-Path $dllXmlDoc)) {
throw "$assemblyName.xml was not found at: $Linux64BinPath"
}
$genAPIArgs = "$linuxDllPath","-libPath:$Linux64BinPath,$Linux64BinPath\ref"
Write-Log "GenAPI cmd: $genAPIExe $genAPIArgsString"
Write-Log "GenAPI cmd: $genAPIExe $genAPIArgs"
Start-NativeExecution { & $genAPIExe $genAPIArgs } | Out-File $generatedSource -Force
Write-Log "Reference assembly file generated at: $generatedSource"
@@ -2262,6 +2270,9 @@ function New-ReferenceAssembly
Copy-Item $refBinPath $RefAssemblyDestinationPath -Force
Write-Log "Reference assembly '$assemblyName.dll' built and copied to $RefAssemblyDestinationPath"
Copy-Item $dllXmlDoc $RefAssemblyDestinationPath -Force
Write-Log "Xml document '$assemblyName.xml' copied to $RefAssemblyDestinationPath"
if ($assemblyName -eq "System.Management.Automation") {
$SMAReferenceAssembly = $refBinPath
}
@@ -2361,6 +2372,11 @@ function CleanupGeneratedSourceCode
Pattern = "[System.Runtime.CompilerServices.CompilerGeneratedAttribute, System.Runtime.CompilerServices.NullableContextAttribute((byte)2)]"
Replacement = "/* [System.Runtime.CompilerServices.CompilerGeneratedAttribute, System.Runtime.CompilerServices.NullableContextAttribute((byte)2)] */ "
},
@{
ApplyTo = @("System.Management.Automation")
Pattern = "[System.Runtime.CompilerServices.CompilerGeneratedAttribute, System.Runtime.CompilerServices.IsReadOnlyAttribute]"
Replacement = "/* [System.Runtime.CompilerServices.CompilerGeneratedAttribute, System.Runtime.CompilerServices.IsReadOnlyAttribute] */ "
},
@{
ApplyTo = @("System.Management.Automation", "Microsoft.PowerShell.ConsoleHost")
Pattern = "[System.Runtime.CompilerServices.NullableAttribute((byte)2)]"