1
+ // Copyright (c) Microsoft Corporation. All rights reserved.
2
+ // Licensed under the MIT License.
3
+
4
+ using Microsoft . Windows . PowerShell . ScriptAnalyzer . Generic ;
5
+ using System ;
6
+ using System . Collections . Generic ;
7
+ using System . Globalization ;
8
+ using System . Linq ;
9
+ using System . Management . Automation . Language ;
10
+ #if ! CORECLR
11
+ using System . ComponentModel . Composition ;
12
+ #endif
13
+
14
+ namespace Microsoft . Windows . PowerShell . ScriptAnalyzer . BuiltinRules
15
+ {
16
+ #if ! CORECLR
17
+ [ Export ( typeof ( IScriptRule ) ) ]
18
+ #endif
19
+
20
+ /// <summary>
21
+ /// Rule that identifies parameter blocks with multiple parameters in
22
+ /// the same parameter set that are marked as ValueFromPipeline=true, which
23
+ /// can cause undefined behavior.
24
+ /// </summary>
25
+ public class UseSingleValueFromPipelineParameter : IScriptRule
26
+ {
27
+ private const string AllParameterSetsName = "__AllParameterSets" ;
28
+
29
+ /// <summary>
30
+ /// Analyzes the PowerShell AST for parameter sets with multiple ValueFromPipeline parameters.
31
+ /// </summary>
32
+ /// <param name="ast">The PowerShell Abstract Syntax Tree to analyze.</param>
33
+ /// <param name="fileName">The name of the file being analyzed (for diagnostic reporting).</param>
34
+ /// <returns>A collection of diagnostic records for each violating parameter.</returns>
35
+ public IEnumerable < DiagnosticRecord > AnalyzeScript ( Ast ast , string fileName )
36
+ {
37
+ if ( ast == null )
38
+ {
39
+ yield break ;
40
+ }
41
+ // Find all param blocks that have a Parameter attribute with
42
+ // ValueFromPipeline set to true.
43
+ var paramBlocks = ast . FindAll ( testAst => testAst is ParamBlockAst , true )
44
+ . Where ( paramBlock => paramBlock . FindAll (
45
+ attributeAst => attributeAst is AttributeAst attr &&
46
+ ParameterAttributeAstHasValueFromPipeline ( attr ) ,
47
+ true
48
+ ) . Any ( ) ) ;
49
+
50
+ foreach ( var paramBlock in paramBlocks )
51
+ {
52
+ // Find all parameter declarations in the current param block
53
+ // Convert the generic ast objects into ParameterAst Objects
54
+ // For each ParameterAst, find all it's attributes that have
55
+ // ValueFromPipeline set to true (either explicitly or
56
+ // implicitly). Flatten the results into a single collection of
57
+ // Annonymous objects relating the parameter with it's attribute
58
+ // and then group them by parameter set name.
59
+ //
60
+ //
61
+ // https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parameter_sets?#reserved-parameter-set-name
62
+ //
63
+ // The default parameter set name is '__AllParameterSets'.
64
+ // Not specifying a parameter set name and using the parameter
65
+ // set name '__AllParameterSets' are equivalent, so we shouldn't
66
+ // treat them like they're different just because one is an
67
+ // empty string and the other is not.
68
+ //
69
+ // Filter the list to only keep parameter sets that have more
70
+ // than one ValueFromPipeline parameter.
71
+ var parameterSetGroups = paramBlock . FindAll ( n => n is ParameterAst , true )
72
+ . Cast < ParameterAst > ( )
73
+ . SelectMany ( parameter => parameter . FindAll (
74
+ a => a is AttributeAst attr && ParameterAttributeAstHasValueFromPipeline ( attr ) ,
75
+ true
76
+ ) . Cast < AttributeAst > ( ) . Select ( attr => new { Parameter = parameter , Attribute = attr } ) )
77
+ . GroupBy ( item => GetParameterSetForAttribute ( item . Attribute ) ?? AllParameterSetsName )
78
+ . Where ( group => group . Count ( ) > 1 ) ;
79
+
80
+
81
+ foreach ( var group in parameterSetGroups )
82
+ {
83
+ // __AllParameterSets being the default name is...obscure.
84
+ // Instead we'll show the user "default". It's more than
85
+ // likely the user has not specified a parameter set name,
86
+ // so default will make sense. If they have used 'default'
87
+ // as their parameter set name, then we're still correct.
88
+ var parameterSetName = group . Key == AllParameterSetsName ? "default" : group . Key ;
89
+
90
+ // Create a concatenated string of parameter names that
91
+ // conflict in this parameter set
92
+ var parameterNames = string . Join ( ", " , group . Select ( item => item . Parameter . Name . VariablePath . UserPath ) ) ;
93
+
94
+ // We emit a diagnostic record for each offending parameter
95
+ // attribute in the parameter set so it's obvious where all the
96
+ // occurrences are.
97
+ foreach ( var item in group )
98
+ {
99
+ var message = string . Format ( CultureInfo . CurrentCulture ,
100
+ Strings . UseSingleValueFromPipelineParameterError ,
101
+ parameterNames ,
102
+ parameterSetName ) ;
103
+
104
+ yield return new DiagnosticRecord (
105
+ message ,
106
+ item . Attribute . Extent ,
107
+ GetName ( ) ,
108
+ DiagnosticSeverity . Warning ,
109
+ fileName ,
110
+ parameterSetName ) ;
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ /// <summary>
117
+ /// Returns whether the specified AttributeAst represents a Parameter attribute
118
+ /// that has the ValueFromPipeline named argument set to true (either explicitly or
119
+ /// implicitly).
120
+ /// </summary>
121
+ /// <param name="attributeAst">The Parameter attribute to examine.</param>
122
+ /// <returns>Whether the attribute has the ValueFromPipeline named argument set to true.</returns>
123
+ private static bool ParameterAttributeAstHasValueFromPipeline ( AttributeAst attributeAst )
124
+ {
125
+ // Exit quickly if the attribute is null, has no named arguments, or
126
+ // is not a parameter attribute.
127
+ if ( attributeAst ? . NamedArguments == null ||
128
+ ! string . Equals ( attributeAst . TypeName ? . Name , "Parameter" , StringComparison . OrdinalIgnoreCase ) )
129
+ {
130
+ return false ;
131
+ }
132
+
133
+ return attributeAst . NamedArguments
134
+ . OfType < NamedAttributeArgumentAst > ( )
135
+ . Any ( namedArg => string . Equals (
136
+ namedArg ? . ArgumentName ,
137
+ "ValueFromPipeline" ,
138
+ StringComparison . OrdinalIgnoreCase
139
+ // Helper.Instance.GetNamedArgumentAttributeValue handles both explicit ($true)
140
+ // and implicit (no value specified) ValueFromPipeline declarations
141
+ ) && Helper . Instance . GetNamedArgumentAttributeValue ( namedArg ) ) ;
142
+ }
143
+
144
+ /// <summary>
145
+ /// Gets the ParameterSetName value from a Parameter attribute.
146
+ /// </summary>
147
+ /// <param name="attributeAst">The Parameter attribute to examine.</param>
148
+ /// <returns>The parameter set name, or null if not found or empty.</returns>
149
+ private static string GetParameterSetForAttribute ( AttributeAst attributeAst )
150
+ {
151
+ // Exit quickly if the attribute is null, has no named arguments, or
152
+ // is not a parameter attribute.
153
+ if ( attributeAst ? . NamedArguments == null ||
154
+ ! string . Equals ( attributeAst . TypeName . Name , "Parameter" , StringComparison . OrdinalIgnoreCase ) )
155
+ {
156
+ return null ;
157
+ }
158
+
159
+ return attributeAst . NamedArguments
160
+ . OfType < NamedAttributeArgumentAst > ( )
161
+ . Where ( namedArg => string . Equals (
162
+ namedArg ? . ArgumentName ,
163
+ "ParameterSetName" ,
164
+ StringComparison . OrdinalIgnoreCase
165
+ ) )
166
+ . Select ( namedArg => namedArg ? . Argument )
167
+ . OfType < StringConstantExpressionAst > ( )
168
+ . Select ( stringConstAst => stringConstAst ? . Value )
169
+ . FirstOrDefault ( value => ! string . IsNullOrWhiteSpace ( value ) ) ;
170
+ }
171
+
172
+ public string GetCommonName ( ) => Strings . UseSingleValueFromPipelineParameterCommonName ;
173
+
174
+ public string GetDescription ( ) => Strings . UseSingleValueFromPipelineParameterDescription ;
175
+
176
+ public string GetName ( ) => Strings . UseSingleValueFromPipelineParameterName ;
177
+
178
+ public RuleSeverity GetSeverity ( ) => RuleSeverity . Warning ;
179
+
180
+ public string GetSourceName ( ) => Strings . SourceName ;
181
+
182
+ public SourceType GetSourceType ( ) => SourceType . Builtin ;
183
+ }
184
+ }
0 commit comments