Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rule for missing process block when command supports the pipeline #1373

Merged
merged 23 commits into from
Dec 9, 2019
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1ef888d
Add rule for missing process block #1368
mattmcnabb Nov 16, 2019
1ad72e7
Fix build by regenerating strongly typed files by running '.\New-Stro…
bergmeister Nov 16, 2019
794366b
Update strongly typed resources via Visual Studio
bergmeister Nov 16, 2019
86673bf
minimise diff in resx file
bergmeister Nov 16, 2019
dd42829
refactor UseProcessBlockForPipelineCommands rule
mattmcnabb Nov 16, 2019
32916c2
add tests and docs for UseProcessBlockForPipelineCommands rule
mattmcnabb Nov 16, 2019
fe2c8e7
Fix logic non-pipeline function and add test case
mattmcnabb Nov 17, 2019
585a735
fixing some test failures
mattmcnabb Nov 17, 2019
b499d9e
incrememt number of expected rules
mattmcnabb Nov 17, 2019
2df5e70
fix style suggestions for PR #1373
mattmcnabb Nov 19, 2019
fbe8bc4
fix rule readme.md
mattmcnabb Nov 19, 2019
5921add
clean diff for Strings.resx to avoid future merge conflicts
Nov 22, 2019
79aaf0f
Fix last 2 rule documentation test failures by fixing markdown link
Nov 22, 2019
850b457
Update Rules/Strings.resx
mattmcnabb Nov 22, 2019
61220e7
Update Rules/Strings.resx
mattmcnabb Nov 22, 2019
fcae260
Update Rules/Strings.resx
mattmcnabb Nov 22, 2019
504fdd6
empty-commit to re-trigger CI due to sporadic failure
Nov 22, 2019
a593ff7
Update Rules/Strings.resx
mattmcnabb Nov 25, 2019
4a358a0
Update Rules/Strings.resx
mattmcnabb Nov 25, 2019
707f02d
corrections to use process block rule and tests - candidate for merge
mattmcnabb Dec 4, 2019
c7a2531
More style fixes for #1373
mattmcnabb Dec 4, 2019
a7ed122
update localized strings
mattmcnabb Dec 5, 2019
a28b4d2
replace implicitly typed vars
mattmcnabb Dec 5, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions RuleDocumentation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
|[UseDeclaredVarsMoreThanAssignments](./UseDeclaredVarsMoreThanAssignments.md) | Warning | |
|[UseLiteralInitializerForHashtable](./UseLiteralInitializerForHashtable.md) | Warning | |
|[UseOutputTypeCorrectly](./UseOutputTypeCorrectly.md) | Information | |
|[UseProcessBlockForPipelineCommands](./UseProcessBlockForPipelineCommands.md) | Warning | |
|[UsePSCredentialType](./UsePSCredentialType.md) | Warning | |
|[UseShouldProcessForStateChangingFunctions](./UseShouldProcessForStateChangingFunctions.md) | Warning | |
|[UseSingularNouns<sup>*</sup>](./UseSingularNouns.md) | Warning | |
Expand Down
62 changes: 62 additions & 0 deletions RuleDocumentation/UseProcessBlockForPipelineCommands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# UseProcessBlockForPipelineCommands

**Severity Level: Warning**

## Description

Functions that support pipeline input should always handle parameter input in a process block. Unexpected behavior can result if input is handled directly in the body of a function where parameters declare pipeline support.

## Example

### Wrong

``` PowerShell
Function Get-Number
{
[CmdletBinding()]
Param(
[Parameter(ValueFromPipeline)]
[int]
$Number
)

$Number
}
```

#### Result

```
PS C:\> 1..5 | Broken
mattmcnabb marked this conversation as resolved.
Show resolved Hide resolved
5
```

### Correct

``` PowerShell
Function Get-Number
{
[CmdletBinding()]
Param(
[Parameter(ValueFromPipeline)]
[int]
$Number
)

process
{
$Number
}
}
```

#### Result

```
PS C:\> 1..5 | Correct
1
2
3
4
5
```
129 changes: 73 additions & 56 deletions Rules/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion Rules/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1080,4 +1080,16 @@
<data name="UseCorrectCasingName" xml:space="preserve">
<value>UseCorrectCasing</value>
</data>
</root>
<data name="UseProcessBlockForPipelineCommandsCommonName" xml:space="preserve">
<value>Use process block for pipeline commands.</value>
mattmcnabb marked this conversation as resolved.
Show resolved Hide resolved
</data>
<data name="UseProcessBlockForPipelineCommandsDescription" xml:space="preserve">
<value>If a parameter accepts input via the pipeline, the function should use at least a process block to correctly support pipeline features.</value>
mattmcnabb marked this conversation as resolved.
Show resolved Hide resolved
</data>
<data name="UseProcessBlockForPipelineCommandsError" xml:space="preserve">
<value>Commands that support pipeline input should return output in a process block.</value>
mattmcnabb marked this conversation as resolved.
Show resolved Hide resolved
</data>
<data name="UseProcessBlockForPipelineCommandsName" xml:space="preserve">
<value>UseProcessBlockForPipelineCommands</value>
</data>
</root>
96 changes: 96 additions & 0 deletions Rules/UseProcessBlockForPipelineCommands.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Management.Automation.Language;
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
#if !CORECLR
using System.ComponentModel.Composition;
#endif
using System.Globalization;

namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
{
#if !CORECLR
[Export(typeof(IScriptRule))]
#endif
public class UseProcessBlockForPipelineCommands : IScriptRule
{
public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
{
if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage);

IEnumerable<Ast> functionAsts = ast.FindAll(testAst => testAst is FunctionDefinitionAst, true);

foreach (FunctionDefinitionAst funcAst in functionAsts)
{
if
(
funcAst.Body.ParamBlock == null
|| funcAst.Body.ParamBlock.Attributes == null
|| funcAst.Body.ParamBlock.Parameters == null
|| funcAst.Body.ProcessBlock != null
)
bergmeister marked this conversation as resolved.
Show resolved Hide resolved
{ continue; }

foreach (var paramAst in funcAst.Body.ParamBlock.Parameters)
{
foreach (var paramAstAttribute in paramAst.Attributes)
{
if (!(paramAstAttribute is AttributeAst)) { continue; }
mattmcnabb marked this conversation as resolved.
Show resolved Hide resolved

var namedArguments = (paramAstAttribute as AttributeAst).NamedArguments;
if (namedArguments == null) { continue; }

foreach (NamedAttributeArgumentAst namedArgument in namedArguments)
{
if
(
!String.Equals(namedArgument.ArgumentName, "valuefrompipeline", StringComparison.OrdinalIgnoreCase)
mattmcnabb marked this conversation as resolved.
Show resolved Hide resolved
&& !String.Equals(namedArgument.ArgumentName, "valuefrompipelinebypropertyname", StringComparison.OrdinalIgnoreCase)
mattmcnabb marked this conversation as resolved.
Show resolved Hide resolved
)
{ continue; }

yield return new DiagnosticRecord(
string.Format(CultureInfo.CurrentCulture, Strings.UseProcessBlockForPipelineCommandsError, paramAst.Name.VariablePath.UserPath),
paramAst.Name.Extent,
GetName(),
DiagnosticSeverity.Warning,
fileName,
paramAst.Name.VariablePath.UserPath
);
}
}
}
}
}

public string GetName()
{
return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), Strings.UseProcessBlockForPipelineCommandsName);
}

public string GetCommonName()
{
return string.Format(CultureInfo.CurrentCulture, Strings.UseProcessBlockForPipelineCommandsCommonName);
}

public string GetDescription()
{
return string.Format(CultureInfo.CurrentCulture, Strings.UseProcessBlockForPipelineCommandsDescription);
}

public SourceType GetSourceType()
{
return SourceType.Builtin;
}

public RuleSeverity GetSeverity()
{
return RuleSeverity.Warning;
}

public string GetSourceName()
{
return string.Format(CultureInfo.CurrentCulture, Strings.SourceName);
}
}
}
2 changes: 1 addition & 1 deletion Tests/Engine/GetScriptAnalyzerRule.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Describe "Test Name parameters" {

It "get Rules with no parameters supplied" {
$defaultRules = Get-ScriptAnalyzerRule
$expectedNumRules = 60
$expectedNumRules = 61
if ((Test-PSEditionCoreClr) -or (Test-PSVersionV3) -or (Test-PSVersionV4))
{
# for PSv3 PSAvoidGlobalAliases is not shipped because
Expand Down
37 changes: 37 additions & 0 deletions Tests/Rules/UseProcessBlockForPipelineCommands.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
Describe "UseProcessBlockForPipelineCommands" {
BeforeAll {
$RuleName = 'PSUseProcessBlockForPipelineCommands'
$NoProcessBlock = 'function BadFunc1 { [CmdletBinding()] param ([Parameter(ValueFromPipeline)]$Param1) }'
$NoProcessBlockByPropertyName = 'function $BadFunc2 { [CmdletBinding()] param ([Parameter(ValueFromPipelineByPropertyName)]$Param1) }'
$HasProcessBlock = 'function GoodFunc1 { [CmdletBinding()] param ([Parameter(ValueFromPipeline)]$Param1) process { } }'
$HasProcessBlockByPropertyName = 'function GoodFunc2 { [CmdletBinding()] param ([Parameter(ValueFromPipelineByPropertyName)]$Param1) process { } }'
$NoAttribute = 'function GoodFunc3 { [CmdletBinding()] param ([Parameter()]$Param1) }'
$HasTypeDeclaration = 'function GoodFunc3 { [CmdletBinding()] param ([Parameter()][string]$Param1) }'
}

Context "When there are violations" {
$Cases = @(
@{ScriptDefinition = $NoProcessBlock; Name = "NoProcessBlock"}
@{ScriptDefinition = $NoProcessBlockByPropertyName; Name = "NoProcessBlockByPropertyName"}
)
It "has 1 violation for function <Name>" {
param ($ScriptDefinition)

Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName | Should Not BeNullOrEmpty
mattmcnabb marked this conversation as resolved.
Show resolved Hide resolved
} -TestCases $Cases
}

Context "When there are no violations" {
$Cases = @(
@{ScriptDefinition = $HasProcessBlock; Name = "HasProcessBlock" }
@{ScriptDefinition = $HasProcessBlockByPropertyName; Name = "HasProcessBlockByPropertyName" }
@{ScriptDefinition = $NoAttribute; Name = "NoAttribute" }
@{ScriptDefinition = $HasTypeDeclaration; Name = "HasTypeDeclaration"}
)
It "has no violations for function <Name>" {
param ($ScriptDefinition)

Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName | Should BeNullOrEmpty
mattmcnabb marked this conversation as resolved.
Show resolved Hide resolved
} -TestCases $Cases
}
}