From 3cea006aa370bd4e1fd275cd836e901837d523bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:19:58 +0000 Subject: [PATCH 1/5] Initial plan From beb066a6f96da31b9177808c32d63b6ee91c957e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:24:55 +0000 Subject: [PATCH 2/5] Handle ValidateRange static integral bounds Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- Rules/AvoidValidateRangeTypeMismatch.cs | 17 ++++++++++++----- .../AvoidValidateRangeTypeMismatch.tests.ps1 | 2 ++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Rules/AvoidValidateRangeTypeMismatch.cs b/Rules/AvoidValidateRangeTypeMismatch.cs index 319a865ba..4aff662c2 100644 --- a/Rules/AvoidValidateRangeTypeMismatch.cs +++ b/Rules/AvoidValidateRangeTypeMismatch.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.Linq; using System.Management.Automation.Language; +using System.Reflection; using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; #if !CORECLR using System.ComponentModel.Composition; @@ -109,16 +110,22 @@ private static bool IsIntegralBoundary(ExpressionAst expressionAst) return IsIntegralBoundary(unaryExpressionAst.Child); } + if (expressionAst is MemberExpressionAst memberExpressionAst + && memberExpressionAst.Static + && memberExpressionAst.Expression is TypeExpressionAst typeExpressionAst + && memberExpressionAst.Member is StringConstantExpressionAst memberName) + { + Type type = typeExpressionAst.TypeName.GetReflectionType(); + FieldInfo field = type?.GetField(memberName.Value, BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase); + return field != null && field.IsLiteral && IntegralTypes.Contains(field.FieldType); + } + return false; } private static bool IsValidateRangeAttribute(AttributeAst attributeAst) { - string attributeName = attributeAst.TypeName.FullName; - int lastDot = attributeName.LastIndexOf('.'); - string simpleName = lastDot < 0 ? attributeName : attributeName.Substring(lastDot + 1); - return String.Equals(simpleName, "ValidateRange", StringComparison.OrdinalIgnoreCase) - || String.Equals(simpleName, "ValidateRangeAttribute", StringComparison.OrdinalIgnoreCase); + return attributeAst.TypeName.GetReflectionType() == typeof(System.Management.Automation.ValidateRangeAttribute); } public string GetName() diff --git a/Tests/Rules/AvoidValidateRangeTypeMismatch.tests.ps1 b/Tests/Rules/AvoidValidateRangeTypeMismatch.tests.ps1 index bc3915240..6711d1fc9 100644 --- a/Tests/Rules/AvoidValidateRangeTypeMismatch.tests.ps1 +++ b/Tests/Rules/AvoidValidateRangeTypeMismatch.tests.ps1 @@ -15,6 +15,7 @@ Describe 'AvoidValidateRangeTypeMismatch' { 'function Test { param([double][System.Management.Automation.ValidateRangeAttribute(1, 2)]$Value) }' 'function Test { param([double][ValidateRange([int]0.0, [int]100.0)]$Value) }' 'function Test { param([double][ValidateRange([int32]0.0, [int32]100.0)]$Value) }' + 'function Test { param([double][ValidateRange(0, [int]::MaxValue)]$Value) }' ) foreach ($scriptDefinition in $scripts) { @@ -30,6 +31,7 @@ Describe 'AvoidValidateRangeTypeMismatch' { 'function Test { param([single][ValidateRange(1.0, 2.0)]$Value) }' 'function Test { param([decimal][ValidateRange(1.0, 2.0)]$Value) }' 'function Test { param([double][ValidateRange(1, 2.0)]$Value) }' + 'function Test { param([double][Contoso.ValidateRange(0, 100)]$Value) }' 'function Test { param([int][ValidateRange(1, 2)]$Value) }' ) From 391e7775f3e9db374c6f2a5b989325cfe2622083 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:34:59 +0000 Subject: [PATCH 3/5] Fix ValidateRange review edge cases Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- Rules/AvoidValidateRangeTypeMismatch.cs | 4 ++-- Tests/Rules/AvoidValidateRangeTypeMismatch.tests.ps1 | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Rules/AvoidValidateRangeTypeMismatch.cs b/Rules/AvoidValidateRangeTypeMismatch.cs index 4aff662c2..aa48db75f 100644 --- a/Rules/AvoidValidateRangeTypeMismatch.cs +++ b/Rules/AvoidValidateRangeTypeMismatch.cs @@ -116,7 +116,7 @@ private static bool IsIntegralBoundary(ExpressionAst expressionAst) && memberExpressionAst.Member is StringConstantExpressionAst memberName) { Type type = typeExpressionAst.TypeName.GetReflectionType(); - FieldInfo field = type?.GetField(memberName.Value, BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase); + FieldInfo field = type?.GetField(memberName.Value, BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase | BindingFlags.FlattenHierarchy); return field != null && field.IsLiteral && IntegralTypes.Contains(field.FieldType); } @@ -125,7 +125,7 @@ private static bool IsIntegralBoundary(ExpressionAst expressionAst) private static bool IsValidateRangeAttribute(AttributeAst attributeAst) { - return attributeAst.TypeName.GetReflectionType() == typeof(System.Management.Automation.ValidateRangeAttribute); + return attributeAst.TypeName.GetReflectionAttributeType() == typeof(System.Management.Automation.ValidateRangeAttribute); } public string GetName() diff --git a/Tests/Rules/AvoidValidateRangeTypeMismatch.tests.ps1 b/Tests/Rules/AvoidValidateRangeTypeMismatch.tests.ps1 index 6711d1fc9..1819213d2 100644 --- a/Tests/Rules/AvoidValidateRangeTypeMismatch.tests.ps1 +++ b/Tests/Rules/AvoidValidateRangeTypeMismatch.tests.ps1 @@ -4,6 +4,16 @@ Describe 'AvoidValidateRangeTypeMismatch' { BeforeAll { $ruleName = 'PSAvoidValidateRangeTypeMismatch' + Add-Type -TypeDefinition @' +public class ValidateRangeBoundsBaseForScriptAnalyzer +{ + public const int Max = 100; +} + +public class ValidateRangeBoundsForScriptAnalyzer : ValidateRangeBoundsBaseForScriptAnalyzer +{ +} +'@ } It 'reports integral boundaries for floating-point and decimal parameter types' { @@ -16,6 +26,8 @@ Describe 'AvoidValidateRangeTypeMismatch' { 'function Test { param([double][ValidateRange([int]0.0, [int]100.0)]$Value) }' 'function Test { param([double][ValidateRange([int32]0.0, [int32]100.0)]$Value) }' 'function Test { param([double][ValidateRange(0, [int]::MaxValue)]$Value) }' + 'function Test { param([double][ValidateRange(0, [ValidateRangeBoundsForScriptAnalyzer]::Max)]$Value) }' + 'function Test { param([double][System.Management.Automation.ValidateRange(0, 100)]$Value) }' ) foreach ($scriptDefinition in $scripts) { From 0fd8633e3459b73f92a9a33e71fd6410ea627c84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:05:56 +0000 Subject: [PATCH 4/5] Add real Contoso.ValidateRangeAttribute to test fixture Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- Tests/Rules/AvoidValidateRangeTypeMismatch.tests.ps1 | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Tests/Rules/AvoidValidateRangeTypeMismatch.tests.ps1 b/Tests/Rules/AvoidValidateRangeTypeMismatch.tests.ps1 index 1819213d2..148f09308 100644 --- a/Tests/Rules/AvoidValidateRangeTypeMismatch.tests.ps1 +++ b/Tests/Rules/AvoidValidateRangeTypeMismatch.tests.ps1 @@ -13,6 +13,17 @@ public class ValidateRangeBoundsBaseForScriptAnalyzer public class ValidateRangeBoundsForScriptAnalyzer : ValidateRangeBoundsBaseForScriptAnalyzer { } + +namespace Contoso +{ + [System.AttributeUsage(System.AttributeTargets.All)] + public class ValidateRangeAttribute : System.Attribute + { + public ValidateRangeAttribute(int minRange, int maxRange) + { + } + } +} '@ } From bd60450420dae323987c32c6b425e90ac9ccec01 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:43:39 +0000 Subject: [PATCH 5/5] Test non-constant ValidateRange bounds Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- .../AvoidValidateRangeTypeMismatch.tests.ps1 | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Tests/Rules/AvoidValidateRangeTypeMismatch.tests.ps1 b/Tests/Rules/AvoidValidateRangeTypeMismatch.tests.ps1 index 148f09308..24e6ad129 100644 --- a/Tests/Rules/AvoidValidateRangeTypeMismatch.tests.ps1 +++ b/Tests/Rules/AvoidValidateRangeTypeMismatch.tests.ps1 @@ -12,6 +12,18 @@ public class ValidateRangeBoundsBaseForScriptAnalyzer public class ValidateRangeBoundsForScriptAnalyzer : ValidateRangeBoundsBaseForScriptAnalyzer { + public static readonly int ReadonlyIntegral = 100; + public const double FloatingPoint = 100.0; + public static int ThrowingGetterInvocationCount; + + public static int ThrowingProperty + { + get + { + ThrowingGetterInvocationCount++; + throw new System.InvalidOperationException(); + } + } } namespace Contoso @@ -62,4 +74,19 @@ namespace Contoso Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule $ruleName | Should -BeNullOrEmpty } } + + It 'ignores non-constant static member expressions without invoking getters' { + $scripts = @( + 'function Test { param([double][ValidateRange(0, [ValidateRangeBoundsForScriptAnalyzer]::ThrowingProperty)]$Value) }' + 'function Test { param([double][ValidateRange(0, [ValidateRangeBoundsForScriptAnalyzer]::ReadonlyIntegral)]$Value) }' + 'function Test { param([double][ValidateRange(0, [ValidateRangeBoundsForScriptAnalyzer]::FloatingPoint)]$Value) }' + ) + + foreach ($scriptDefinition in $scripts) { + Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -IncludeRule $ruleName | + Where-Object RuleName -eq $ruleName | Should -BeNullOrEmpty + } + + [ValidateRangeBoundsForScriptAnalyzer]::ThrowingGetterInvocationCount | Should -Be 0 + } }