Skip to content

Commit 7cdaee3

Browse files
Copilotbergmeister
andauthored
Add ValidateRange boundary type mismatch rule
Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com>
1 parent 4fee79e commit 7cdaee3

5 files changed

Lines changed: 239 additions & 0 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Globalization;
7+
using System.Linq;
8+
using System.Management.Automation.Language;
9+
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
10+
#if !CORECLR
11+
using System.ComponentModel.Composition;
12+
#endif
13+
14+
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
15+
{
16+
/// <summary>
17+
/// AvoidValidateRangeTypeMismatch: Detect integer ValidateRange bounds on floating-point and decimal parameters.
18+
/// </summary>
19+
#if !CORECLR
20+
[Export(typeof(IScriptRule))]
21+
#endif
22+
public sealed class AvoidValidateRangeTypeMismatch : IScriptRule
23+
{
24+
private static readonly HashSet<string> FloatingPointAndDecimalTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
25+
{
26+
"double", "System.Double",
27+
"single", "float", "System.Single",
28+
"decimal", "System.Decimal"
29+
};
30+
31+
private static readonly HashSet<string> IntegralTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
32+
{
33+
"byte", "System.Byte",
34+
"sbyte", "System.SByte",
35+
"short", "System.Int16",
36+
"ushort", "System.UInt16",
37+
"int", "System.Int32",
38+
"uint", "System.UInt32",
39+
"long", "System.Int64",
40+
"ulong", "System.UInt64"
41+
};
42+
43+
public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
44+
{
45+
if (ast is null)
46+
{
47+
throw new ArgumentNullException(Strings.NullAstErrorMessage);
48+
}
49+
50+
IEnumerable<Ast> parameterAsts = ast.FindAll(testAst => testAst is ParameterAst, searchNestedScriptBlocks: true);
51+
foreach (ParameterAst parameterAst in parameterAsts)
52+
{
53+
TypeConstraintAst typeConstraint = parameterAst.Attributes.OfType<TypeConstraintAst>().FirstOrDefault();
54+
if (typeConstraint == null || !FloatingPointAndDecimalTypes.Contains(typeConstraint.TypeName.FullName))
55+
{
56+
continue;
57+
}
58+
59+
foreach (AttributeAst attributeAst in parameterAst.Attributes.OfType<AttributeAst>())
60+
{
61+
if (!IsValidateRangeAttribute(attributeAst)
62+
|| attributeAst.PositionalArguments.Count < 2
63+
|| !attributeAst.PositionalArguments.Take(2).Any(IsIntegralBoundary))
64+
{
65+
continue;
66+
}
67+
68+
yield return new DiagnosticRecord(
69+
String.Format(CultureInfo.CurrentCulture, Strings.AvoidValidateRangeTypeMismatchError, parameterAst.Name, typeConstraint.TypeName.FullName),
70+
attributeAst.Extent,
71+
GetName(),
72+
DiagnosticSeverity.Warning,
73+
fileName);
74+
}
75+
}
76+
}
77+
78+
private static bool IsIntegralBoundary(ExpressionAst expressionAst)
79+
{
80+
if (expressionAst is ConstantExpressionAst constantExpressionAst)
81+
{
82+
return constantExpressionAst.Value != null
83+
&& IntegralTypes.Contains(constantExpressionAst.Value.GetType().FullName);
84+
}
85+
86+
if (expressionAst is ConvertExpressionAst convertExpressionAst)
87+
{
88+
return IntegralTypes.Contains(convertExpressionAst.Type.TypeName.FullName)
89+
&& IsIntegralBoundary(convertExpressionAst.Child);
90+
}
91+
92+
if (expressionAst is UnaryExpressionAst unaryExpressionAst
93+
&& (unaryExpressionAst.TokenKind == TokenKind.Minus || unaryExpressionAst.TokenKind == TokenKind.Plus))
94+
{
95+
return IsIntegralBoundary(unaryExpressionAst.Child);
96+
}
97+
98+
return false;
99+
}
100+
101+
private static bool IsValidateRangeAttribute(AttributeAst attributeAst)
102+
{
103+
string attributeName = attributeAst.TypeName.FullName;
104+
int lastDot = attributeName.LastIndexOf('.');
105+
string simpleName = lastDot < 0 ? attributeName : attributeName.Substring(lastDot + 1);
106+
return String.Equals(simpleName, "ValidateRange", StringComparison.OrdinalIgnoreCase)
107+
|| String.Equals(simpleName, "ValidateRangeAttribute", StringComparison.OrdinalIgnoreCase);
108+
}
109+
110+
public string GetName()
111+
{
112+
return String.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), Strings.AvoidValidateRangeTypeMismatchName);
113+
}
114+
115+
public string GetCommonName()
116+
{
117+
return Strings.AvoidValidateRangeTypeMismatchCommonName;
118+
}
119+
120+
public string GetDescription()
121+
{
122+
return Strings.AvoidValidateRangeTypeMismatchDescription;
123+
}
124+
125+
public SourceType GetSourceType()
126+
{
127+
return SourceType.Builtin;
128+
}
129+
130+
public RuleSeverity GetSeverity()
131+
{
132+
return RuleSeverity.Warning;
133+
}
134+
135+
public string GetSourceName()
136+
{
137+
return String.Format(CultureInfo.CurrentCulture, Strings.SourceName);
138+
}
139+
}
140+
}

