1+ //---------------------------------------------------------------------------------
2+ // Copyright (c) Microsoft Corporation.
3+ // The MIT License (MIT)
4+ //
5+ // Permission is hereby granted, free of charge, to any person obtaining a copy
6+ // of this software and associated documentation files (the "Software"), to deal
7+ // in the Software without restriction, including without limitation the rights
8+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+ // copies of the Software, and to permit persons to whom the Software is
10+ // furnished to do so, subject to the following conditions:
11+ //
12+ // The above copyright notice and this permission notice shall be included in all
13+ // copies or substantial portions of the Software.
14+ //
15+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+ // SOFTWARE.
22+ //---------------------------------------------------------------------------------
23+
24+ using System ;
25+ using System . Collections . Generic ;
26+ using System . Collections . Concurrent ;
27+ using System . Collections . ObjectModel ;
28+ using System . Linq ;
29+ using System . Management . Automation ;
30+ using System . Management . Automation . Language ;
31+ using Microsoft . Windows . PowerShell . ScriptAnalyzer . Generic ;
32+ #if ! CORECLR
33+ using System . ComponentModel . Composition ;
34+ #endif
35+ using System . Globalization ;
36+
37+ namespace Microsoft . Windows . PowerShell . ScriptAnalyzer . BuiltinRules
38+ {
39+ /// <summary>
40+ /// UseFullyQualifiedCmdletNames: Checks if cmdlet and function invocations use fully qualified module names.
41+ /// </summary>
42+ #if ! CORECLR
43+ [ Export ( typeof ( IScriptRule ) ) ]
44+ #endif
45+ public class UseFullyQualifiedCmdletNames : ConfigurableRule
46+ {
47+ private readonly ConcurrentDictionary < string , ResolvedCommand > resolutionCache =
48+ new ConcurrentDictionary < string , ResolvedCommand > ( StringComparer . OrdinalIgnoreCase ) ;
49+
50+ internal const string AnalyzerName = "Microsoft.Windows.PowerShell.ScriptAnalyzer" ;
51+
52+ /// <summary>
53+ /// Modules to ignore when applying this rule.
54+ /// Commands from these modules will not be expanded to their fully qualified names.
55+ /// Default is empty array (no modules ignored - all cmdlets are processed).
56+ /// </summary>
57+ [ ConfigurableRuleProperty ( defaultValue : new string [ ] { } ) ]
58+ public string [ ] IgnoredModules { get ; protected set ; }
59+
60+ /// <summary>
61+ /// Analyzes the given ast to find cmdlet invocations that are not fully qualified.
62+ /// </summary>
63+ /// <param name="ast">The script's ast</param>
64+ /// <param name="fileName">The script's file name</param>
65+ /// <returns>The diagnostic results of this rule</returns>
66+ public override IEnumerable < DiagnosticRecord > AnalyzeScript ( Ast ast , string fileName )
67+ {
68+ if ( ast == null )
69+ {
70+ throw new ArgumentNullException ( nameof ( ast ) ) ;
71+ }
72+
73+ var functionDefinitions = ast . FindAll ( testAst => testAst is FunctionDefinitionAst , true ) . Cast < FunctionDefinitionAst > ( ) . ToList ( ) ;
74+
75+ var commandAsts = ast . FindAll ( testAst => testAst is CommandAst , true ) . Cast < CommandAst > ( ) ;
76+
77+ foreach ( var commandAst in commandAsts )
78+ {
79+ var commandName = commandAst . GetCommandName ( ) ;
80+ if ( string . IsNullOrWhiteSpace ( commandName ) || commandName . Contains ( "\\ " ) )
81+ {
82+ continue ;
83+ }
84+
85+ // Skip commands that resolve to a locally declared function, since qualifying them would change behavior.
86+ if ( IsShadowedByLocalFunction ( commandAst , commandName , functionDefinitions ) )
87+ {
88+ continue ;
89+ }
90+
91+ var resolvedCommand = resolutionCache . GetOrAdd ( commandName , ResolveCommand ) ;
92+ if ( resolvedCommand . FullyQualifiedName == null )
93+ {
94+ continue ;
95+ }
96+
97+ // Re-check ignored modules for cached results (in case IgnoredModules was changed).
98+ if ( IgnoredModules != null && IgnoredModules . Contains ( resolvedCommand . ModuleName , StringComparer . OrdinalIgnoreCase ) )
99+ {
100+ continue ;
101+ }
102+
103+ var extent = commandAst . CommandElements [ 0 ] . Extent ;
104+
105+ string message = string . Format (
106+ CultureInfo . CurrentCulture ,
107+ GetErrorResource ( resolvedCommand . CommandType ) ,
108+ commandName ,
109+ resolvedCommand . FullyQualifiedName ) ;
110+
111+ string correctionDescription = string . Format (
112+ CultureInfo . CurrentCulture ,
113+ Strings . UseFullyQualifiedCmdletNamesCorrection ,
114+ commandName ,
115+ resolvedCommand . FullyQualifiedName ) ;
116+
117+ var suggestedCorrections = new Collection < CorrectionExtent >
118+ {
119+ new CorrectionExtent (
120+ extent . StartLineNumber ,
121+ extent . EndLineNumber ,
122+ extent . StartColumnNumber ,
123+ extent . EndColumnNumber ,
124+ resolvedCommand . FullyQualifiedName ,
125+ fileName ,
126+ correctionDescription )
127+ } ;
128+
129+ yield return new DiagnosticRecord (
130+ message ,
131+ extent ,
132+ GetName ( ) ,
133+ DiagnosticSeverity . Warning ,
134+ fileName ,
135+ null ,
136+ suggestedCorrections ) ;
137+ }
138+ }
139+
140+ /// <summary>
141+ /// Checks whether a command name matches a function declared in a scope that is visible at the
142+ /// command's location.
143+ /// </summary>
144+ private static bool IsShadowedByLocalFunction (
145+ CommandAst commandAst ,
146+ string commandName ,
147+ IEnumerable < FunctionDefinitionAst > functionDefinitions )
148+ {
149+ var commandScope = GetContainingScriptBlock ( commandAst ) ;
150+
151+ foreach ( var functionDefinition in functionDefinitions )
152+ {
153+ if ( ! functionDefinition . Name . Equals ( commandName , StringComparison . OrdinalIgnoreCase ) )
154+ {
155+ continue ;
156+ }
157+
158+ var functionScope = GetContainingScriptBlock ( functionDefinition ) ;
159+ if ( functionScope != null &&
160+ ( functionScope == commandScope || IsAncestorOf ( functionScope , commandScope ) ) )
161+ {
162+ return true ;
163+ }
164+ }
165+
166+ return false ;
167+ }
168+
169+ /// <summary>
170+ /// Returns the nearest enclosing script block, which represents the scope where a function is
171+ /// declared or a command is invoked.
172+ /// </summary>
173+ private static ScriptBlockAst GetContainingScriptBlock ( Ast node )
174+ {
175+ for ( Ast current = node ; current != null ; current = current . Parent )
176+ {
177+ if ( current is ScriptBlockAst scriptBlock )
178+ {
179+ return scriptBlock ;
180+ }
181+ }
182+
183+ return null ;
184+ }
185+
186+ /// <summary>
187+ /// Returns true if the first AST is an ancestor of the second.
188+ /// </summary>
189+ private static bool IsAncestorOf ( Ast ancestor , Ast descendant )
190+ {
191+ for ( Ast current = descendant . Parent ; current != null ; current = current . Parent )
192+ {
193+ if ( current == ancestor )
194+ {
195+ return true ;
196+ }
197+ }
198+
199+ return false ;
200+ }
201+
202+ /// <summary>
203+ /// Returns the message resource appropriate for the resolved command type.
204+ /// </summary>
205+ private static string GetErrorResource ( CommandTypes commandType )
206+ {
207+ switch ( commandType )
208+ {
209+ case CommandTypes . Alias :
210+ return Strings . UseFullyQualifiedCmdletNamesAliasError ;
211+ case CommandTypes . Function :
212+ return Strings . UseFullyQualifiedCmdletNamesFunctionError ;
213+ default :
214+ return Strings . UseFullyQualifiedCmdletNamesCommandError ;
215+ }
216+ }
217+
218+ /// <summary>
219+ /// Resolves the command info for a given name using the shared runspace.
220+ /// </summary>
221+ /// <param name="commandName">The command name to resolve.</param>
222+ /// <returns>A cached result describing the resolved command.</returns>
223+ private ResolvedCommand ResolveCommand ( string commandName )
224+ {
225+ var commandInfo = Helper . Instance . GetCommandInfo ( commandName , CommandTypes . All ) ;
226+ if ( commandInfo == null )
227+ {
228+ return new ResolvedCommand ( null , null , CommandTypes . Application ) ;
229+ }
230+
231+ if ( commandInfo . CommandType != CommandTypes . Cmdlet &&
232+ commandInfo . CommandType != CommandTypes . Function &&
233+ commandInfo . CommandType != CommandTypes . Alias )
234+ {
235+ return new ResolvedCommand ( null , null , commandInfo . CommandType ) ;
236+ }
237+
238+ var commandType = commandInfo . CommandType ;
239+
240+ string moduleName = commandInfo . ModuleName ;
241+ string resolvedName = commandInfo . Name ;
242+
243+ if ( commandInfo is AliasInfo aliasInfo )
244+ {
245+ if ( aliasInfo . ResolvedCommand == null )
246+ {
247+ return new ResolvedCommand ( null , null , commandType ) ;
248+ }
249+
250+ resolvedName = aliasInfo . ResolvedCommand . Name ;
251+ moduleName = aliasInfo . ResolvedCommand . ModuleName ;
252+ }
253+
254+ if ( string . IsNullOrEmpty ( moduleName ) || string . IsNullOrEmpty ( resolvedName ) )
255+ {
256+ return new ResolvedCommand ( null , null , commandType ) ;
257+ }
258+
259+ return new ResolvedCommand ( $ "{ moduleName } \\ { resolvedName } ", moduleName , commandType ) ;
260+ }
261+
262+ /// <summary>
263+ /// Holds the result of resolving a command name.
264+ /// </summary>
265+ private sealed class ResolvedCommand
266+ {
267+ public string FullyQualifiedName { get ; }
268+
269+ public string ModuleName { get ; }
270+
271+ public CommandTypes CommandType { get ; }
272+
273+ public ResolvedCommand ( string fullyQualifiedName , string moduleName , CommandTypes commandType )
274+ {
275+ FullyQualifiedName = fullyQualifiedName ;
276+ ModuleName = moduleName ;
277+ CommandType = commandType ;
278+ }
279+ }
280+
281+ /// <summary>
282+ /// Retrieves the localized name of this rule.
283+ /// </summary>
284+ /// <returns>The localized name of this rule</returns>
285+ public override string GetName ( )
286+ {
287+ return string . Format ( CultureInfo . CurrentCulture , Strings . NameSpaceFormat , GetSourceName ( ) , Strings . UseFullyQualifiedCmdletNamesName ) ;
288+ }
289+
290+ /// <summary>
291+ /// Retrieves the common name of this rule.
292+ /// </summary>
293+ /// <returns>The common name of this rule</returns>
294+ public override string GetCommonName ( )
295+ {
296+ return string . Format ( CultureInfo . CurrentCulture , Strings . UseFullyQualifiedCmdletNamesCommonName ) ;
297+ }
298+
299+ /// <summary>
300+ /// Retrieves the localized description of this rule.
301+ /// </summary>
302+ /// <returns>The localized description of this rule</returns>
303+ public override string GetDescription ( )
304+ {
305+ return string . Format ( CultureInfo . CurrentCulture , Strings . UseFullyQualifiedCmdletNamesDescription ) ;
306+ }
307+
308+ /// <summary>
309+ /// Retrieves the source type of this rule.
310+ /// </summary>
311+ /// <returns>The source type of this rule</returns>
312+ public override SourceType GetSourceType ( )
313+ {
314+ return SourceType . Builtin ;
315+ }
316+
317+ /// <summary>
318+ /// Retrieves the source name of this rule.
319+ /// </summary>
320+ /// <returns>The source name of this rule</returns>
321+ public override string GetSourceName ( )
322+ {
323+ return "PS" ;
324+ }
325+
326+ /// <summary>
327+ /// Retrieves the severity of this rule.
328+ /// </summary>
329+ /// <returns>The severity of this rule</returns>
330+ public override RuleSeverity GetSeverity ( )
331+ {
332+ return RuleSeverity . Warning ;
333+ }
334+ }
335+ }
0 commit comments