Skip to content

Commit f572b2a

Browse files
committed
Add JSON settings parser
1 parent 2aeef95 commit f572b2a

File tree

3 files changed

+111
-0
lines changed

3 files changed

+111
-0
lines changed

Engine/Engine.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,15 @@
7171

7272
<ItemGroup Condition="'$(TargetFramework)' == 'net8'">
7373
<PackageReference Include="System.Management.Automation" />
74+
<PackageReference Include="Newtonsoft.Json" />
7475
</ItemGroup>
76+
7577
<PropertyGroup Condition="'$(TargetFramework)' == 'net8'">
7678
<DefineConstants>$(DefineConstants);PSV7;CORECLR</DefineConstants>
7779
</PropertyGroup>
7880

7981
<ItemGroup Condition="'$(TargetFramework)' == 'net462' ">
82+
<PackageReference Include="Newtonsoft.Json" />
8083
<PackageReference Include="Microsoft.PowerShell.5.ReferenceAssemblies" />
8184
</ItemGroup>
8285

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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.IO;
7+
using Newtonsoft.Json;
8+
9+
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer
10+
{
11+
12+
/// <summary>
13+
/// Parses JSON settings files (extension .json) into <see cref="SettingsData"/>.
14+
/// Expected top-level properties:
15+
/// Severity : string or string array
16+
/// IncludeRules : string or string array
17+
/// ExcludeRules : string or string array
18+
/// CustomRulePath : string or string array
19+
/// IncludeDefaultRules : bool
20+
/// RecurseCustomRulePath : bool
21+
/// Rules : object with ruleName -> { argumentName : value } mapping
22+
/// Parsing logic:
23+
/// 1. Read entire stream into a string.
24+
/// 2. Deserialize to DTO with Newtonsoft.Json (case-insensitive by default).
25+
/// 3. Validate null result -> invalid data.
26+
/// 4. Normalize each collection to empty lists when absent.
27+
/// 5. Rebuild rule arguments as case-insensitive dictionaries.
28+
/// Throws <see cref="InvalidDataException"/> on malformed JSON or missing structure.
29+
/// </summary>
30+
internal sealed class JsonSettingsParser : ISettingsParser
31+
{
32+
33+
/// <summary>
34+
/// DTO for deserializing JSON settings.
35+
/// </summary>
36+
private sealed class JsonSettingsDto
37+
{
38+
public List<string> Severity { get; set; }
39+
public List<string> IncludeRules { get; set; }
40+
public List<string> ExcludeRules { get; set; }
41+
public List<string> CustomRulePath { get; set; }
42+
public bool? IncludeDefaultRules { get; set; }
43+
public bool? RecurseCustomRulePath { get; set; }
44+
public Dictionary<string, Dictionary<string, object>> Rules { get; set; }
45+
}
46+
47+
public string FormatName => "json";
48+
49+
/// <summary>
50+
/// Determines if this parser can handle the supplied path by checking for .json extension.
51+
/// </summary>
52+
/// <param name="pathOrExtension">File path or extension string.</param>
53+
/// <returns>True if extension is .json.</returns>
54+
public bool CanParse(string pathOrExtension) =>
55+
string.Equals(Path.GetExtension(pathOrExtension), ".json", StringComparison.OrdinalIgnoreCase);
56+
57+
/// <summary>
58+
/// Parses a JSON settings file stream into <see cref="SettingsData"/>.
59+
/// </summary>
60+
/// <param name="content">Readable stream positioned at start of JSON content.</param>
61+
/// <param name="sourcePath">Original file path (for error context).</param>
62+
/// <returns>Populated <see cref="SettingsData"/>.</returns>
63+
/// <exception cref="InvalidDataException">
64+
/// Thrown on JSON deserialization error or invalid/empty root object.
65+
/// </exception>
66+
public SettingsData Parse(Stream content, string sourcePath)
67+
{
68+
using var reader = new StreamReader(content);
69+
string json = reader.ReadToEnd();
70+
JsonSettingsDto dto;
71+
try
72+
{
73+
dto = JsonConvert.DeserializeObject<JsonSettingsDto>(json);
74+
}
75+
catch (JsonException je)
76+
{
77+
throw new InvalidDataException($"Failed to parse settings JSON '{sourcePath}': {je.Message}", je);
78+
}
79+
if (dto == null)
80+
throw new InvalidDataException($"Settings JSON '{sourcePath}' is empty or invalid.");
81+
82+
// Normalize rule arguments into case-insensitive dictionaries
83+
var ruleArgs = new Dictionary<string, Dictionary<string, object>>(StringComparer.OrdinalIgnoreCase);
84+
if (dto.Rules != null)
85+
{
86+
foreach (var kv in dto.Rules)
87+
{
88+
ruleArgs[kv.Key] = kv.Value != null
89+
? new Dictionary<string, object>(kv.Value, StringComparer.OrdinalIgnoreCase)
90+
: new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
91+
}
92+
}
93+
94+
return new SettingsData
95+
{
96+
IncludeRules = dto.IncludeRules ?? new List<string>(),
97+
ExcludeRules = dto.ExcludeRules ?? new List<string>(),
98+
Severities = dto.Severity ?? new List<string>(),
99+
CustomRulePath = dto.CustomRulePath ?? new List<string>(),
100+
IncludeDefaultRules = dto.IncludeDefaultRules.GetValueOrDefault(),
101+
RecurseCustomRulePath = dto.RecurseCustomRulePath.GetValueOrDefault(),
102+
RuleArguments = ruleArgs
103+
};
104+
}
105+
}
106+
107+
}

Engine/Settings/Settings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public static class Settings
3434
/// </summary>
3535
private static readonly List<ISettingsParser> s_parsers = new()
3636
{
37+
new JsonSettingsParser(),
3738
new Psd1SettingsParser()
3839
};
3940

0 commit comments

Comments
 (0)