‎Rules/Strings.resx‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,6 +1221,18 @@
12211221
<data name="AvoidMultipleTypeAttributesName" xml:space="preserve">
12221222
<value>AvoidMultipleTypeAttributes</value>
12231223
</data>
1224+
<data name="AvoidValidateRangeTypeMismatchCommonName" xml:space="preserve">
1225+
<value>Avoid mismatched ValidateRange boundary types.</value>
1226+
</data>
1227+
<data name="AvoidValidateRangeTypeMismatchDescription" xml:space="preserve">
1228+
<value>Integer ValidateRange boundaries can allow fractional values for floating-point and decimal parameters. Use floating-point or decimal boundaries that match the parameter type.</value>
1229+
</data>
1230+
<data name="AvoidValidateRangeTypeMismatchError" xml:space="preserve">
1231+
<value>ValidateRange boundaries should match the type of parameter '{0}' ({1}) to correctly restrict fractional values.</value>
1232+
</data>
1233+
<data name="AvoidValidateRangeTypeMismatchName" xml:space="preserve">
1234+
<value>AvoidValidateRangeTypeMismatch</value>
1235+
</data>
12241236
<data name="AvoidUsingBrokenHashAlgorithmsCommonName" xml:space="preserve">
12251237
<value>Avoid Using Broken Hash Algorithms</value>
12261238
</data>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
Describe 'AvoidValidateRangeTypeMismatch' {
5+
BeforeAll {
6+
$ruleName = 'PSAvoidValidateRangeTypeMismatch'
7+
}
8+
9+
It 'reports integral boundaries for floating-point and decimal parameter types' {
10+
$scripts = @(
11+
'function Test { param([double][ValidateRange(1, 2)]$Value) }'
12+
'function Test { param([single][ValidateRange(-1, 2)]$Value) }'
13+
'function Test { param([decimal][ValidateRange(1, 2.0)]$Value) }'
14+
'function Test { param([double][System.Management.Automation.ValidateRangeAttribute(1, 2)]$Value) }'
15+
)
16+
17+
foreach ($scriptDefinition in $scripts) {
18+
$diagnostics = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule $ruleName
19+
$diagnostics.Count | Should -Be 1
20+
$diagnostics[0].Extent.Text | Should -Match 'ValidateRange'
21+
}
22+
}
23+
24+
It 'ignores matching floating-point boundaries and non-floating-point parameters' {
25+
$scripts = @(
26+
'function Test { param([double][ValidateRange(1.0, 2.0)]$Value) }'
27+
'function Test { param([single][ValidateRange(1.0, 2.0)]$Value) }'
28+
'function Test { param([decimal][ValidateRange(1.0, 2.0)]$Value) }'
29+
'function Test { param([int][ValidateRange(1, 2)]$Value) }'
30+
)
31+
32+
foreach ($scriptDefinition in $scripts) {
33+
Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule $ruleName | Should -BeNullOrEmpty
34+
}
35+
}
36+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
description: Detect integer ValidateRange boundaries on floating-point and decimal parameters
3+
ms.topic: reference
4+
title: AvoidValidateRangeTypeMismatch
5+
---
6+
# AvoidValidateRangeTypeMismatch
7+
8+
**Severity Level: Warning**
9+
10+
**Default state: Always enabled**
11+
12+
## Description
13+
14+
Integer boundaries in a `ValidateRange` attribute can select integer validation behavior for
15+
floating-point and decimal parameters. This can allow fractional values outside the declared range.
16+
Use boundaries with the same numeric type as the parameter.
17+
18+
## Example
19+
20+
### Noncompliant
21+
22+
```powershell
23+
function Set-Percentage
24+
{
25+
param([double][ValidateRange(0, 100)]$Value)
26+
}
27+
```
28+
29+
### Compliant
30+
31+
```powershell
32+
function Set-Percentage
33+
{
34+
param([double][ValidateRange(0.0, 100.0)]$Value)
35+
}
36+
```
37+
38+
## Configure rule
39+
40+
This rule is always enabled and isn't configurable. Use one of the following methods to avoid using
41+
this rule:
42+
43+
- Create a custom rule configuration file to include only the rules you want or exclude the rules
44+
you don't want.
45+
- Add the appropriate rule suppression attributes to your code to suppress the rule for specific
46+
code blocks. For more information, see the _Suppressing rules_ section of [Using PSScriptAnalyzer][01].
47+
48+
<!-- Link references -->
49+
[01]: ../using-scriptanalyzer.md#suppressing-rules

‎docs/Rules/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ title: List of PSScriptAnalyzer rules
5252
| [AvoidUsingUsernameAndPasswordParams][32] | Error | Always enabled | |
5353
| [AvoidUsingWMICmdlet][33] | Warning | Always enabled | |
5454
| [AvoidUsingWriteHost][34] | Warning | Always enabled | |
55+
| [AvoidValidateRangeTypeMismatch][81] | Warning | Always enabled | |
5556
| [DSCDscExamplesPresent][35] | Information | Always enabled | |
5657
| [DSCDscTestsPresent][36] | Information | Always enabled | |
5758
| [DSCReturnCorrectTypesForDSCFunctions][37] | Information | Always enabled | |
@@ -180,3 +181,4 @@ title: List of PSScriptAnalyzer rules
180181
[78]: UseToExportFieldsInManifest.md
181182
[79]: UseUsingScopeModifierInNewRunspaces.md
182183
[80]: UseUTF8EncodingForHelpFile.md
184+
[81]: AvoidValidateRangeTypeMismatch.md

0 commit comments

Comments
 (0)