Skip to content

Commit 1a1a621

Browse files
Jawz84SeeminglyScience
authored andcommitted
Feature #29 splat expression (#30)
* Add -AllParameters parameter to ConvertTo-SplatExpression * Add help descriptoin for paramater -AllParameters * Throw unimplemented exception when using -AllParameters parameter * Add test for -AllParameters parameter * rudimentary partial implementation of -AllParameters functioality * Create ParameterList from remaining parameters, next line brace format * Remove CommonParameters from ParameterList, lame implementation. * Add ParameterSet logic, refactor into function - broken Linq expressions * Remove obsolete variable CommonParameters * Repair linq expressions, change outputtype to make thing easier. * Re-add logic for single parametersets, remove whitespace. * Re-add logic to omit (optional) commonparameters. * Add support for -MandatoryParameters switchparameter. * Simplified implementation of parameterset matching * Clarified comments * Fix bugs with no bound parameters, and unmatched parametersets * Fix wrong splat variable placement when no parameters where provided. * Make ambigious parameterset throw error * Add formatting and hints for type and mandatory the ugly way. tests pass * Add class Parameter * Add asterisk to Symbols class * Add aligner functionality so '=' signs can be aligned in splat. * Add empty return for params without a value. * Fix StyleCop warnings * Refactor GetEdits, so all parameters can be handled in the same way. * Add -NoHints parameter, so people can choose not to display typehints. * Move executionContext out of GetEdits() for sane testing. * Fix error when ui == null in testsetup. Await doesn't like null. * Fix error with equalSignAligner when pList holds no items. * Add tests for -AllParameters, -MandatoryParameters and -NoHints * Fix test CmdLet wit no params, given incorrect param. * Process review on CommandSplatTests * Process review for SettingsStrings.resx * Process review for Parameter.cs * Fix attribute names, no need to append 'Attribute' here. * Change pList to parameterList for readability * Process review, remove comments, fix indentation, fix naming. * Replace custom const with PowerShell public constant string. * Use TryGetValue() instead of more complicated LINQ expression. * Fix build warnings, and fix NoHints test * Refactor ResolveParameterSet for performance and clarity. * Remove '=' alignment (Alt-Shift-F does that, respecting tabs/spaces) * Optimize parameterInfo * Move Write.AsExpressionValue() to PowerShellScriptWriter class * Move parameter hint logic to PowershellScriptWriter * Adapt tests to new hint implementation
1 parent dd7b6f9 commit 1a1a621

File tree

9 files changed

+749
-80
lines changed

9 files changed

+749
-80
lines changed

