Skip to content

Commit 9771645

Browse files
author
Quoc Truong
committed
Merge pull request #424 from PowerShell/FixBugs
Fix bugs related variable scopes not being considered
2 parents 15e8b5f + 54f504c commit 9771645

8 files changed

+203
-14
lines changed

Engine/Helper.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ internal set
100100
/// </summary>
101101
private Dictionary<Ast, VariableAnalysis> VariableAnalysisDictionary;
102102

103+
private string[] functionScopes = new string[] { "global:", "local:", "script:", "private:" };
104+
105+
private string[] variableScopes = new string[] { "global:", "local:", "script:", "private:", "variable:", ":" };
106+
103107
#endregion
104108

105109
/// <summary>
@@ -269,6 +273,60 @@ item is TypeDefinitionAst
269273
return false;
270274
}
271275

276+
private string NameWithoutScope(string name, string[] scopes)
277+
{
278+
if (String.IsNullOrWhiteSpace(name) || scopes == null)
279+
{
280+
return name;
281+
}
282+
283+
// checks whether function name starts with scope
284+
foreach (string scope in scopes)
285+
{
286+
// trim the scope part
287+
if (name.IndexOf(scope) == 0)
288+
{
289+
return name.Substring(scope.Length);
290+
}
291+
}
292+
293+
// no scope
294+
return name;
295+
}
296+
297+
/// <summary>
298+
/// Given a function name, strip the scope of the name
299+
/// </summary>
300+
/// <param name="functionName"></param>
301+
/// <returns></returns>
302+
public string FunctionNameWithoutScope(string functionName)
303+
{
304+
return NameWithoutScope(functionName, functionScopes);
305+
}
306+
307+
/// <summary>
308+
/// Given a variable name, strip the scope
309+
/// </summary>
310+
/// <param name="variableName"></param>
311+
/// <returns></returns>
312+
public string VariableNameWithoutScope(VariablePath variablePath)
313+
{
314+
if (variablePath == null || variablePath.UserPath == null)
315+
{
316+
return null;
317+
}
318+
319+
// strip out the drive if there is one
320+
if (!string.IsNullOrWhiteSpace(variablePath.DriveName)
321+
// checks that variable starts with drivename:
322+
&& variablePath.UserPath.IndexOf(string.Concat(variablePath.DriveName, ":")) == 0)
323+
{
324+
return variablePath.UserPath.Substring(variablePath.DriveName.Length + 1);
325+
}
326+
327+
return NameWithoutScope(variablePath.UserPath, variableScopes);
328+
}
329+
272330
/// <summary>
273331
/// Given a commandast, checks whether it uses splatted variable
274332
/// </summary>

