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+ }
0 commit comments