Skip to content

Commit 4cda851

Browse files
author
Kapil Borle
authored
Merge pull request #652 from PowerShell/kapilmb/FixShouldProcess
Fix ShouldProcess rule
2 parents 5a6c740 + a3f8460 commit 4cda851

File tree

2 files changed

+101
-25
lines changed

2 files changed

+101
-25
lines changed

Rules/UseShouldProcessCorrectly.cs

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@ public class UseShouldProcessCorrectly : IScriptRule
3737
private FunctionReferenceDigraph funcDigraph;
3838
private List<DiagnosticRecord> diagnosticRecords;
3939
private readonly Vertex shouldProcessVertex;
40+
private readonly Vertex implicitShouldProcessVertex;
4041

4142
public UseShouldProcessCorrectly()
4243
{
4344
diagnosticRecords = new List<DiagnosticRecord>();
4445
shouldProcessVertex = new Vertex("ShouldProcess", null);
46+
implicitShouldProcessVertex = new Vertex("implicitShouldProcessVertex", null);
4547
}
4648

4749
/// <summary>
@@ -150,10 +152,12 @@ private DiagnosticRecord GetViolation(Vertex v)
150152
return null;
151153
}
152154

153-
bool callsShouldProcess = funcDigraph.IsConnected(v, shouldProcessVertex);
154155
if (DeclaresSupportsShouldProcess(fast))
155156
{
156-
if (!callsShouldProcess)
157+
bool callsShouldProcess = funcDigraph.IsConnected(v, shouldProcessVertex);
158+
bool callsCommandWithShouldProcess = funcDigraph.IsConnected(v, implicitShouldProcessVertex);
159+
if (!callsShouldProcess
160+
&& !callsCommandWithShouldProcess)
157161
{
158162
return new DiagnosticRecord(
159163
string.Format(
@@ -168,12 +172,12 @@ private DiagnosticRecord GetViolation(Vertex v)
168172
}
169173
else
170174
{
171-
if (callsShouldProcess)
175+
if (callsShouldProcessDirectly(v))
172176
{
173177
// check if upstream function declares SupportShouldProcess
174178
// if so, this might just be a helper function
175179
// do not flag this case
176-
if (UpstreamDeclaresShouldProcess(v))
180+
if (v.IsNestedFunctionDefinition && UpstreamDeclaresShouldProcess(v))
177181
{
178182
return null;
179183
}
@@ -193,6 +197,11 @@ private DiagnosticRecord GetViolation(Vertex v)
193197
return null;
194198
}
195199

200+
private bool callsShouldProcessDirectly(Vertex vertex)
201+
{
202+
return funcDigraph.GetNeighbors(vertex).Contains(shouldProcessVertex);
203+
}
204+
196205
/// <summary>
197206
/// Checks if an upstream function declares SupportsShouldProcess
198207
/// </summary>
@@ -344,10 +353,10 @@ private void CheckForSupportShouldProcess()
344353

345354
if (commandsWithSupportShouldProcess.Count > 0)
346355
{
347-
funcDigraph.AddVertex(shouldProcessVertex);
356+
funcDigraph.AddVertex(implicitShouldProcessVertex);
348357
foreach(var v in commandsWithSupportShouldProcess)
349358
{
350-
funcDigraph.AddEdge(v, shouldProcessVertex);
359+
funcDigraph.AddEdge(v, implicitShouldProcessVertex);
351360
}
352361
}
353362
}
@@ -359,10 +368,23 @@ private void CheckForSupportShouldProcess()
359368
class Vertex
360369
{
361370
public string Name {get { return name;}}
362-
public Ast Ast {get {return ast; }}
371+
public Ast Ast
372+
{
373+
get
374+
{
375+
return ast;
376+
}
377+
set
378+
{
379+
ast = value;
380+
}
381+
}
382+
383+
public bool IsNestedFunctionDefinition {get {return isNestedFunctionDefinition;}}
363384

364385
private string name;
365386
private Ast ast;
387+
private bool isNestedFunctionDefinition;
366388

367389
public Vertex()
368390
{
@@ -379,6 +401,12 @@ public Vertex (string name, Ast ast)
379401
this.ast = ast;
380402
}
381403

404+
public Vertex (String name, Ast ast, bool isNestedFunctionDefinition)
405+
: this(name, ast)
406+
{
407+
this.isNestedFunctionDefinition = isNestedFunctionDefinition;
408+
}
409+
382410
/// <summary>
383411
/// Returns string representation of a Vertex instance
384412
/// </summary>
@@ -460,11 +488,30 @@ public FunctionReferenceDigraph()
460488
/// <summary>
461489
/// Add a vertex to the graph
462490
/// </summary>
463-
public void AddVertex(Vertex name)
491+
public void AddVertex(Vertex vertex)
464492
{
465-
if (!digraph.ContainsVertex(name))
493+
bool containsVertex = false;
494+
495+
// if the graph contains a vertex with name equal to that
496+
// of the input vertex, then update the vertex's ast if the
497+
// input vertex's ast is of FunctionDefinitionAst type
498+
foreach(Vertex v in digraph.GetVertices())
499+
{
500+
if (v.Equals(vertex))
501+
{
502+
containsVertex = true;
503+
if (vertex.Ast != null
504+
&& vertex.Ast is FunctionDefinitionAst)
505+
{
506+
v.Ast = vertex.Ast;
507+
}
508+
break;
509+
}
510+
}
511+
512+
if (!containsVertex)
466513
{
467-
digraph.AddVertex(name);
514+
digraph.AddVertex(vertex);
468515
}
469516
}
470517

@@ -491,7 +538,7 @@ public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst ast
491538
return AstVisitAction.SkipChildren;
492539
}
493540

494-
var functionVertex = new Vertex (ast.Name, ast);
541+
var functionVertex = new Vertex(ast.Name, ast, IsWithinFunctionDefinition());
495542
functionVisitStack.Push(functionVertex);
496543
AddVertex(functionVertex);
497544
ast.Body.Visit(this);
@@ -549,18 +596,15 @@ public override AstVisitAction VisitInvokeMemberExpression(InvokeMemberExpressio
549596
}
550597

551598
// Suppose we find <Expression>.<Member>, we split it up and create
552-
// and edge from <Expression>-><Member>. Even though <Expression> is not
599+
// and edge only to <Member>. Even though <Expression> is not
553600
// necessarily a function, we do it because we are mainly interested in
554601
// finding connection between a function and ShouldProcess and this approach
555602
// prevents any unnecessary complexity.
556-
var exprVertex = new Vertex (expr, ast.Expression);
557603
var memberVertex = new Vertex (memberExprAst.Value, memberExprAst);
558-
AddVertex(exprVertex);
559604
AddVertex(memberVertex);
560-
AddEdge(exprVertex, memberVertex);
561605
if (IsWithinFunctionDefinition())
562606
{
563-
AddEdge(GetCurrentFunctionContext(), exprVertex);
607+
AddEdge(GetCurrentFunctionContext(), memberVertex);
564608
}
565609

566610
return AstVisitAction.Continue;
@@ -597,5 +641,10 @@ public int GetOutDegree(Vertex v)
597641
{
598642
return digraph.GetOutDegree(v);
599643
}
644+
645+
public IEnumerable<Vertex> GetNeighbors(Vertex v)
646+
{
647+
return digraph.GetNeighbors(v);
648+
}
600649
}
601650
}

Tests/Rules/UseShouldProcessCorrectly.tests.ps1

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Foo
5555
$violations.Count | Should Be 0
5656
}
5757

