Skip to content

Commit a503078

Browse files
authored
Warn when 'Get-' prefix was omitted as part of the PSAvoidUsingCmdletAliases rule. (#927)
* Warn when 'Get-' prefix was omitted. Rename some variables to make the code clearer * update docs * do not warn for get- completion when the command exists natively (e.g. 'date' or service' on Unix systems) * add test that there are no warnings about get- completion when native command takes precedence. Tested on Ubuntu 16.04 LTS * use `date` for test because on some MacOs distros, service is not available * Fix GetCommandInfoInternal method to actually use the CommandTypes argument However, its calling method GetCommandInfo was relying on this bug (or its callers), therefore the method was declared legacy in order to not break existing behaviour * change test syntax as requested in PR
1 parent 7fd10cd commit a503078

9 files changed

+109
-32
lines changed

Engine/Helper.cs

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ public HashSet<string> GetExportedFunction(Ast ast)
399399
IEnumerable<Ast> cmdAsts = ast.FindAll(item => item is CommandAst
400400
&& exportFunctionsCmdlet.Contains((item as CommandAst).GetCommandName(), StringComparer.OrdinalIgnoreCase), true);
401401

402-
CommandInfo exportMM = Helper.Instance.GetCommandInfo("export-modulemember", CommandTypes.Cmdlet);
402+
CommandInfo exportMM = Helper.Instance.GetCommandInfoLegacy("export-modulemember", CommandTypes.Cmdlet);
403403

404404
// switch parameters
405405
IEnumerable<ParameterMetadata> switchParams = (exportMM != null) ? exportMM.Parameters.Values.Where<ParameterMetadata>(pm => pm.SwitchParameter) : Enumerable.Empty<ParameterMetadata>();
@@ -614,7 +614,7 @@ public bool PositionalParameterUsed(CommandAst cmdAst, bool moreThanThreePositio
614614
return false;
615615
}
616616

617-
var commandInfo = GetCommandInfo(cmdAst.GetCommandName());
617+
var commandInfo = GetCommandInfoLegacy(cmdAst.GetCommandName());
618618
if (commandInfo == null || (commandInfo.CommandType != System.Management.Automation.CommandTypes.Cmdlet))
619619
{
620620
return false;
@@ -694,26 +694,38 @@ public bool PositionalParameterUsed(CommandAst cmdAst, bool moreThanThreePositio
694694
/// Get a CommandInfo object of the given command name
695695
/// </summary>
696696
/// <returns>Returns null if command does not exists</returns>
697-
private CommandInfo GetCommandInfoInternal(string cmdName, CommandTypes? commandType = null)
697+
private CommandInfo GetCommandInfoInternal(string cmdName, CommandTypes? commandType)
698698
{
699699
using (var ps = System.Management.Automation.PowerShell.Create())
700700
{
701-
var cmdInfo = ps.AddCommand("Get-Command")
702-
.AddArgument(cmdName)
703-
.AddParameter("ErrorAction", "SilentlyContinue")
704-
.Invoke<CommandInfo>()
705-
.FirstOrDefault();
706-
return cmdInfo;
701+
var psCommand = ps.AddCommand("Get-Command")
702+
.AddParameter("Name", cmdName)
703+
.AddParameter("ErrorAction", "SilentlyContinue");
704+
705+
if(commandType!=null)
706+
{
707+
psCommand.AddParameter("CommandType", commandType);
708+
}
709+
710+
var commandInfo = psCommand.Invoke<CommandInfo>()
711+
.FirstOrDefault();
712+
713+
return commandInfo;
707714
}
708715
}
709716

710717
/// <summary>
711-
/// Given a command's name, checks whether it exists
718+
719+
/// Legacy method, new callers should use <see cref="GetCommandInfo"/> instead.
720+
/// Given a command's name, checks whether it exists. It does not use the passed in CommandTypes parameter, which is a bug.
721+
/// But existing method callers are already depending on this behaviour and therefore this could not be simply fixed.
722+
/// It also populates the commandInfoCache which can have side effects in some cases.
712723
/// </summary>
713-
/// <param name="name"></param>
714-
/// <param name="commandType"></param>
724+
/// <param name="name">Command Name.</param>
725+
/// <param name="commandType">Not being used.</param>
715726
/// <returns></returns>
716-
public CommandInfo GetCommandInfo(string name, CommandTypes? commandType = null)
727+
[Obsolete]
728+
public CommandInfo GetCommandInfoLegacy(string name, CommandTypes? commandType = null)
717729
{
718730
if (string.IsNullOrWhiteSpace(name))
719731
{
@@ -740,6 +752,32 @@ public CommandInfo GetCommandInfo(string name, CommandTypes? commandType = null)
740752
}
741753
}
742754

755+
/// <summary>
756+
/// Given a command's name, checks whether it exists.
757+
/// </summary>
758+
/// <param name="name"></param>
759+
/// <param name="commandType"></param>
760+
/// <returns></returns>
761+
public CommandInfo GetCommandInfo(string name, CommandTypes? commandType = null)
762+
{
763+
if (string.IsNullOrWhiteSpace(name))
764+
{
765+
return null;
766+
}
767+
768+
lock (getCommandLock)
769+
{
770+
if (commandInfoCache.ContainsKey(name))
771+
{
772+
return commandInfoCache[name];
773+
}
774+
775+
var commandInfo = GetCommandInfoInternal(name, commandType);
776+
777+
return commandInfo;
778+
}
779+
}
780+
743781
/// <summary>
744782
/// Returns the get, set and test targetresource dsc function
745783
/// </summary>

RuleDocumentation/AvoidUsingCmdletAliases.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
## Description
66

77
An alias is an alternate name or nickname for a CMDLet or for a command element, such as a function, script, file, or executable file.
8-
You can use the alias instead of the command name in any Windows PowerShell commands.
8+
You can use the alias instead of the command name in any Windows PowerShell commands. There are also implicit aliases: When PowerShell cannot find the cmdlet name, it will try to append `Get-` to the command as a last resort before, therefore e.g. `verb` will excute `Get-Verb`.
99

1010
Every PowerShell author learns the actual command names, but different authors learn and use different aliases. Aliases can make code difficult to read, understand and
1111
impact availability.

Rules/AvoidAlias.cs

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#endif
1111
using System.Globalization;
1212
using System.Linq;
13+
using System.Management.Automation;
1314

1415
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
1516
{
@@ -95,38 +96,54 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
9596
IEnumerable<Ast> foundAsts = ast.FindAll(testAst => testAst is CommandAst, true);
9697

9798
// Iterates all CommandAsts and check the command name.
98-
foreach (Ast foundAst in foundAsts)
99+
foreach (CommandAst cmdAst in foundAsts)
99100
{
100-
CommandAst cmdAst = (CommandAst)foundAst;
101-
102101
// Check if the command ast should be ignored
103102
if (IgnoreCommandast(cmdAst))
104103
{
105104
continue;
106105
}
107106

108-
string aliasName = cmdAst.GetCommandName();
107+
string commandName = cmdAst.GetCommandName();
109108

110109
// Handles the exception caused by commands like, {& $PLINK $args 2> $TempErrorFile}.
111110
// You can also review the remark section in following document,
112111
// MSDN: CommandAst.GetCommandName Method
113-
if (aliasName == null
114-
|| whiteList.Contains<string>(aliasName, StringComparer.OrdinalIgnoreCase))
112+
if (commandName == null
113+
|| whiteList.Contains<string>(commandName, StringComparer.OrdinalIgnoreCase))
115114
{
116115
continue;
117116
}
118117

119-
string cmdletName = Helper.Instance.GetCmdletNameFromAlias(aliasName);
120-
if (!String.IsNullOrEmpty(cmdletName))
118+
string cmdletNameIfCommandNameWasAlias = Helper.Instance.GetCmdletNameFromAlias(commandName);
119+
if (!String.IsNullOrEmpty(cmdletNameIfCommandNameWasAlias))
121120
{
122121
yield return new DiagnosticRecord(
123-
string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingCmdletAliasesError, aliasName, cmdletName),
122+
string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingCmdletAliasesError, commandName, cmdletNameIfCommandNameWasAlias),
124123
GetCommandExtent(cmdAst),
125124
GetName(),
126125
DiagnosticSeverity.Warning,
127126
fileName,
128-
aliasName,
129-
suggestedCorrections: GetCorrectionExtent(cmdAst, cmdletName));
127+
commandName,
128+
suggestedCorrections: GetCorrectionExtent(cmdAst, cmdletNameIfCommandNameWasAlias));
129+
}
130+
131+
var isNativeCommand = Helper.Instance.GetCommandInfo(commandName, CommandTypes.Application | CommandTypes.ExternalScript) != null;
132+
if (!isNativeCommand)
133+
{
134+
var commdNameWithGetPrefix = $"Get-{commandName}";
135+
var cmdletNameIfCommandWasMissingGetPrefix = Helper.Instance.GetCommandInfo($"Get-{commandName}");
136+
if (cmdletNameIfCommandWasMissingGetPrefix != null)
137+
{
138+
yield return new DiagnosticRecord(
139+
string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingCmdletAliasesMissingGetPrefixError, commandName, commdNameWithGetPrefix),
140+
GetCommandExtent(cmdAst),
141+
GetName(),
142+
DiagnosticSeverity.Warning,
143+
fileName,
144+
commandName,
145+
suggestedCorrections: GetCorrectionExtent(cmdAst, commdNameWithGetPrefix));
146+
}
130147
}
131148
}
132149
}

