7
7
using System . Management . Automation ;
8
8
using System . Management . Automation . Language ;
9
9
using Microsoft . Windows . PowerShell . ScriptAnalyzer . Generic ;
10
+ using System . Collections . Concurrent ;
10
11
#if ! CORECLR
11
12
using System . ComponentModel . Composition ;
12
13
#endif
@@ -22,6 +23,27 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
22
23
#endif
23
24
public class UseCmdletCorrectly : IScriptRule
24
25
{
26
+ // Cache of the mandatory parameters of cmdlets in PackageManagement
27
+ // Key: Cmdlet name
28
+ // Value: List of mandatory parameters
29
+ private static readonly ConcurrentDictionary < string , IReadOnlyList < string > > s_pkgMgmtMandatoryParameters =
30
+ new ConcurrentDictionary < string , IReadOnlyList < string > > ( new Dictionary < string , IReadOnlyList < string > >
31
+ {
32
+ { "Find-Package" , new string [ 0 ] } ,
33
+ { "Find-PackageProvider" , new string [ 0 ] } ,
34
+ { "Get-Package" , new string [ 0 ] } ,
35
+ { "Get-PackageProvider" , new string [ 0 ] } ,
36
+ { "Get-PackageSource" , new string [ 0 ] } ,
37
+ { "Import-PackageProvider" , new string [ ] { "Name" } } ,
38
+ { "Install-Package" , new string [ ] { "Name" } } ,
39
+ { "Install-PackageProvider" , new string [ ] { "Name" } } ,
40
+ { "Register-PackageSource" , new string [ ] { "ProviderName" } } ,
41
+ { "Save-Package" , new string [ ] { "Name" , "InputObject" } } ,
42
+ { "Set-PackageSource" , new string [ ] { "Name" , "Location" } } ,
43
+ { "Uninstall-Package" , new string [ ] { "Name" , "InputObject" } } ,
44
+ { "Unregister-PackageSource" , new string [ ] { "Name" , "InputObject" } } ,
45
+ } ) ;
46
+
25
47
/// <summary>
26
48
/// AnalyzeScript: Check that cmdlets are invoked with the correct mandatory parameter
27
49
/// </summary>
@@ -61,41 +83,69 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
61
83
/// <returns></returns>
62
84
private bool MandatoryParameterExists ( CommandAst cmdAst )
63
85
{
64
- CommandInfo cmdInfo = null ;
65
- List < ParameterMetadata > mandParams = new List < ParameterMetadata > ( ) ;
66
- IEnumerable < CommandElementAst > ceAsts = null ;
67
- bool returnValue = false ;
86
+ #region Compares parameter list and mandatory parameter list.
68
87
69
- #region Predicates
88
+ CommandInfo cmdInfo = Helper . Instance . GetCommandInfoLegacy ( cmdAst . GetCommandName ( ) ) ;
70
89
71
- // Predicate to find ParameterAsts.
72
- Func < CommandElementAst , bool > foundParamASTs = delegate ( CommandElementAst ceAst )
90
+ // If we can't resolve the command or it's not a cmdlet, we are done
91
+ if ( cmdInfo == null || ( cmdInfo . CommandType != System . Management . Automation . CommandTypes . Cmdlet ) )
73
92
{
74
- if ( ceAst is CommandParameterAst ) return true ;
75
- return false ;
76
- } ;
77
-
78
- #endregion
93
+ return true ;
94
+ }
79
95
80
- #region Compares parameter list and mandatory parameter list.
96
+ // We can't statically analyze splatted variables, so ignore them
97
+ if ( Helper . Instance . HasSplattedVariable ( cmdAst ) )
98
+ {
99
+ return true ;
100
+ }
81
101
82
- cmdInfo = Helper . Instance . GetCommandInfoLegacy ( cmdAst . GetCommandName ( ) ) ;
83
- if ( cmdInfo == null || ( cmdInfo . CommandType != System . Management . Automation . CommandTypes . Cmdlet ) )
102
+ // Positional parameters could be mandatory, so we assume all is well
103
+ if ( Helper . Instance . PositionalParameterUsed ( cmdAst ) && Helper . Instance . IsKnownCmdletFunctionOrExternalScript ( cmdAst ) )
84
104
{
85
105
return true ;
86
106
}
87
107
88
- // ignores if splatted variable is used
89
- if ( Helper . Instance . HasSplattedVariable ( cmdAst ) )
108
+ // If the command is piped to, this also precludes mandatory parameters
109
+ if ( cmdAst . Parent is PipelineAst parentPipeline
110
+ && parentPipeline . PipelineElements . Count > 1
111
+ && parentPipeline . PipelineElements [ 0 ] != cmdAst )
90
112
{
91
113
return true ;
92
114
}
93
115
94
- // Gets parameters from command elements.
95
- ceAsts = cmdAst . CommandElements . Where < CommandElementAst > ( foundParamASTs ) ;
116
+ // We want to check cmdlets from PackageManagement separately because they experience a deadlock
117
+ // when cmdInfo.Parameters or cmdInfo.ParameterSets is accessed.
118
+ // See https://github.com/PowerShell/PSScriptAnalyzer/issues/1297
119
+ if ( s_pkgMgmtMandatoryParameters . TryGetValue ( cmdInfo . Name , out IReadOnlyList < string > pkgMgmtCmdletMandatoryParams ) )
120
+ {
121
+ // If the command has no parameter sets with mandatory parameters, we are done
122
+ if ( pkgMgmtCmdletMandatoryParams . Count == 0 )
123
+ {
124
+ return true ;
125
+ }
126
+
127
+ // We make the following simplifications here that all apply to the PackageManagement cmdlets:
128
+ // - Only one mandatory parameter per parameter set
129
+ // - Any part of the parameter prefix is valid
130
+ // - There are no parameter sets without mandatory parameters
131
+ IEnumerable < CommandParameterAst > parameterAsts = cmdAst . CommandElements . OfType < CommandParameterAst > ( ) ;
132
+ foreach ( string mandatoryParameter in pkgMgmtCmdletMandatoryParams )
133
+ {
134
+ foreach ( CommandParameterAst parameterAst in parameterAsts )
135
+ {
136
+ if ( mandatoryParameter . StartsWith ( parameterAst . ParameterName ) )
137
+ {
138
+ return true ;
139
+ }
140
+ }
141
+ }
142
+
143
+ return false ;
144
+ }
96
145
97
146
// Gets mandatory parameters from cmdlet.
98
147
// If cannot find any mandatory parameter, it's not necessary to do a further check for current cmdlet.
148
+ var mandatoryParameters = new List < ParameterMetadata > ( ) ;
99
149
try
100
150
{
101
151
int noOfParamSets = cmdInfo . ParameterSets . Count ;
@@ -119,7 +169,7 @@ private bool MandatoryParameterExists(CommandAst cmdAst)
119
169
120
170
if ( count >= noOfParamSets )
121
171
{
122
- mandParams . Add ( pm ) ;
172
+ mandatoryParameters . Add ( pm ) ;
123
173
}
124
174
}
125
175
}
@@ -129,28 +179,25 @@ private bool MandatoryParameterExists(CommandAst cmdAst)
129
179
return true ;
130
180
}
131
181
132
- if ( mandParams . Count == 0 || ( Helper . Instance . IsKnownCmdletFunctionOrExternalScript ( cmdAst ) && Helper . Instance . PositionalParameterUsed ( cmdAst ) ) )
182
+ if ( mandatoryParameters . Count == 0 )
133
183
{
134
- returnValue = true ;
184
+ return true ;
135
185
}
136
- else
186
+
187
+ // Compares parameter list and mandatory parameter list.
188
+ foreach ( CommandElementAst commandElementAst in cmdAst . CommandElements . OfType < CommandParameterAst > ( ) )
137
189
{
138
- // Compares parameter list and mandatory parameter list.
139
- foreach ( CommandElementAst ceAst in ceAsts )
190
+ CommandParameterAst cpAst = ( CommandParameterAst ) commandElementAst ;
191
+ if ( mandatoryParameters . Count < ParameterMetadata > ( item =>
192
+ item . Name . Equals ( cpAst . ParameterName , StringComparison . OrdinalIgnoreCase ) ) > 0 )
140
193
{
141
- CommandParameterAst cpAst = ( CommandParameterAst ) ceAst ;
142
- if ( mandParams . Count < ParameterMetadata > ( item =>
143
- item . Name . Equals ( cpAst . ParameterName , StringComparison . OrdinalIgnoreCase ) ) > 0 )
144
- {
145
- returnValue = true ;
146
- break ;
147
- }
194
+ return true ;
148
195
}
149
196
}
150
197
151
198
#endregion
152
199
153
- return returnValue ;
200
+ return false ;
154
201
}
155
202
156
203
/// <summary>
0 commit comments