58-
It "finds no violation if downstream function does not declare SupportsShouldProcess" {
58+
It "finds violation if downstream function does not declare SupportsShouldProcess" {
5959
$scriptDef = @'
6060
function Foo
6161
{
@@ -80,10 +80,10 @@ function Bar
8080
Foo
8181
'@
8282
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDef -IncludeRule PSShouldProcess
83-
$violations.Count | Should Be 0
83+
$violations.Count | Should Be 1
8484
}
8585

86-
It "finds no violation for 2 level downstream calls" {
86+
It "finds violation for 2 level downstream calls" {
8787
$scriptDef = @'
8888
function Foo
8989
{
@@ -113,11 +113,11 @@ function Bar
113113
Foo
114114
'@
115115
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDef -IncludeRule PSShouldProcess
116-
$violations.Count | Should Be 0
116+
$violations.Count | Should Be 1
117117
}
118118
}
119119

120-
Context "When downstream function is defined locally in a function scope" {
120+
Context "When nested function definition calls ShouldProcess" {
121121
It "finds no violation" {
122122
$scriptDef = @'
123123
function Foo
@@ -142,8 +142,8 @@ function Foo
142142
}
143143
}
144144

145-
Context "When a builtin command with SupportsShouldProcess is called" {
146-
It "finds no violation for a cmdlet" {
145+
Context "When a builtin command that supports ShouldProcess is called" {
146+
It "finds no violation when caller declares SupportsShouldProcess and callee is a cmdlet with ShouldProcess" {
147147
$scriptDef = @'
148148
function Remove-Foo {
149149
[CmdletBinding(SupportsShouldProcess)]
@@ -158,7 +158,21 @@ function Remove-Foo {
158158
$violations.Count | Should Be 0
159159
}
160160

161-
It "finds no violation for a function" {
161+
It "finds no violation when caller does not declare SupportsShouldProcess and callee is a cmdlet with ShouldProcess" {
162+
$scriptDef = @'
163+
function Remove-Foo {
164+
Param(
165+
[string] $Path
166+
)
167+
Write-Verbose "Removing $($path)"
168+
Remove-Item -Path $Path
169+
}
170+
'@
171+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDef -IncludeRule PSShouldProcess
172+
$violations.Count | Should Be 0
173+
}
174+
175+
It "finds no violation when caller declares SupportsShouldProcess and callee is a function with ShouldProcess" {
162176
$scriptDef = @'
163177
function Install-Foo {
164178
[CmdletBinding(SupportsShouldProcess)]
@@ -167,6 +181,19 @@ function Install-Foo {
167181
)
168182
Install-Module $ModuleName
169183
}
184+
'@
185+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDef -IncludeRule PSShouldProcess
186+
$violations.Count | Should Be 0
187+
}
188+
189+
It "finds no violation when caller does not declare SupportsShouldProcess and callee is a function with ShouldProcess" {
190+
$scriptDef = @'
191+
function Install-Foo {
192+
Param(
193+
[string] $ModuleName
194+
)
195+
Install-Module $ModuleName
196+
}
170197
'@
171198
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDef -IncludeRule PSShouldProcess
172199
$violations.Count | Should Be 0
@@ -175,7 +202,7 @@ function Install-Foo {
175202
It "finds no violation for a function with self reference" {
176203
$scriptDef = @'
177204
function Install-ModuleWithDeps {
178-
[CmdletBinding(SupportsShouldProcess)]
205+
[CmdletBinding(SupportsShouldProcess)]
179206
Param(
180207
[Parameter(ValueFromPipeline)]
181208
[string] $ModuleName

0 commit comments

Comments
 (0)