Engine/VariableAnalysis.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,13 @@ private void ProcessParameters(IEnumerable<ParameterAst> parameters)
9292
{
9393
foreach (NamedAttributeArgumentAst arg in args)
9494
{
95-
if (String.Equals(arg.ArgumentName, "mandatory", StringComparison.OrdinalIgnoreCase)
96-
&& String.Equals(arg.Argument.Extent.Text, "$true", StringComparison.OrdinalIgnoreCase))
97-
{
98-
isSwitchOrMandatory = true;
95+
if (String.Equals(arg.ArgumentName, "mandatory", StringComparison.OrdinalIgnoreCase))
96+
{
97+
// check for the case mandatory=$true and just mandatory
98+
if (arg.ExpressionOmitted || (!arg.ExpressionOmitted && String.Equals(arg.Argument.Extent.Text, "$true", StringComparison.OrdinalIgnoreCase)))
99+
{
100+
isSwitchOrMandatory = true;
101+
}
99102
}
100103
}
101104
}

Rules/AvoidReservedCharInCmdlet.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using System.Linq;
1616
using System.Management.Automation.Language;
1717
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
18+
using Microsoft.Windows.PowerShell.ScriptAnalyzer;
1819
using System.ComponentModel.Composition;
1920
using System.Globalization;
2021

@@ -43,7 +44,9 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
4344

4445
foreach (FunctionDefinitionAst funcAst in funcAsts)
4546
{
46-
if (funcAst.Name != null && funcAst.Name.Intersect(reservedChars).Count() > 0)
47+
string funcName = Helper.Instance.FunctionNameWithoutScope(funcAst.Name);
48+
49+
if (funcName != null && funcName.Intersect(reservedChars).Count() > 0)
4750
{
4851
yield return new DiagnosticRecord(string.Format(CultureInfo.CurrentCulture, Strings.ReservedCmdletCharError, funcAst.Name),
4952
funcAst.Extent, GetName(), DiagnosticSeverity.Warning, fileName);

Rules/UseApprovedVerbs.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName) {
5454

5555
foreach (FunctionDefinitionAst funcAst in funcAsts)
5656
{
57-
funcName = funcAst.Name;
57+
funcName = Helper.Instance.FunctionNameWithoutScope(funcAst.Name);
5858

5959
if (funcName != null && funcName.Contains('-'))
6060
{

Rules/UseDeclaredVarsMoreThanAssignments.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,17 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
5353

5454
if (assignmentVarAst != null)
5555
{
56-
//Ignore if variable is global or environment variable
56+
//Ignore if variable is global or environment variable or scope is function
5757
if (!Helper.Instance.IsVariableGlobalOrEnvironment(assignmentVarAst, ast)
58-
&& !assignmentVarAst.VariablePath.IsScript)
58+
&& !assignmentVarAst.VariablePath.IsScript
59+
&& !string.Equals(assignmentVarAst.VariablePath.DriveName, "function", StringComparison.OrdinalIgnoreCase))
5960
{
60-
if (!assignments.ContainsKey(assignmentVarAst.VariablePath.UserPath))
61+
62+
string variableName = Helper.Instance.VariableNameWithoutScope(assignmentVarAst.VariablePath);
63+
64+
if (!assignments.ContainsKey(variableName))
6165
{
62-
assignments.Add(assignmentVarAst.VariablePath.UserPath, assignmentAst);
66+
assignments.Add(variableName, assignmentAst);
6367
}
6468
}
6569
}
@@ -70,7 +74,7 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
7074
{
7175
foreach (VariableExpressionAst varAst in varAsts)
7276
{
73-
varKey = varAst.VariablePath.UserPath;
77+
varKey = Helper.Instance.VariableNameWithoutScope(varAst.VariablePath);
7478
inAssignment = false;
7579

7680
if (assignments.ContainsKey(varKey))

Rules/UseShouldProcessCorrectly.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
5454

5555
attributeAsts = funcDefAst.FindAll(testAst => testAst is NamedAttributeArgumentAst, true);
5656
foreach (NamedAttributeArgumentAst attributeAst in attributeAsts) {
57-
hasShouldProcessAttribute |= attributeAst.ArgumentName.Equals(supportsShouldProcess, StringComparison.OrdinalIgnoreCase) && attributeAst.Argument.Extent.Text.Equals(trueString, StringComparison.OrdinalIgnoreCase);
57+
hasShouldProcessAttribute |= ((attributeAst.ArgumentName.Equals(supportsShouldProcess, StringComparison.OrdinalIgnoreCase) && attributeAst.Argument.Extent.Text.Equals(trueString, StringComparison.OrdinalIgnoreCase))
58+
// checks for the case if the user just use the attribute without setting it to true
59+
|| (attributeAst.ArgumentName.Equals(supportsShouldProcess, StringComparison.OrdinalIgnoreCase) && attributeAst.ExpressionOmitted));
5860
}
5961

6062
memberAsts = funcDefAst.FindAll(testAst => testAst is MemberExpressionAst, true);

Tests/Rules/GoodCmdlet.ps1

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,117 @@ function Get-File
107107
}
108108
}
109109

110+
<#
111+
.Synopsis
112+
Short description
113+
.DESCRIPTION
114+
Long description
115+
.EXAMPLE
116+
Example of how to use this cmdlet
117+
.EXAMPLE
118+
Another example of how to use this cmdlet
119+
.INPUTS
120+
Inputs to this cmdlet (if any)
121+
.OUTPUTS
122+
Output from this cmdlet (if any)
123+
.NOTES
124+
General notes
125+
.COMPONENT
126+
The component this cmdlet belongs to
127+
.ROLE
128+
The role this cmdlet belongs to
129+
.FUNCTIONALITY
130+
The functionality that best describes this cmdlet
131+
#>
132+
function Get-Folder
133+
{
134+
[CmdletBinding(DefaultParameterSetName='Parameter Set 1',
135+
SupportsShouldProcess,
136+
PositionalBinding=$false,
137+
HelpUri = 'http://www.microsoft.com/',
138+
ConfirmImpact='Medium')]
139+
[Alias()]
140+
[OutputType([String], [System.Double], [Hashtable], "MyCustom.OutputType")]
141+
[OutputType("System.Int32", ParameterSetName="ID")]
142+
143+
Param
144+
(
145+
# Param1 help description
146+
[Parameter(Mandatory=$true,
147+
ValueFromPipeline=$true,
148+
ValueFromPipelineByPropertyName=$true,
149+
ValueFromRemainingArguments=$false,
150+
Position=0,
151+
ParameterSetName='Parameter Set 1')]
152+
[ValidateNotNull()]
153+
[ValidateNotNullOrEmpty()]
154+
[ValidateCount(0,5)]
155+
[ValidateSet("sun", "moon", "earth")]
156+
[Alias("p1")]
157+
$Param1,
158+
159+
# Param2 help description
160+
[Parameter(ParameterSetName='Parameter Set 1')]
161+
[AllowNull()]
162+
[AllowEmptyCollection()]
163+
[AllowEmptyString()]
164+
[ValidateScript({$true})]
165+
[ValidateRange(0,5)]
166+
[int]
167+
$Param2,
168+
169+
# Param3 help description
170+
[Parameter(ParameterSetName='Another Parameter Set')]
171+
[ValidatePattern("[a-z]*")]
172+
[ValidateLength(0,15)]
173+
[String]
174+
$Param3,
175+
[bool]
176+
$Force
177+
)
178+
179+
Begin
180+
{
181+
}
182+
Process
183+
{
184+
if ($pscmdlet.ShouldProcess("Target", "Operation"))
185+
{
186+
Write-Verbose "Write Verbose"
187+
Get-Process
188+
}
189+
190+
$a = 4.5
191+
192+
if ($true)
193+
{
194+
$a
195+
}
196+
197+
$a | Write-Warning
198+
199+
$b = @{"hash"="table"}
200+
201+
Write-Debug @b
202+
203+
[pscustomobject]@{
204+
PSTypeName = 'MyCustom.OutputType'
205+
Prop1 = 'SomeValue'
206+
Prop2 = 'OtherValue'
207+
}
208+
209+
return @{"hash"="true"}
210+
}
211+
End
212+
{
213+
if ($pscmdlet.ShouldContinue("Yes", "No")) {
214+
}
215+
[System.Void] $Param3
216+
}
217+
}
218+
110219
#Write-Verbose should not be required because this is not an advanced function
111-
function Get-SimpleFunc
220+
function global:Get-SimpleFunc
112221
{
113222

114223
}

Tests/Rules/UseDeclaredVarsMoreThanAssignmentsNoViolations.ps1

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,14 @@ $foo.property = "This also should not raise errors"
55

66
# Output field separator builtin special variable
77
$OFS = ', '
8-
[string]('apple', 'banana', 'orange')
8+
[string]('apple', 'banana', 'orange')
9+
10+
# Using scope
11+
$private:x = 42
12+
$x
13+
14+
$variable:a = 52
15+
$a
16+
17+
#function
18+
$function:prompt = [ScriptBlock]::Create($newPrompt)

0 commit comments

Comments
 (0)