77using System . Management . Automation ;
88using System . Management . Automation . Language ;
99using Microsoft . Windows . PowerShell . ScriptAnalyzer . Generic ;
10+ using System . Collections . Concurrent ;
1011#if ! CORECLR
1112using System . ComponentModel . Composition ;
1213#endif
@@ -22,6 +23,27 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
2223#endif
2324 public class UseCmdletCorrectly : IScriptRule
2425 {
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+
2547 /// <summary>
2648 /// AnalyzeScript: Check that cmdlets are invoked with the correct mandatory parameter
2749 /// </summary>
@@ -61,41 +83,69 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
6183 /// <returns></returns>
6284 private bool MandatoryParameterExists ( CommandAst cmdAst )
6385 {
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.
6887
69- #region Predicates
88+ CommandInfo cmdInfo = Helper . Instance . GetCommandInfoLegacy ( cmdAst . GetCommandName ( ) ) ;
7089
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 ) )
7392 {
74- if ( ceAst is CommandParameterAst ) return true ;
75- return false ;
76- } ;
77-
78- #endregion
93+ return true ;
94+ }
7995
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+ }
81101
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 ) )
84104 {
85105 return true ;
86106 }
87107
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 )
90112 {
91113 return true ;
92114 }
93115
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+ }
96145
97146 // Gets mandatory parameters from cmdlet.
98147 // If cannot find any mandatory parameter, it's not necessary to do a further check for current cmdlet.
148+ var mandatoryParameters = new List < ParameterMetadata > ( ) ;
99149 try
100150 {
101151 int noOfParamSets = cmdInfo . ParameterSets . Count ;
@@ -119,7 +169,7 @@ private bool MandatoryParameterExists(CommandAst cmdAst)
119169
120170 if ( count >= noOfParamSets )
121171 {
122- mandParams . Add ( pm ) ;
172+ mandatoryParameters . Add ( pm ) ;
123173 }
124174 }
125175 }
@@ -129,28 +179,25 @@ private bool MandatoryParameterExists(CommandAst cmdAst)
129179 return true ;
130180 }
131181
132- if ( mandParams . Count == 0 || ( Helper . Instance . IsKnownCmdletFunctionOrExternalScript ( cmdAst ) && Helper . Instance . PositionalParameterUsed ( cmdAst ) ) )
182+ if ( mandatoryParameters . Count == 0 )
133183 {
134- returnValue = true ;
184+ return true ;
135185 }
136- else
186+
187+ // Compares parameter list and mandatory parameter list.
188+ foreach ( CommandElementAst commandElementAst in cmdAst . CommandElements . OfType < CommandParameterAst > ( ) )
137189 {
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 )
140193 {
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 ;
148195 }
149196 }
150197
151198 #endregion
152199
153- return returnValue ;
200+ return false ;
154201 }
155202
156203 /// <summary>
0 commit comments