src/EditorServicesCommandSuite/CodeGeneration/PowerShellScriptWriter.cs

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using System.Management.Automation;
55
using System.Management.Automation.Language;
66
using System.Reflection;
7+
using System.Text;
8+
using EditorServicesCommandSuite.CodeGeneration.Refactors;
79
using EditorServicesCommandSuite.Internal;
810
using EditorServicesCommandSuite.Language;
911
using EditorServicesCommandSuite.Reflection;
@@ -152,6 +154,13 @@ public bool AddUsingStatements(HashSet<string> namespaces, out int replaceLength
152154

153155
public override void Write(params char[] buffer) => base.Write(buffer);
154156

157+
public void WriteFirstCharLowerCase(string value)
158+
{
159+
var chars = value.ToCharArray();
160+
Write(char.ToLower(chars[0]));
161+
Write(chars, 1, chars.Count() - 1);
162+
}
163+
155164
public override void StartWriting(int startOffset, int endOffset)
156165
{
157166
SetPosition(startOffset);
@@ -277,7 +286,7 @@ internal void WriteAssignment(Action rhs, Action lhs)
277286
lhs();
278287
}
279288

280-
internal void WriteVariable(string variableName, bool isSplat = false)
289+
internal void WriteVariable(string variableName, bool isSplat = false, bool shouldFirstCharBeLowerCase = false)
281290
{
282291
if (isSplat)
283292
{
@@ -294,7 +303,14 @@ internal void WriteVariable(string variableName, bool isSplat = false)
294303
Write(CurlyOpen);
295304
}
296305

297-
Write(variableName);
306+
if (shouldFirstCharBeLowerCase)
307+
{
308+
WriteFirstCharLowerCase(variableName);
309+
}
310+
else
311+
{
312+
Write(variableName);
313+
}
298314

299315
if (shouldEscape)
300316
{
@@ -588,6 +604,72 @@ internal void WriteReflectionTypeExpression(Type type)
588604
Write(ParenClose);
589605
}
590606

607+
internal void WriteAsExpressionValue(Parameter param, bool shouldNotWriteHints)
608+
{
609+
if (param.Value == null)
610+
{
611+
if (shouldNotWriteHints)
612+
{
613+
WriteVariable(param.Name, shouldFirstCharBeLowerCase: true);
614+
}
615+
else
616+
{
617+
Write(Dollar);
618+
619+
if (param.IsMandatory)
620+
{
621+
Write("mandatory");
622+
Write(param.Type.Name.Replace("[]", "Array"));
623+
}
624+
else
625+
{
626+
WriteFirstCharLowerCase(param.Type.Name.Replace("[]", "Array"));
627+
}
628+
629+
Write(param.Name);
630+
}
631+
632+
return;
633+
}
634+
635+
if (param.Value.ConstantValue is bool boolean)
636+
{
637+
Write(Dollar + boolean.ToString().ToLower());
638+
return;
639+
}
640+
641+
if (param.Value.Value is StringConstantExpressionAst ||
642+
param.Value.Value is ExpandableStringExpressionAst)
643+
{
644+
dynamic stringExpression = param.Value.Value;
645+
if (stringExpression.StringConstantType != StringConstantType.BareWord)
646+
{
647+
Write(param.Value.Value.Extent.Text);
648+
return;
649+
}
650+
651+
if (param.Value.Value is StringConstantExpressionAst constant)
652+
{
653+
WriteStringExpression(
654+
StringConstantType.SingleQuoted,
655+
constant.Value);
656+
return;
657+
}
658+
659+
WriteStringExpression(
660+
StringConstantType.DoubleQuoted,
661+
param.Value.Value.Extent.Text);
662+
return;
663+
}
664+
665+
WriteEachWithSeparator(
666+
param.Value.Value.Extent.Text.Split(
667+
new[] { this.NewLine },
668+
StringSplitOptions.None),
669+
line => Write(line),
670+
() => WriteLine());
671+
}
672+
591673
internal void WriteAttributeStatement(PSTypeName typeName)
592674
{
593675
OpenAttributeStatement(typeName);

src/EditorServicesCommandSuite/CodeGeneration/Refactors/CommandSplatRefactor.cs

Lines changed: 134 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Globalization;
34
using System.Linq;
@@ -32,32 +33,110 @@ internal static async Task<IEnumerable<DocumentEdit>> GetEdits(
3233
string variableName,
3334
CommandAst commandAst,
3435
bool newLineAfterHashtable,
36+
bool allParameters,
37+
bool mandatoryParameters,
38+
bool noHints,
39+
EngineIntrinsics executionContext,
3540
IRefactorUI ui = null)
3641
{
3742
var parentStatement = commandAst.FindParent<StatementAst>();
43+
var commandName = commandAst.GetCommandName();
3844
var elements = commandAst.CommandElements.Skip(1);
39-
4045
var elementsExtent = elements.JoinExtents();
41-
var boundParameters = StaticParameterBinder.BindCommand(commandAst, true);
42-
if (!boundParameters.BoundParameters.Any())
46+
var boundParameters = StaticParameterBinder.BindCommand(commandAst, resolve: true);
47+
48+
if (boundParameters.BindingExceptions.TryGetValue(commandName, out StaticBindingError globalError) &&
49+
globalError.BindingException.ErrorId.Equals("AmbiguousParameterSet", StringComparison.Ordinal))
50+
{
51+
if (ui != null)
52+
{
53+
await ui.ShowErrorMessageAsync(globalError.BindingException.Message);
54+
return Enumerable.Empty<DocumentEdit>();
55+
}
56+
57+
throw new PSInvalidOperationException(globalError.BindingException.Message, globalError.BindingException);
58+
}
59+
60+
if (boundParameters.BoundParameters.Count == 0 &&
61+
!(allParameters || mandatoryParameters))
4362
{
4463
return Enumerable.Empty<DocumentEdit>();
4564
}
4665

66+
var commandInfo = executionContext
67+
.InvokeCommand
68+
.GetCommand(commandName, CommandTypes.All);
69+
70+
var parameterSetName = ResolveParameterSet(boundParameters, commandInfo);
71+
72+
var parameterInfo = commandInfo.ParameterSets
73+
.FirstOrDefault(set => set.Name.Equals(parameterSetName, StringComparison.Ordinal))
74+
?.Parameters;
75+
76+
List<Parameter> parameterList = new List<Parameter>();
77+
78+
foreach (var param in parameterInfo)
79+
{
80+
var shouldAdd = false;
81+
ParameterBindingResult boundParameterValue = null;
82+
83+
if (allParameters)
84+
{
85+
shouldAdd = true;
86+
}
87+
88+
if (mandatoryParameters && param.IsMandatory)
89+
{
90+
shouldAdd = true;
91+
}
92+
93+
if (Cmdlet.CommonParameters.Contains(param.Name) || Cmdlet.OptionalCommonParameters.Contains(param.Name))
94+
{
95+
shouldAdd = false;
96+
}
97+
98+
if (boundParameters.BoundParameters.ContainsKey(param.Name))
99+
{
100+
boundParameters.BoundParameters.TryGetValue(
101+
param.Name,
102+
out boundParameterValue);
103+
}
104+
105+
if (boundParameterValue != null || shouldAdd)
106+
{
107+
parameterList.Add(
108+
new Parameter(
109+
param.Name,
110+
boundParameterValue,
111+
param.IsMandatory,
112+
param.ParameterType));
113+
}
114+
}
115+
47116
var splatWriter = new PowerShellScriptWriter(commandAst);
117+
var elementsWriter = new PowerShellScriptWriter(commandAst);
118+
48119
splatWriter.SetPosition(parentStatement);
49120
splatWriter.WriteAssignment(
50121
() => splatWriter.WriteVariable(variableName),
51122
() => splatWriter.OpenHashtable());
52123

53-
var elementsWriter = new PowerShellScriptWriter(commandAst);
54-
elementsWriter.SetPosition(elementsExtent);
124+
if (elementsExtent is EmptyExtent)
125+
{
126+
elementsWriter.SetPosition(parentStatement, atEnd: true);
127+
elementsWriter.Write(Symbols.Space);
128+
}
129+
else
130+
{
131+
elementsWriter.SetPosition(elementsExtent);
132+
}
133+
55134
elementsWriter.WriteVariable(variableName, isSplat: true);
56135

57136
var first = true;
58-
foreach (var param in boundParameters.BoundParameters)
137+
foreach (var param in parameterList)
59138
{
60-
if (param.Key.All(c => char.IsDigit(c)))
139+
if (param.Name.All(c => char.IsDigit(c)))
61140
{
62141
elementsWriter.Write(
63142
Symbols.Space
@@ -75,21 +154,25 @@ internal static async Task<IEnumerable<DocumentEdit>> GetEdits(
75154
}
76155

77156
splatWriter.WriteHashtableEntry(
78-
param.Key,
79-
() => Write.AsExpressionValue(splatWriter, param.Value));
157+
param.Name,
158+
() => splatWriter.WriteAsExpressionValue(param, noHints));
80159
}
81160

82161
foreach (var bindingException in boundParameters.BindingExceptions)
83162
{
84163
elementsWriter.Write(Symbols.Space);
85164
elementsWriter.Write(bindingException.Value.CommandElement.Extent.Text);
86165

87-
await ui?.ShowWarningMessageAsync(
88-
string.Format(
89-
CultureInfo.CurrentCulture,
90-
CommandSplatStrings.CouldNotResolvePositionalArgument,
91-
bindingException.Value.CommandElement.Extent.Text),
92-
waitForResponse: false);
166+
// The ui?.ShowWarningMessageAsync() pattern does not work during testing. Await does not seem to like null.
167+
if (ui != null)
168+
{
169+
await ui.ShowWarningMessageAsync(
170+
string.Format(
171+
CultureInfo.CurrentCulture,
172+
CommandSplatStrings.CouldNotResolvePositionalArgument,
173+
bindingException.Value.CommandElement.Extent.Text),
174+
waitForResponse: false);
175+
}
93176
}
94177

95178
splatWriter.CloseHashtable();
@@ -122,14 +205,50 @@ internal override async Task<IEnumerable<DocumentEdit>> RequestEdits(DocumentCon
122205
var splatVariable = string.IsNullOrWhiteSpace(config.VariableName)
123206
? GetSplatVariableName(ast.CommandElements.First())
124207
: config.VariableName;
208+
var executionContext = CommandSuite.Instance.ExecutionContext;
125209

126210
return await GetEdits(
127211
splatVariable,
128212
ast,
129213
config.NewLineAfterHashtable.IsPresent,
214+
config.AllParameters.IsPresent,
215+
config.MandatoryParameters.IsPresent,
216+
config.NoHints.IsPresent,
217+
executionContext,
130218
UI);
131219
}
132220

221+
private static string ResolveParameterSet(
222+
StaticBindingResult paramBinder,
223+
CommandInfo commandInfo)
224+
{
225+
if (commandInfo.ParameterSets.Count == 1)
226+
{
227+
return commandInfo.ParameterSets[0].Name;
228+
}
229+
230+
foreach (CommandParameterSetInfo parameterSet in commandInfo.ParameterSets)
231+
{
232+
var currentSetParameterNames = new HashSet<string>(parameterSet.Parameters.Select(p => p.Name));
233+
var isMatch = true;
234+
foreach (string parameterName in paramBinder.BoundParameters.Keys)
235+
{
236+
if (!currentSetParameterNames.Contains(parameterName))
237+
{
238+
isMatch = false;
239+
break;
240+
}
241+
}
242+
243+
if (isMatch)
244+
{
245+
return parameterSet.Name;
246+
}
247+
}
248+
249+
return ParameterAttribute.AllParameterSets;
250+
}
251+
133252
private string GetSplatVariableName(CommandElementAst element)
134253
{
135254
var nameConstant = element as StringConstantExpressionAst;

src/EditorServicesCommandSuite/CodeGeneration/Refactors/CommandSplatRefactorSettings.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,24 @@ namespace EditorServicesCommandSuite.CodeGeneration.Refactors
55
internal class CommandSplatRefactorSettings : RefactorConfiguration
66
{
77
[Parameter(Position = 0)]
8-
[DefaultFromSettingAttribute("CommandSplatRefactor.VariableName", Default = "$null")]
8+
[DefaultFromSetting("CommandSplatRefactor.VariableName", Default = "$null")]
99
[ValidateNotNullOrEmpty]
1010
public string VariableName { get; set; }
1111

1212
[Parameter]
13-
[DefaultFromSettingAttribute("CommandSplatRefactor.NewLineAfterHashtable", Default = "$true")]
13+
[DefaultFromSetting("CommandSplatRefactor.NewLineAfterHashtable", Default = "$true")]
1414
public SwitchParameter NewLineAfterHashtable { get; set; }
15+
16+
[Parameter]
17+
[DefaultFromSetting("CommandSplatRefactor.AllParameters", Default = "$false")]
18+
public SwitchParameter AllParameters { get; set; }
19+
20+
[Parameter]
21+
[DefaultFromSetting("CommandSplatRefactor.MandatoryParameters", Default = "$false")]
22+
public SwitchParameter MandatoryParameters { get; set; }
23+
24+
[Parameter]
25+
[DefaultFromSetting("CommandSplatRefactor.NoHints", Default = "$false")]
26+
public SwitchParameter NoHints { get; set; }
1527
}
1628
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
using System.Management.Automation;
3+
using System.Management.Automation.Language;
4+
using System.Text;
5+
using EditorServicesCommandSuite.Internal;
6+
7+
namespace EditorServicesCommandSuite.CodeGeneration.Refactors
8+
{
9+
internal class Parameter
10+
{
11+
internal Parameter(
12+
string name,
13+
ParameterBindingResult value,
14+
bool isMandatory,
15+
Type parameterType)
16+
{
17+
Name = name;
18+
Value = value;
19+
IsMandatory = isMandatory;
20+
Type = parameterType;
21+
}
22+
23+
public string Name { get; }
24+
25+
public ParameterBindingResult Value { get; }
26+
27+
public bool IsMandatory { get; }
28+
29+
public Type Type { get; }
30+
}
31+
}

0 commit comments

Comments
 (0)