Rules/AvoidPositionalParameters.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
3939
// MSDN: CommandAst.GetCommandName Method
4040
if (cmdAst.GetCommandName() == null) continue;
4141

42-
if (Helper.Instance.GetCommandInfo(cmdAst.GetCommandName()) != null
42+
if (Helper.Instance.GetCommandInfoLegacy(cmdAst.GetCommandName()) != null
4343
&& Helper.Instance.PositionalParameterUsed(cmdAst, true))
4444
{
4545
PipelineAst parent = cmdAst.Parent as PipelineAst;

Rules/Strings.Designer.cs

Lines changed: 11 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Rules/Strings.resx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,10 @@
118118
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
119119
</resheader>
120120
<data name="AvoidUsingCmdletAliasesDescription" xml:space="preserve">
121-
<value>An alias is an alternate name or nickname for a cmdlet or for a command element, such as a function, script, file, or executable file. But when writing scripts that will potentially need to be maintained over time, either by the original author or another Windows PowerShell scripter, please consider using full cmdlet name instead of alias. Aliases can introduce these problems, readability, understandability and availability.</value>
121+
<value>An alias is an alternate name or nickname for a cmdlet or for a command element, such as a function, script, file, or executable file. An implicit alias is also the omission of the 'Get-' prefix for commands with this prefix. But when writing scripts that will potentially need to be maintained over time, either by the original author or another Windows PowerShell scripter, please consider using full cmdlet name instead of alias. Aliases can introduce these problems, readability, understandability and availability.</value>
122122
</data>
123123
<data name="AvoidUsingCmdletAliasesCommonName" xml:space="preserve">
124-
<value>Avoid Using Cmdlet Aliases</value>
124+
<value>Avoid Using Cmdlet Aliases or omitting the 'Get-' prefix.</value>
125125
</data>
126126
<data name="AvoidUsingEmptyCatchBlockDescription" xml:space="preserve">
127127
<value>Empty catch blocks are considered poor design decisions because if an error occurs in the try block, this error is simply swallowed and not acted upon. While this does not inherently lead to bad things. It can and this should be avoided if possible. To fix a violation of this rule, using Write-Error or throw statements in catch blocks.</value>
@@ -1005,4 +1005,7 @@
10051005
<data name="AvoidAssignmentToAutomaticVariableName" xml:space="preserve">
10061006
<value>AvoidAssignmentToAutomaticVariable</value>
10071007
</data>
1008+
<data name="AvoidUsingCmdletAliasesMissingGetPrefixError" xml:space="preserve">
1009+
<value>'{0}' is implicitly aliasing '{1}' because it is missing the 'Get-' prefix. This can introduce possible problems and make scripts hard to maintain. Please consider changing command to its full name.</value>
1010+
</data>
10081011
</root>

Rules/UseCmdletCorrectly.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ private bool MandatoryParameterExists(CommandAst cmdAst)
7979

8080
#region Compares parameter list and mandatory parameter list.
8181

82-
cmdInfo = Helper.Instance.GetCommandInfo(cmdAst.GetCommandName());
82+
cmdInfo = Helper.Instance.GetCommandInfoLegacy(cmdAst.GetCommandName());
8383
if (cmdInfo == null || (cmdInfo.CommandType != System.Management.Automation.CommandTypes.Cmdlet))
8484
{
8585
return true;

Rules/UseShouldProcessCorrectly.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ private bool SupportsShouldProcess(string cmdName)
299299
return false;
300300
}
301301

302-
var cmdInfo = Helper.Instance.GetCommandInfo(cmdName);
302+
var cmdInfo = Helper.Instance.GetCommandInfoLegacy(cmdName);
303303
if (cmdInfo == null)
304304
{
305305
return false;

Tests/Rules/AvoidUsingAlias.tests.ps1

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ Describe "AvoidUsingAlias" {
2626
Test-CorrectionExtent $violationFilepath $violations[1] 1 'cls' 'Clear-Host'
2727
$violations[1].SuggestedCorrections[0].Description | Should -Be 'Replace cls with Clear-Host'
2828
}
29+
30+
It "warns when 'Get-' prefix was omitted" {
31+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition 'verb' | Where-Object { $_.RuleName -eq $violationName }
32+
$violations.Count | Should -Be 1
33+
}
2934
}
3035

3136
Context "Violation Extent" {
@@ -95,5 +100,10 @@ Configuration MyDscConfiguration {
95100
$violations = Invoke-ScriptAnalyzer -ScriptDefinition "CD" -Settings $settings -IncludeRule $violationName
96101
$violations.Count | Should -Be 0
97102
}
103+
104+
It "do not warn when about Get-* completed cmdlets when the command exists natively on Unix platforms" -skip:(-not ($IsLinux -or $IsMacOS)) {
105+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition 'date' | Where-Object { $_.RuleName -eq $violationName }
106+
$violations.Count | Should -Be 0
107+
}
98108
}
99109
}

0 commit comments

Comments
 (0)