Skip to content

Commit 091a1ea

Browse files
committed
Added New-ScriptAnalyzerSettingsFile and updated Get-ScriptAnalyzerRule to include rule options (called options to not cause confusion with settings)
1 parent c92738a commit 091a1ea

File tree

8 files changed

+497
-47
lines changed

8 files changed

+497
-47
lines changed

Engine/Commands/GetScriptAnalyzerRuleCommand.cs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Globalization;
99
using System.Linq;
1010
using System.Management.Automation;
11+
using System.Reflection;
1112

1213
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands
1314
{
@@ -114,8 +115,35 @@ protected override void ProcessRecord()
114115

115116
foreach (IRule rule in rules)
116117
{
118+
IEnumerable<RuleOptionInfo> optionInfos = null;
119+
120+
if (rule is ConfigurableRule configurable)
121+
{
122+
var props = rule.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
123+
var optList = new List<RuleOptionInfo>();
124+
125+
foreach (var p in props)
126+
{
127+
if (p.GetCustomAttribute<ConfigurableRulePropertyAttribute>(inherit: true) == null) {
128+
continue;
129+
}
130+
131+
optList.Add(new RuleOptionInfo
132+
{
133+
Name = p.Name,
134+
OptionType = p.PropertyType,
135+
DefaultValue = p.GetValue(rule)
136+
});
137+
}
138+
139+
if (optList.Count > 0)
140+
{
141+
optionInfos = optList;
142+
}
143+
}
144+
117145
WriteObject(new RuleInfo(rule.GetName(), rule.GetCommonName(), rule.GetDescription(),
118-
rule.GetSourceType(), rule.GetSourceName(), rule.GetSeverity(), rule.GetType()));
146+
rule.GetSourceType(), rule.GetSourceName(), rule.GetSeverity(), rule.GetType(), optionInfos));
119147
}
120148
}
121149
}
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
2+
using System;
3+
using System.Collections;
4+
using System.Collections.Generic;
5+
using System.IO;
6+
using System.Linq;
7+
using System.Management.Automation;
8+
using System.Management.Automation.Language;
9+
using System.Reflection;
10+
11+
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands
12+
{
13+
/// <summary>
14+
/// Creates a new PSScriptAnalyzer settings file in the specified directory
15+
/// optionally based on a preset, a blank template, or all rules with default arguments.
16+
/// </summary>
17+
[Cmdlet(VerbsCommon.New, "ScriptAnalyzerSettingsFile", SupportsShouldProcess = true)]
18+
[OutputType(typeof(string))]
19+
public sealed class NewScriptAnalyzerSettingsFileCommand : PSCmdlet, IOutputWriter
20+
{
21+
private const string BaseOption_All = "All";
22+
private const string BaseOption_Blank = "Blank";
23+
24+
/// <summary>
25+
/// Target directory (or file path) where the settings file will be created. Defaults to
26+
/// current location.
27+
/// </summary>
28+
[Parameter(Position = 0)]
29+
[ValidateNotNullOrEmpty]
30+
public string Path { get; set; }
31+
32+
/// <summary>
33+
/// Settings file format/extension (e.g. json, psd1). Defaults to first supported format.
34+
/// </summary>
35+
[Parameter]
36+
[ArgumentCompleter(typeof(FileFormatCompleter))]
37+
[ValidateNotNullOrEmpty]
38+
public string FileFormat { get; set; }
39+
40+
/// <summary>
41+
/// Base content: 'Blank', 'All', or a preset name returned by Get-SettingPresets.
42+
/// 'Blank' -> minimal empty settings.
43+
/// 'All' -> include all rules and their configurable arguments with current defaults.
44+
/// preset -> copy preset contents.
45+
/// </summary>
46+
[Parameter]
47+
[ArgumentCompleter(typeof(SettingsBaseCompleter))]
48+
[ValidateNotNullOrEmpty]
49+
public string Base { get; set; } = BaseOption_Blank;
50+
51+
/// <summary>
52+
/// Overwrite existing file if present.
53+
/// </summary>
54+
[Parameter]
55+
public SwitchParameter Force { get; set; }
56+
57+
protected override void BeginProcessing()
58+
{
59+
Helper.Instance = new Helper(SessionState.InvokeCommand);
60+
Helper.Instance.Initialize();
61+
62+
string[] rulePaths = Helper.ProcessCustomRulePaths(null, SessionState, false);
63+
ScriptAnalyzer.Instance.Initialize(this, rulePaths, null, null, null, null == rulePaths);
64+
}
65+
66+
protected override void ProcessRecord()
67+
{
68+
// Default Path
69+
if (string.IsNullOrWhiteSpace(Path))
70+
{
71+
Path = SessionState.Path.CurrentFileSystemLocation.ProviderPath;
72+
}
73+
74+
// If user passed an existing file path, switch to its directory.
75+
if (File.Exists(Path))
76+
{
77+
Path = System.IO.Path.GetDirectoryName(Path);
78+
}
79+
80+
// Require the directory to already exist (do not create it).
81+
if (!Directory.Exists(Path))
82+
{
83+
ThrowTerminatingError(new ErrorRecord(
84+
new DirectoryNotFoundException($"Directory '{Path}' does not exist."),
85+
"DIRECTORY_NOT_FOUND",
86+
ErrorCategory.ObjectNotFound,
87+
Path));
88+
return;
89+
}
90+
91+
// Ensure FileSystem provider for target Path.
92+
ProviderInfo providerInfo;
93+
try
94+
{
95+
SessionState.Path.GetResolvedProviderPathFromPSPath(Path, out providerInfo);
96+
}
97+
catch (Exception ex)
98+
{
99+
ThrowTerminatingError(new ErrorRecord(
100+
new InvalidOperationException($"Cannot resolve path '{Path}': {ex.Message}", ex),
101+
"PATH_RESOLVE_FAILED",
102+
ErrorCategory.InvalidArgument,
103+
Path));
104+
return;
105+
}
106+
107+
if (!string.Equals(providerInfo.Name, "FileSystem", StringComparison.OrdinalIgnoreCase))
108+
{
109+
ThrowTerminatingError(new ErrorRecord(
110+
new InvalidOperationException("Target path must be in the FileSystem provider."),
111+
"INVALID_PROVIDER",
112+
ErrorCategory.InvalidArgument,
113+
Path));
114+
}
115+
116+
// Default format to first supported.
117+
if (string.IsNullOrWhiteSpace(FileFormat))
118+
{
119+
FileFormat = Settings.GetSettingsFormats().First();
120+
}
121+
122+
// Validate requested format.
123+
if (!Settings.GetSettingsFormats().Any(f => string.Equals(f, FileFormat, StringComparison.OrdinalIgnoreCase)))
124+
{
125+
ThrowTerminatingError(new ErrorRecord(
126+
new ArgumentException($"Unsupported settings format '{FileFormat}'."),
127+
"UNSUPPORTED_FORMAT",
128+
ErrorCategory.InvalidArgument,
129+
FileFormat));
130+
}
131+
132+
var targetFile = System.IO.Path.Combine(Path, $"{Settings.DefaultSettingsFileName}.{FileFormat}");
133+
134+
if (File.Exists(targetFile) && !Force)
135+
{
136+
WriteWarning($"Settings file already exists: {targetFile}. Use -Force to overwrite.");
137+
return;
138+
}
139+
140+
SettingsData data;
141+
try
142+
{
143+
data = BuildSettingsData();
144+
}
145+
catch (Exception ex)
146+
{
147+
ThrowTerminatingError(new ErrorRecord(
148+
ex,
149+
"BUILD_SETTINGS_FAILED",
150+
ErrorCategory.InvalidData,
151+
Base));
152+
return;
153+
}
154+
155+
string content;
156+
try
157+
{
158+
content = Settings.Serialize(data, FileFormat);
159+
}
160+
catch (Exception ex)
161+
{
162+
ThrowTerminatingError(new ErrorRecord(
163+
ex,
164+
"SERIALIZE_FAILED",
165+
ErrorCategory.InvalidData,
166+
FileFormat));
167+
return;
168+
}
169+
170+
if (ShouldProcess(targetFile, "Create settings file"))
171+
{
172+
try
173+
{
174+
File.WriteAllText(targetFile, content);
175+
WriteVerbose($"Created settings file: {targetFile}");
176+
}
177+
catch (Exception ex)
178+
{
179+
ThrowTerminatingError(new ErrorRecord(
180+
ex,
181+
"CREATE_FILE_FAILED",
182+
ErrorCategory.InvalidData,
183+
targetFile));
184+
return;
185+
}
186+
WriteObject(targetFile);
187+
}
188+
}
189+
190+
private SettingsData BuildSettingsData()
191+
{
192+
if (string.Equals(Base, BaseOption_Blank, StringComparison.OrdinalIgnoreCase))
193+
{
194+
return new SettingsData(); // empty snapshot
195+
}
196+
197+
if (string.Equals(Base, BaseOption_All, StringComparison.OrdinalIgnoreCase))
198+
{
199+
return BuildAllSettingsData();
200+
}
201+
202+
// Preset
203+
var presetPath = Settings.TryResolvePreset(Base);
204+
if (presetPath == null)
205+
{
206+
throw new FileNotFoundException($"Preset '{Base}' not found.");
207+
}
208+
return Settings.Create(presetPath);
209+
}
210+
211+
private SettingsData BuildAllSettingsData()
212+
{
213+
var ruleNames = new List<string>();
214+
var ruleArgs = new Dictionary<string, Dictionary<string, object>>(StringComparer.OrdinalIgnoreCase);
215+
216+
var modNames = ScriptAnalyzer.Instance.GetValidModulePaths();
217+
var rules = ScriptAnalyzer.Instance.GetRule(modNames, null) ?? Enumerable.Empty<IRule>();
218+
219+
foreach (var rule in rules)
220+
{
221+
var name = rule.GetName();
222+
ruleNames.Add(name);
223+
224+
if (rule is ConfigurableRule configurable)
225+
{
226+
var props = rule.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
227+
var argDict = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
228+
foreach (var p in props)
229+
{
230+
if (p.GetCustomAttribute<ConfigurableRulePropertyAttribute>(inherit: true) == null)
231+
{
232+
continue;
233+
}
234+
argDict[p.Name] = p.GetValue(rule);
235+
}
236+
if (argDict.Count > 0)
237+
{
238+
ruleArgs[name] = argDict;
239+
}
240+
}
241+
}
242+
243+
return new SettingsData
244+
{
245+
IncludeRules = ruleNames,
246+
RuleArguments = ruleArgs,
247+
};
248+
}
249+
250+
#region Completers
251+
252+
private sealed class FileFormatCompleter : IArgumentCompleter
253+
{
254+
public IEnumerable<CompletionResult> CompleteArgument(string commandName,
255+
string parameterName, string wordToComplete, CommandAst commandAst,
256+
IDictionary fakeBoundParameters)
257+
{
258+
foreach (var fmt in Settings.GetSettingsFormats())
259+
{
260+
if (fmt.StartsWith(wordToComplete ?? string.Empty, StringComparison.OrdinalIgnoreCase))
261+
{
262+
yield return new CompletionResult(fmt, fmt, CompletionResultType.ParameterValue, $"Settings format '{fmt}'");
263+
}
264+
}
265+
}
266+
}
267+
268+
private sealed class SettingsBaseCompleter : IArgumentCompleter
269+
{
270+
public IEnumerable<CompletionResult> CompleteArgument(string commandName,
271+
string parameterName, string wordToComplete, CommandAst commandAst,
272+
IDictionary fakeBoundParameters)
273+
{
274+
var bases = new List<string> { BaseOption_Blank, BaseOption_All };
275+
bases.AddRange(Settings.GetSettingPresets());
276+
277+
foreach (var b in bases)
278+
{
279+
if (b.StartsWith(wordToComplete ?? string.Empty, StringComparison.OrdinalIgnoreCase))
280+
{
281+
yield return new CompletionResult(b, b, CompletionResultType.ParameterValue, $"Base template '{b}'");
282+
}
283+
}
284+
}
285+
}
286+
287+
#endregion
288+
}
289+
}

0 commit comments

Comments
 (0)