Skip to content

Commit 239b039

Browse files
committed
Add RuleOptionInfo class and update RuleInfo to include options for configurable rules
1 parent a143b9f commit 239b039

4 files changed

Lines changed: 201 additions & 1 deletion

File tree

Engine/Commands/GetScriptAnalyzerRuleCommand.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,12 @@ protected override void ProcessRecord()
114114

115115
foreach (IRule rule in rules)
116116
{
117+
var ruleOptions = rule is ConfigurableRule
118+
? RuleOptionInfo.GetRuleOptions(rule)
119+
: null;
120+
117121
WriteObject(new RuleInfo(rule.GetName(), rule.GetCommonName(), rule.GetDescription(),
118-
rule.GetSourceType(), rule.GetSourceName(), rule.GetSeverity(), rule.GetType()));
122+
rule.GetSourceType(), rule.GetSourceName(), rule.GetSeverity(), rule.GetType(), ruleOptions));
119123
}
120124
}
121125
}

Engine/Generic/RuleInfo.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.Collections.Generic;
56
using System.Diagnostics.CodeAnalysis;
67

78
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic
@@ -18,6 +19,7 @@ public class RuleInfo
1819
private string sourceName;
1920
private RuleSeverity ruleSeverity;
2021
private Type implementingType;
22+
private IReadOnlyList<RuleOptionInfo> options;
2123

2224
/// <summary>
2325
/// Name: The name of the rule.
@@ -90,6 +92,16 @@ public Type ImplementingType
9092
private set { implementingType = value; }
9193
}
9294

95+
/// <summary>
96+
/// Options : The configurable properties for this rule, if any.
97+
/// </summary>
98+
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
99+
public IReadOnlyList<RuleOptionInfo> Options
100+
{
101+
get { return options; }
102+
private set { options = value; }
103+
}
104+
93105
/// <summary>
94106
/// Constructor for a RuleInfo.
95107
/// </summary>
@@ -128,6 +140,29 @@ public RuleInfo(string name, string commonName, string description, SourceType s
128140
ImplementingType = implementingType;
129141
}
130142

143+
/// <summary>
144+
/// Constructor for a RuleInfo.
145+
/// </summary>
146+
/// <param name="name">Name of the rule.</param>
147+
/// <param name="commonName">Common Name of the rule.</param>
148+
/// <param name="description">Description of the rule.</param>
149+
/// <param name="sourceType">Source type of the rule.</param>
150+
/// <param name="sourceName">Source name of the rule.</param>
151+
/// <param name="severity">Severity of the rule.</param>
152+
/// <param name="implementingType">The dotnet type of the rule.</param>
153+
/// <param name="options">The configurable properties for this rule.</param>
154+
public RuleInfo(string name, string commonName, string description, SourceType sourceType, string sourceName, RuleSeverity severity, Type implementingType, IReadOnlyList<RuleOptionInfo> options)
155+
{
156+
RuleName = name;
157+
CommonName = commonName;
158+
Description = description;
159+
SourceType = sourceType;
160+
SourceName = sourceName;
161+
Severity = severity;
162+
ImplementingType = implementingType;
163+
Options = options;
164+
}
165+
131166
public override string ToString()
132167
{
133168
return RuleName;

Engine/Generic/RuleOptionInfo.cs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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.Linq;
7+
using System.Reflection;
8+
9+
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic
10+
{
11+
/// <summary>
12+
/// Holds metadata for a single configurable rule property.
13+
/// </summary>
14+
public class RuleOptionInfo
15+
{
16+
/// <summary>
17+
/// The name of the configurable property.
18+
/// </summary>
19+
public string Name { get; internal set; }
20+
21+
/// <summary>
22+
/// The CLR type of the property value.
23+
/// </summary>
24+
public Type OptionType { get; internal set; }
25+
26+
/// <summary>
27+
/// The default value declared via the ConfigurableRuleProperty attribute.
28+
/// </summary>
29+
public object DefaultValue { get; internal set; }
30+
31+
/// <summary>
32+
/// The set of valid values for this property, if constrained.
33+
/// Null when any value of the declared type is acceptable.
34+
/// </summary>
35+
public object[] PossibleValues { get; internal set; }
36+
37+
/// <summary>
38+
/// Extracts RuleOptionInfo entries for every ConfigurableRuleProperty on
39+
/// the given rule. For string properties backed by a private enum, the
40+
/// possible values are populated from the enum members.
41+
/// </summary>
42+
/// <param name="rule">The rule instance to inspect.</param>
43+
/// <returns>
44+
/// A list of option metadata, ordered with Enable first then the
45+
/// remainder sorted alphabetically.
46+
/// </returns>
47+
public static List<RuleOptionInfo> GetRuleOptions(IRule rule)
48+
{
49+
var options = new List<RuleOptionInfo>();
50+
Type ruleType = rule.GetType();
51+
52+
PropertyInfo[] properties = ruleType.GetProperties(BindingFlags.Instance | BindingFlags.Public);
53+
54+
// Collect all private nested enums declared on the rule type so we
55+
// can match them against string properties whose default value is an
56+
// enum member name.
57+
Type[] nestedEnums = ruleType
58+
.GetNestedTypes(BindingFlags.NonPublic | BindingFlags.Public)
59+
.Where(t => t.IsEnum)
60+
.ToArray();
61+
62+
foreach (PropertyInfo prop in properties)
63+
{
64+
var attr = prop.GetCustomAttribute<ConfigurableRulePropertyAttribute>(inherit: true);
65+
if (attr == null)
66+
{
67+
continue;
68+
}
69+
70+
var info = new RuleOptionInfo
71+
{
72+
Name = prop.Name,
73+
OptionType = prop.PropertyType,
74+
DefaultValue = attr.DefaultValue,
75+
PossibleValues = null
76+
};
77+
78+
// For string properties, attempt to find a matching private enum
79+
// whose member names include the default value. This mirrors the
80+
// pattern used by rules such as UseConsistentIndentation and
81+
// ProvideCommentHelp where a string property is parsed into a
82+
// private enum via Enum.TryParse.
83+
//
84+
// When multiple enums contain the default value (e.g. both have
85+
// a "None" member), prefer the enum whose name contains the
86+
// property name or vice-versa (e.g. property "Kind" matches enum
87+
// "IndentationKind"). This helps avoid incorrect matches when a rule
88+
// declares several enums with possible overlapping member names.
89+
if (prop.PropertyType == typeof(string) && attr.DefaultValue is string defaultStr)
90+
{
91+
Type bestMatch = null;
92+
bool bestHasNameRelation = false;
93+
94+
foreach (Type enumType in nestedEnums)
95+
{
96+
if (!Enum.GetNames(enumType).Any(n =>
97+
string.Equals(n, defaultStr, StringComparison.OrdinalIgnoreCase)))
98+
{
99+
continue;
100+
}
101+
102+
bool hasNameRelation =
103+
enumType.Name.IndexOf(prop.Name, StringComparison.OrdinalIgnoreCase) >= 0 ||
104+
prop.Name.IndexOf(enumType.Name, StringComparison.OrdinalIgnoreCase) >= 0;
105+
106+
// Take this enum if we have no match yet, or if it has a
107+
// name-based relationship and the previous match did not.
108+
if (bestMatch == null || (hasNameRelation && !bestHasNameRelation))
109+
{
110+
bestMatch = enumType;
111+
bestHasNameRelation = hasNameRelation;
112+
}
113+
}
114+
115+
if (bestMatch != null)
116+
{
117+
info.PossibleValues = Enum.GetNames(bestMatch);
118+
}
119+
}
120+
121+
options.Add(info);
122+
}
123+
124+
// Sort with "Enable" first, then alphabetically by name for consistent ordering.
125+
return options
126+
.OrderBy(o => string.Equals(o.Name, "Enable", StringComparison.OrdinalIgnoreCase) ? 0 : 1)
127+
.ThenBy(o => o.Name, StringComparer.OrdinalIgnoreCase)
128+
.ToList();
129+
}
130+
}
131+
}

Tests/Engine/GetScriptAnalyzerRule.tests.ps1

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,33 @@ Describe "TestImplementingType" {
180180
$type.BaseType.Name | Should -Be "ConfigurableRule"
181181
}
182182
}
183+
184+
Describe "TestOptions" {
185+
BeforeAll {
186+
$configurableRule = Get-ScriptAnalyzerRule PSUseConsistentIndentation
187+
$nonConfigurableRule = Get-ScriptAnalyzerRule PSAvoidUsingInvokeExpression
188+
}
189+
190+
It "returns Options for a configurable rule" {
191+
$configurableRule.Options | Should -Not -BeNullOrEmpty
192+
}
193+
194+
It "includes the Enable option" {
195+
$configurableRule.Options.Name | Should -Contain 'Enable'
196+
}
197+
198+
It "places Enable as the first option" {
199+
$configurableRule.Options[0].Name | Should -Be 'Enable'
200+
}
201+
202+
It "populates PossibleValues for enum-backed string properties" {
203+
$kindOption = $configurableRule.Options | Where-Object Name -eq 'Kind'
204+
$kindOption.PossibleValues | Should -Not -BeNullOrEmpty
205+
$kindOption.PossibleValues | Should -Contain 'Space'
206+
$kindOption.PossibleValues | Should -Contain 'Tab'
207+
}
208+
209+
It "returns null Options for a non-configurable rule" {
210+
$nonConfigurableRule.Options | Should -BeNullOrEmpty
211+
}
212+
}

0 commit comments

Comments
 (0)