diff --git a/src/PSRule/Commands/InvokeRuleBlockCommand.cs b/src/PSRule/Commands/InvokeRuleBlockCommand.cs index edbb6db9fa..f64130777e 100644 --- a/src/PSRule/Commands/InvokeRuleBlockCommand.cs +++ b/src/PSRule/Commands/InvokeRuleBlockCommand.cs @@ -103,7 +103,7 @@ private bool AcceptsType() if (Type == null) return true; - var comparer = RunspaceContext.CurrentThread.LanguageScope.Binding.GetComparer(); + var comparer = RunspaceContext.CurrentThread.LanguageScope.GetBindingComparer(); var targetType = RunspaceContext.CurrentThread.RuleRecord.TargetType; for (var i = 0; i < Type.Length; i++) { diff --git a/src/PSRule/Definitions/Expressions/LanguageExpressionBuilder.cs b/src/PSRule/Definitions/Expressions/LanguageExpressionBuilder.cs index 706e96d8bf..eec7407914 100644 --- a/src/PSRule/Definitions/Expressions/LanguageExpressionBuilder.cs +++ b/src/PSRule/Definitions/Expressions/LanguageExpressionBuilder.cs @@ -287,7 +287,7 @@ private static bool AcceptsType(string[] type) if (type == null) return true; - var comparer = RunspaceContext.CurrentThread.LanguageScope.Binding.GetComparer(); + var comparer = RunspaceContext.CurrentThread.LanguageScope.GetBindingComparer(); var targetType = RunspaceContext.CurrentThread.RuleRecord.TargetType; for (var i = 0; i < type.Length; i++) { @@ -322,7 +322,7 @@ private static bool AcceptsRule(string[] rule) var context = RunspaceContext.CurrentThread; - var stringComparer = context.LanguageScope.Binding.GetComparer(); + var stringComparer = context.LanguageScope.GetBindingComparer(); var resourceIdComparer = ResourceIdEqualityComparer.Default; var ruleRecord = context.RuleRecord; diff --git a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs index 9c0c0785af..d44c6f22ea 100644 --- a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs +++ b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs @@ -1354,7 +1354,7 @@ private static bool TryScope(IExpressionContext context, LanguageExpression.Prop if (svalue != DOT || context?.Context?.LanguageScope == null) return Invalid(context, svalue); - if (!context.Context.LanguageScope.TryGetScope(o, out var scope)) + if (!context.Context.TryGetScope(o, out var scope)) return Invalid(context, svalue); operand = Operand.FromScope(scope); diff --git a/src/PSRule/Pipeline/InvokeRulePipeline.cs b/src/PSRule/Pipeline/InvokeRulePipeline.cs index 85f7252f93..f300f947f2 100644 --- a/src/PSRule/Pipeline/InvokeRulePipeline.cs +++ b/src/PSRule/Pipeline/InvokeRulePipeline.cs @@ -101,13 +101,12 @@ private InvokeResult ProcessTargetObject(TargetObject targetObject) foreach (var ruleBlockTarget in _RuleGraph.GetSingleTarget()) { // Enter rule block scope - Context.EnterLanguageScope(ruleBlockTarget.Value.Source); var ruleRecord = Context.EnterRuleBlock(ruleBlock: ruleBlockTarget.Value); ruleCounter++; try { - if (Context.Binding.ShouldFilter) + if (Context.Binding != null && Context.Binding.ShouldFilter) continue; // Check if dependency failed @@ -163,11 +162,9 @@ private InvokeResult ProcessTargetObject(TargetObject targetObject) finally { // Exit rule block scope - Context.ExitRuleBlock(); + Context.ExitRuleBlock(ruleBlock: ruleBlockTarget.Value); if (ShouldOutput(ruleRecord.Outcome)) result.Add(ruleRecord); - - Context.ExitLanguageScope(ruleBlockTarget.Value.Source); } } diff --git a/src/PSRule/Pipeline/OptionContext.cs b/src/PSRule/Pipeline/OptionContext.cs index f4ade6d621..91a9a6466d 100644 --- a/src/PSRule/Pipeline/OptionContext.cs +++ b/src/PSRule/Pipeline/OptionContext.cs @@ -7,14 +7,19 @@ namespace PSRule.Pipeline; +#nullable enable + internal sealed class OptionContext { private ConventionOption _Convention; private List _ConventionOrder; - public OptionContext() + public OptionContext(BindTargetMethod bindTargetName, BindTargetMethod bindTargetType, BindTargetMethod bindField, string[] inputTargetType) { - + BindTargetName = bindTargetName; + BindTargetType = bindTargetType; + BindField = bindField; + InputTargetType = inputTargetType; } public Options.BaselineOption Baseline { get; set; } @@ -58,6 +63,12 @@ public ConventionOption Convention public IResourceFilter RuleFilter { get; set; } + public BindTargetMethod BindTargetName { get; } + public BindTargetMethod BindTargetType { get; } + public BindTargetMethod BindField { get; } + + public string[] InputTargetType { get; } + internal int GetConventionOrder(IConvention convention) { if (Convention?.Include == null || Convention.Include.Length == 0) @@ -71,3 +82,5 @@ internal int GetConventionOrder(IConvention convention) return index > -1 ? index : int.MaxValue; } } + +#nullable restore diff --git a/src/PSRule/Pipeline/OptionContextBuilder.cs b/src/PSRule/Pipeline/OptionContextBuilder.cs index d8cc7e5193..519b70feec 100644 --- a/src/PSRule/Pipeline/OptionContextBuilder.cs +++ b/src/PSRule/Pipeline/OptionContextBuilder.cs @@ -13,6 +13,8 @@ namespace PSRule.Pipeline; +#nullable enable + /// /// A helper to create an . /// @@ -23,6 +25,10 @@ internal sealed class OptionContextBuilder private readonly OptionScopeComparer _Comparer; private readonly string[] _DefaultCulture; private readonly List _ConventionOrder; + private readonly BindTargetMethod _BindTargetName; + private readonly BindTargetMethod _BindTargetType; + private readonly BindTargetMethod _BindField; + private readonly string[] _InputTargetType; internal OptionContextBuilder(string[] include = null, Hashtable tag = null, string[] convention = null) { @@ -41,10 +47,17 @@ internal OptionContextBuilder(string[] include = null, Hashtable tag = null, str /// A list of rule identifiers to include set by parameters. If not set all rules that meet filters are included. /// A tag filter to determine which rules are included by parameters. /// A list of conventions to include by parameters. - internal OptionContextBuilder(PSRuleOption option, string[] include = null, Hashtable tag = null, string[] convention = null) + /// + /// + /// + internal OptionContextBuilder(PSRuleOption option, string[] include = null, Hashtable tag = null, string[] convention = null, BindTargetMethod bindTargetName = null, BindTargetMethod bindTargetType = null, BindTargetMethod bindField = null) : this(include, tag, convention) { Workspace(option); + _BindTargetName = bindTargetName; + _BindTargetType = bindTargetType; + _BindField = bindField; + _InputTargetType = option.Input.TargetType; } /// @@ -53,7 +66,7 @@ internal OptionContextBuilder(PSRuleOption option, string[] include = null, Hash internal OptionContext Build(string languageScope) { languageScope = ResourceHelper.NormalizeScope(languageScope); - var context = new OptionContext(); + var context = new OptionContext(_BindTargetName, _BindTargetType, _BindField, _InputTargetType); _Scopes.Sort(_Comparer); @@ -72,6 +85,7 @@ internal OptionContext Build(string languageScope) { Include = GetConventions(_Scopes) }; + return context; } @@ -220,3 +234,5 @@ private static string[] GetDefaultCulture() return result.ToArray(); } } + +#nullable restore diff --git a/src/PSRule/Pipeline/PipelineBuilderBase.cs b/src/PSRule/Pipeline/PipelineBuilderBase.cs index 03a7fab8a9..570c337e1f 100644 --- a/src/PSRule/Pipeline/PipelineBuilderBase.cs +++ b/src/PSRule/Pipeline/PipelineBuilderBase.cs @@ -180,7 +180,7 @@ protected PipelineContext PrepareContext(BindTargetMethod bindTargetName, BindTa bindTargetName: bindTargetName, bindTargetType: bindTargetType, bindField: bindField, - optionBuilder: GetOptionBuilder(), + optionBuilder: GetOptionBuilder(bindTargetName, bindTargetType, bindField), unresolved: unresolved ); } @@ -328,9 +328,9 @@ protected PathFilter GetInputFilter() return _InputFilter; } - private OptionContextBuilder GetOptionBuilder() + private OptionContextBuilder GetOptionBuilder(BindTargetMethod bindTargetName, BindTargetMethod bindTargetType, BindTargetMethod bindField) { - return new OptionContextBuilder(Option, _Include, _Tag, _Convention); + return new OptionContextBuilder(Option, _Include, _Tag, _Convention, bindTargetName, bindTargetType, bindField); } protected void ConfigureBinding(PSRuleOption option) diff --git a/src/PSRule/Pipeline/PipelineContext.cs b/src/PSRule/Pipeline/PipelineContext.cs index cfbb491be8..abd67ad6c5 100644 --- a/src/PSRule/Pipeline/PipelineContext.cs +++ b/src/PSRule/Pipeline/PipelineContext.cs @@ -53,9 +53,6 @@ internal sealed class PipelineContext : IDisposable, IBindingContext internal readonly List SuppressionGroup; internal readonly IHostContext HostContext; internal readonly PipelineInputStream Reader; - internal readonly BindTargetMethod BindTargetName; - internal readonly BindTargetMethod BindTargetType; - internal readonly BindTargetMethod BindField; internal readonly string RunId; internal readonly Stopwatch RunTime; @@ -70,9 +67,6 @@ private PipelineContext(PSRuleOption option, IHostContext hostContext, PipelineI Option = option; HostContext = hostContext; Reader = reader; - BindTargetName = bindTargetName; - BindTargetType = bindTargetType; - BindField = bindField; _LanguageMode = option.Execution.LanguageMode ?? ExecutionOption.Default.LanguageMode.Value; _PathExpressionCache = new Dictionary(); LocalizedDataCache = new Dictionary(); diff --git a/src/PSRule/Pipeline/TargetBinder.cs b/src/PSRule/Pipeline/TargetBinder.cs deleted file mode 100644 index 1aeda6e158..0000000000 --- a/src/PSRule/Pipeline/TargetBinder.cs +++ /dev/null @@ -1,327 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections; -using PSRule.Configuration; -using PSRule.Runtime; -using static PSRule.Pipeline.TargetBinder; - -namespace PSRule.Pipeline; - -/// -/// Responsible for handling binding for a given target object. -/// -internal interface ITargetBinder -{ - void Bind(TargetObject targetObject); - - ITargetBindingContext Using(string languageScope); - - ITargetBindingResult Result(string languageScope); -} - -/// -/// A binding context specific to a language scope. -/// -internal interface ITargetBindingContext -{ - string LanguageScope { get; } - - ITargetBindingResult Bind(object o); - - ITargetBindingResult Bind(TargetObject o); -} - -internal interface ITargetBindingResult -{ - /// - /// The bound TargetName of the target object. - /// - string TargetName { get; } - - string TargetNamePath { get; } - - /// - /// The bound TargetType of the target object. - /// - string TargetType { get; } - - string TargetTypePath { get; } - - /// - /// Additional bound fields of the target object. - /// - Hashtable Field { get; } - - bool ShouldFilter { get; } -} - -/// -/// Builds a TargetBinder. -/// -internal sealed class TargetBinderBuilder -{ - private readonly List _BindingContext; - private readonly HashSet _TypeFilter; - private readonly BindTargetMethod _BindTargetName; - private readonly BindTargetMethod _BindTargetType; - private readonly BindTargetMethod _BindField; - - public TargetBinderBuilder(BindTargetMethod bindTargetName, BindTargetMethod bindTargetType, BindTargetMethod bindField, string[] typeFilter) - { - _BindTargetName = bindTargetName; - _BindTargetType = bindTargetType; - _BindField = bindField; - _BindingContext = []; - if (typeFilter != null && typeFilter.Length > 0) - _TypeFilter = new HashSet(typeFilter, StringComparer.OrdinalIgnoreCase); - } - - /// - /// Build a TargetBinder. - /// - public ITargetBinder Build() - { - return new TargetBinder([.. _BindingContext]); - } - - /// - /// Add a target binding context. - /// - public void With(ITargetBindingContext bindingContext) - { - _BindingContext.Add(bindingContext); - } - - /// - /// Add a target binding context from language scope. Current this only use for tests. - /// - internal void With(ILanguageScope languageScope) - { - With(new TargetBindingContext(languageScope.Name, languageScope.Binding, _BindTargetName, _BindTargetType, _BindField, _TypeFilter)); - } -} - -/// -/// Responsible for handling binding for a given target object. -/// -internal sealed class TargetBinder : ITargetBinder -{ - private const string STANDALONE_SCOPE = "."; - - private readonly Dictionary _BindingContext; - private readonly Dictionary _BindingResult; - - internal TargetBinder(ITargetBindingContext[] bindingContext) - { - _BindingContext = []; - _BindingResult = []; - for (var i = 0; bindingContext != null && i < bindingContext.Length; i++) - _BindingContext.Add(bindingContext[i].LanguageScope ?? STANDALONE_SCOPE, bindingContext[i]); - } - - private sealed class ImmutableHashtable : Hashtable - { - private bool _ReadOnly; - - internal ImmutableHashtable() - : base(StringComparer.OrdinalIgnoreCase) { } - - public override bool IsReadOnly => _ReadOnly; - - public override void Add(object key, object value) - { - if (_ReadOnly) - throw new InvalidOperationException(); - - base.Add(key, value); - } - - public override void Clear() - { - if (_ReadOnly) - throw new InvalidOperationException(); - - base.Clear(); - } - - public override void Remove(object key) - { - if (_ReadOnly) - throw new InvalidOperationException(); - - base.Remove(key); - } - - public override object this[object key] - { - get => base[key]; - set - { - if (_ReadOnly) - throw new InvalidOperationException(); - - base[key] = value; - } - } - - internal void Protect() - { - _ReadOnly = true; - } - } - - internal sealed class TargetBindingResult : ITargetBindingResult - { - public TargetBindingResult(string targetName, string targetNamePath, string targetType, string targetTypePath, bool shouldFilter, Hashtable field) - { - TargetName = targetName; - TargetNamePath = targetNamePath; - TargetType = targetType; - TargetTypePath = targetTypePath; - ShouldFilter = shouldFilter; - Field = field; - } - - /// - public string TargetName { get; } - - /// - public string TargetNamePath { get; } - - /// - public string TargetType { get; } - - /// - public string TargetTypePath { get; } - - /// - public bool ShouldFilter { get; } - - /// - public Hashtable Field { get; } - } - - internal sealed class TargetBindingContext : ITargetBindingContext - { - private readonly bool _PreferTargetInfo; - private readonly bool _IgnoreCase; - private readonly bool _UseQualifiedName; - private readonly FieldMap _Field; - private readonly string[] _TargetName; - private readonly string[] _TargetType; - private readonly string _NameSeparator; - private readonly BindTargetMethod _BindTargetName; - private readonly BindTargetMethod _BindTargetType; - private readonly BindTargetMethod _BindField; - private readonly HashSet _TypeFilter; - - public TargetBindingContext(string languageScope, BindingOption bindingOption, BindTargetMethod bindTargetName, BindTargetMethod bindTargetType, BindTargetMethod bindField, HashSet typeFilter) - { - LanguageScope = languageScope; - _PreferTargetInfo = bindingOption?.PreferTargetInfo ?? BindingOption.Default.PreferTargetInfo.Value; - _IgnoreCase = bindingOption?.IgnoreCase ?? BindingOption.Default.IgnoreCase.Value; - _UseQualifiedName = bindingOption?.UseQualifiedName ?? BindingOption.Default.UseQualifiedName.Value; - _Field = bindingOption?.Field; - _TargetName = bindingOption?.TargetName; - _TargetType = bindingOption?.TargetType; - _NameSeparator = bindingOption?.NameSeparator ?? BindingOption.Default.NameSeparator; - _BindTargetName = bindTargetName; - _BindTargetType = bindTargetType; - _BindField = bindField; - _TypeFilter = typeFilter; - } - - public string LanguageScope { get; } - - public ITargetBindingResult Bind(TargetObject o) - { - var targetNamePath = "."; - var targetName = _PreferTargetInfo && o.TargetName != null ? o.TargetName : _BindTargetName(_TargetName, !_IgnoreCase, _PreferTargetInfo, o.Value, out targetNamePath); - var targetTypePath = "."; - var targetType = _PreferTargetInfo && o.TargetType != null ? o.TargetType : _BindTargetType(_TargetType, !_IgnoreCase, _PreferTargetInfo, o.Value, out targetTypePath); - var shouldFilter = !(_TypeFilter == null || _TypeFilter.Contains(targetType)); - - // Bind custom fields - var field = BindField(_BindField, new[] { _Field }, !_IgnoreCase, o.Value); - return Bind(targetName, targetNamePath, targetType, targetTypePath, field); - } - - public ITargetBindingResult Bind(object o) - { - var targetName = _BindTargetName(_TargetName, !_IgnoreCase, _PreferTargetInfo, o, out var targetNamePath); - var targetType = _BindTargetType(_TargetType, !_IgnoreCase, _PreferTargetInfo, o, out var targetTypePath); - var shouldFilter = !(_TypeFilter == null || _TypeFilter.Contains(targetType)); - - // Bind custom fields - var field = BindField(_BindField, new[] { _Field }, !_IgnoreCase, o); - return Bind(targetName, targetNamePath, targetType, targetTypePath, field); - } - - private ITargetBindingResult Bind(string targetName, string targetNamePath, string targetType, string targetTypePath, Hashtable field) - { - var shouldFilter = !(_TypeFilter == null || _TypeFilter.Contains(targetType)); - - // Use qualified name - if (_UseQualifiedName) - targetName = string.Concat(targetType, _NameSeparator, targetName); - - return new TargetBindingResult - ( - targetName: targetName, - targetNamePath: targetNamePath, - targetType: targetType, - targetTypePath: targetTypePath, - shouldFilter: shouldFilter, - field: field - ); - } - } - - public bool ShouldFilter { get; private set; } - - /// - /// Bind target object based on the supplied baseline. - /// - public void Bind(TargetObject targetObject) - { - foreach (var bindingContext in _BindingContext.Values) - _BindingResult[bindingContext.LanguageScope] = bindingContext.Bind(targetObject); - } - - public ITargetBindingContext Using(string languageScope) - { - return _BindingContext.TryGetValue(languageScope ?? STANDALONE_SCOPE, out var result) ? result : null; - } - - public ITargetBindingResult Result(string languageScope) - { - return _BindingResult.TryGetValue(languageScope ?? STANDALONE_SCOPE, out var result) ? result : null; - } - - /// - /// Bind additional fields. - /// - private static Hashtable BindField(BindTargetMethod bindField, FieldMap[] map, bool caseSensitive, object o) - { - if (map == null || map.Length == 0) - return null; - - var hashtable = new ImmutableHashtable(); - for (var i = 0; i < map.Length; i++) - { - if (map[i] == null || map[i].Count == 0) - continue; - - foreach (var field in map[i]) - { - if (hashtable.ContainsKey(field.Key)) - continue; - - hashtable.Add(field.Key, bindField(field.Value, caseSensitive, false, o, out _)); - } - } - hashtable.Protect(); - return hashtable; - } -} diff --git a/src/PSRule/Runtime/Binding/ITargetBinder.cs b/src/PSRule/Runtime/Binding/ITargetBinder.cs new file mode 100644 index 0000000000..51b35974d1 --- /dev/null +++ b/src/PSRule/Runtime/Binding/ITargetBinder.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSRule.Pipeline; + +namespace PSRule.Runtime.Binding; + +#nullable enable + +/// +/// Responsible for handling binding for a given target object. +/// +internal interface ITargetBinder +{ + /// + /// Bind to an object. + /// + ITargetBindingResult Bind(TargetObject targetObject); + + /// + /// Bind to an object. + /// + ITargetBindingResult Bind(object targetObject); +} + +#nullable restore diff --git a/src/PSRule/Runtime/Binding/ITargetBindingContext.cs b/src/PSRule/Runtime/Binding/ITargetBindingContext.cs new file mode 100644 index 0000000000..3a60aa0eee --- /dev/null +++ b/src/PSRule/Runtime/Binding/ITargetBindingContext.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSRule.Pipeline; + +namespace PSRule.Runtime.Binding; + +/// +/// A binding context specific to a language scope. +/// +internal interface ITargetBindingContext +{ + ITargetBindingResult Bind(object o); + + ITargetBindingResult Bind(TargetObject o); +} diff --git a/src/PSRule/Runtime/Binding/ITargetBindingResult.cs b/src/PSRule/Runtime/Binding/ITargetBindingResult.cs new file mode 100644 index 0000000000..636b40d9e2 --- /dev/null +++ b/src/PSRule/Runtime/Binding/ITargetBindingResult.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; + +namespace PSRule.Runtime.Binding; + +internal interface ITargetBindingResult +{ + /// + /// The bound TargetName of the target object. + /// + string TargetName { get; } + + string TargetNamePath { get; } + + /// + /// The bound TargetType of the target object. + /// + string TargetType { get; } + + string TargetTypePath { get; } + + /// + /// Additional bound fields of the target object. + /// + Hashtable Field { get; } + + bool ShouldFilter { get; } +} diff --git a/src/PSRule/Runtime/Binding/ImmutableHashtable.cs b/src/PSRule/Runtime/Binding/ImmutableHashtable.cs new file mode 100644 index 0000000000..8e0191bf48 --- /dev/null +++ b/src/PSRule/Runtime/Binding/ImmutableHashtable.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; + +namespace PSRule.Runtime.Binding; + +internal sealed class ImmutableHashtable : Hashtable +{ + private bool _ReadOnly; + + internal ImmutableHashtable() + : base(StringComparer.OrdinalIgnoreCase) { } + + public override bool IsReadOnly => _ReadOnly; + + public override void Add(object key, object value) + { + if (_ReadOnly) + throw new InvalidOperationException(); + + base.Add(key, value); + } + + public override void Clear() + { + if (_ReadOnly) + throw new InvalidOperationException(); + + base.Clear(); + } + + public override void Remove(object key) + { + if (_ReadOnly) + throw new InvalidOperationException(); + + base.Remove(key); + } + + public override object this[object key] + { + get => base[key]; + set + { + if (_ReadOnly) + throw new InvalidOperationException(); + + base[key] = value; + } + } + + internal void Protect() + { + _ReadOnly = true; + } +} diff --git a/src/PSRule/Runtime/Binding/TargetBinder.cs b/src/PSRule/Runtime/Binding/TargetBinder.cs new file mode 100644 index 0000000000..7d46697477 --- /dev/null +++ b/src/PSRule/Runtime/Binding/TargetBinder.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSRule.Pipeline; + +namespace PSRule.Runtime.Binding; + +/// +/// Responsible for handling binding for a given target object. +/// +internal sealed class TargetBinder : ITargetBinder +{ + private readonly ITargetBindingContext _BindingContext; + + internal TargetBinder(ITargetBindingContext bindingContext) + { + _BindingContext = bindingContext; + } + + /// + public ITargetBindingResult Bind(TargetObject targetObject) + { + return _BindingContext.Bind(targetObject); + } + + /// + public ITargetBindingResult Bind(object targetObject) + { + return _BindingContext.Bind(targetObject); + } +} diff --git a/src/PSRule/Runtime/Binding/TargetBinderBuilder.cs b/src/PSRule/Runtime/Binding/TargetBinderBuilder.cs new file mode 100644 index 0000000000..03ae67170f --- /dev/null +++ b/src/PSRule/Runtime/Binding/TargetBinderBuilder.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSRule.Configuration; + +namespace PSRule.Runtime.Binding; + +#nullable enable + +/// +/// Builds a TargetBinder. +/// +internal sealed class TargetBinderBuilder +{ + private readonly List _BindingContext; + private readonly HashSet? _TypeFilter; + private readonly BindTargetMethod _BindTargetName; + private readonly BindTargetMethod _BindTargetType; + private readonly BindTargetMethod _BindField; + + public TargetBinderBuilder(BindTargetMethod bindTargetName, BindTargetMethod bindTargetType, BindTargetMethod bindField, string[]? typeFilter) + { + _BindTargetName = bindTargetName; + _BindTargetType = bindTargetType; + _BindField = bindField; + _BindingContext = []; + if (typeFilter != null && typeFilter.Length > 0) + _TypeFilter = new HashSet(typeFilter, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Build a TargetBinder. + /// + public ITargetBinder Build(BindingOption? bindingOption) + { + return new TargetBinder(new TargetBindingContext(bindingOption, _BindTargetName, _BindTargetType, _BindField, _TypeFilter)); + } +} + +#nullable restore diff --git a/src/PSRule/Runtime/Binding/TargetBindingContext.cs b/src/PSRule/Runtime/Binding/TargetBindingContext.cs new file mode 100644 index 0000000000..03997fffc8 --- /dev/null +++ b/src/PSRule/Runtime/Binding/TargetBindingContext.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using PSRule.Configuration; +using PSRule.Pipeline; + +namespace PSRule.Runtime.Binding; + +#nullable enable + +internal sealed class TargetBindingContext : ITargetBindingContext +{ + private readonly bool _PreferTargetInfo; + private readonly bool _IgnoreCase; + private readonly bool _UseQualifiedName; + private readonly FieldMap? _Field; + private readonly string[]? _TargetName; + private readonly string[]? _TargetType; + private readonly string _NameSeparator; + private readonly BindTargetMethod _BindTargetName; + private readonly BindTargetMethod _BindTargetType; + private readonly BindTargetMethod _BindField; + private readonly HashSet? _TypeFilter; + + public TargetBindingContext(BindingOption? bindingOption, BindTargetMethod bindTargetName, BindTargetMethod bindTargetType, BindTargetMethod bindField, HashSet? typeFilter) + { + _PreferTargetInfo = bindingOption?.PreferTargetInfo ?? BindingOption.Default.PreferTargetInfo!.Value; + _IgnoreCase = bindingOption?.IgnoreCase ?? BindingOption.Default.IgnoreCase!.Value; + _UseQualifiedName = bindingOption?.UseQualifiedName ?? BindingOption.Default.UseQualifiedName!.Value; + _Field = bindingOption?.Field; + _TargetName = bindingOption?.TargetName; + _TargetType = bindingOption?.TargetType; + _NameSeparator = bindingOption?.NameSeparator ?? BindingOption.Default.NameSeparator; + _BindTargetName = bindTargetName; + _BindTargetType = bindTargetType; + _BindField = bindField; + _TypeFilter = typeFilter; + } + + public ITargetBindingResult Bind(TargetObject o) + { + var targetNamePath = "."; + var targetName = _PreferTargetInfo && o.TargetName != null ? o.TargetName : _BindTargetName(_TargetName, !_IgnoreCase, _PreferTargetInfo, o.Value, out targetNamePath); + var targetTypePath = "."; + var targetType = _PreferTargetInfo && o.TargetType != null ? o.TargetType : _BindTargetType(_TargetType, !_IgnoreCase, _PreferTargetInfo, o.Value, out targetTypePath); + var shouldFilter = !(_TypeFilter == null || _TypeFilter.Contains(targetType)); + + // Bind custom fields + var field = BindField(_BindField, [_Field], !_IgnoreCase, o.Value); + return Bind(targetName, targetNamePath, targetType, targetTypePath, field); + } + + public ITargetBindingResult Bind(object o) + { + var targetName = _BindTargetName(_TargetName, !_IgnoreCase, _PreferTargetInfo, o, out var targetNamePath); + var targetType = _BindTargetType(_TargetType, !_IgnoreCase, _PreferTargetInfo, o, out var targetTypePath); + var shouldFilter = !(_TypeFilter == null || _TypeFilter.Contains(targetType)); + + // Bind custom fields + var field = BindField(_BindField, [_Field], !_IgnoreCase, o); + return Bind(targetName, targetNamePath, targetType, targetTypePath, field); + } + + private TargetBindingResult Bind(string targetName, string targetNamePath, string targetType, string targetTypePath, Hashtable? field) + { + var shouldFilter = !(_TypeFilter == null || _TypeFilter.Contains(targetType)); + + // Use qualified name + if (_UseQualifiedName) + targetName = string.Concat(targetType, _NameSeparator, targetName); + + return new TargetBindingResult + ( + targetName: targetName, + targetNamePath: targetNamePath, + targetType: targetType, + targetTypePath: targetTypePath, + shouldFilter: shouldFilter, + field: field + ); + } + + /// + /// Bind additional fields. + /// + private static ImmutableHashtable? BindField(BindTargetMethod bindField, FieldMap?[] map, bool caseSensitive, object o) + { + if (map == null || map.Length == 0) + return null; + + var hashtable = new ImmutableHashtable(); + for (var i = 0; i < map.Length; i++) + { + if (map[i] == null || map[i]!.Count == 0) + continue; + + foreach (var field in map[i]!) + { + if (hashtable.ContainsKey(field.Key)) + continue; + + hashtable.Add(field.Key, bindField(field.Value, caseSensitive, false, o, out _)); + } + } + hashtable.Protect(); + return hashtable; + } +} + +#nullable restore diff --git a/src/PSRule/Runtime/Binding/TargetBindingResult.cs b/src/PSRule/Runtime/Binding/TargetBindingResult.cs new file mode 100644 index 0000000000..15da29b052 --- /dev/null +++ b/src/PSRule/Runtime/Binding/TargetBindingResult.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; + +namespace PSRule.Runtime.Binding; + +#nullable enable + +internal sealed class TargetBindingResult : ITargetBindingResult +{ + public TargetBindingResult(string targetName, string targetNamePath, string targetType, string targetTypePath, bool shouldFilter, Hashtable? field) + { + TargetName = targetName; + TargetNamePath = targetNamePath; + TargetType = targetType; + TargetTypePath = targetTypePath; + ShouldFilter = shouldFilter; + Field = field; + } + + /// + public string TargetName { get; } + + /// + public string TargetNamePath { get; } + + /// + public string TargetType { get; } + + /// + public string TargetTypePath { get; } + + /// + public bool ShouldFilter { get; } + + /// + public Hashtable? Field { get; } +} + +#nullable restore diff --git a/src/PSRule/Runtime/ILanguageScope.cs b/src/PSRule/Runtime/ILanguageScope.cs index 50c7907b19..8ad2e13b83 100644 --- a/src/PSRule/Runtime/ILanguageScope.cs +++ b/src/PSRule/Runtime/ILanguageScope.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using PSRule.Configuration; using PSRule.Definitions; using PSRule.Pipeline; +using PSRule.Runtime.Binding; namespace PSRule.Runtime; @@ -20,7 +20,7 @@ internal interface ILanguageScope : IDisposable /// string Name { get; } - BindingOption Binding { get; } + StringComparer GetBindingComparer(); /// /// Get an ordered culture preference list which will be tries for finding help. @@ -54,6 +54,10 @@ internal interface ILanguageScope : IDisposable /// object? GetService(string name); + ITargetBindingResult? Bind(TargetObject targetObject); + + ITargetBindingResult? Bind(object targetObject); + /// /// Try to bind the type of the object. /// @@ -63,11 +67,6 @@ internal interface ILanguageScope : IDisposable /// Try to bind the name of the object. /// bool TryGetName(object o, out string? name, out string? path); - - /// - /// Try to bind the scope of the object. - /// - bool TryGetScope(object o, out string[]? scope); } #nullable restore diff --git a/src/PSRule/Runtime/LanguageScope.cs b/src/PSRule/Runtime/LanguageScope.cs index 7a5bde8596..0ac67cd9e5 100644 --- a/src/PSRule/Runtime/LanguageScope.cs +++ b/src/PSRule/Runtime/LanguageScope.cs @@ -5,61 +5,65 @@ using PSRule.Configuration; using PSRule.Definitions; using PSRule.Pipeline; +using PSRule.Runtime.Binding; namespace PSRule.Runtime; +#nullable enable + [DebuggerDisplay("{Name}")] internal sealed class LanguageScope : ILanguageScope { - private readonly RunspaceContext _Context; - private IDictionary _Configuration; + private IDictionary? _Configuration; private readonly Dictionary _Service; private readonly Dictionary _Filter; + private ITargetBinder? _TargetBinder; + private StringComparer? _BindingComparer; private bool _Disposed; - public LanguageScope(RunspaceContext context, string name) + public LanguageScope(string name) { - _Context = context; Name = ResourceHelper.NormalizeScope(name); - //_Configuration = new Dictionary(); - _Filter = new Dictionary(); - _Service = new Dictionary(); + _Filter = []; + _Service = []; } /// public string Name { [DebuggerStepThrough] get; } - /// - public BindingOption Binding { [DebuggerStepThrough] get; [DebuggerStepThrough] private set; } - /// public string[] Culture { [DebuggerStepThrough] get; [DebuggerStepThrough] private set; } + public StringComparer GetBindingComparer() => _BindingComparer ?? StringComparer.OrdinalIgnoreCase; + /// public void Configure(Dictionary configuration) { + _Configuration ??= new Dictionary(); _Configuration.AddUnique(configuration); } /// public void Configure(OptionContext context) { - if (context == null) - throw new ArgumentNullException(nameof(context)); + if (context == null) throw new ArgumentNullException(nameof(context)); _Configuration = context.Configuration; WithFilter(context.RuleFilter); WithFilter(context.ConventionFilter); - Binding = context.Binding; + _BindingComparer = context.Binding.GetComparer(); Culture = context.Output.Culture; + + var builder = new TargetBinderBuilder(context.BindTargetName, context.BindTargetType, context.BindField, context.InputTargetType); + _TargetBinder = builder.Build(context.Binding); } /// - public bool TryConfigurationValue(string key, out object value) + public bool TryConfigurationValue(string key, out object? value) { value = null; - return !string.IsNullOrEmpty(key) && _Configuration.TryGetValue(key, out value); + return !string.IsNullOrEmpty(key) && _Configuration != null && _Configuration.TryGetValue(key, out value); } /// @@ -69,7 +73,7 @@ public void WithFilter(IResourceFilter resourceFilter) } /// - public IResourceFilter GetFilter(ResourceKind kind) + public IResourceFilter? GetFilter(ResourceKind kind) { return _Filter.TryGetValue(kind, out var filter) ? filter : null; } @@ -84,61 +88,48 @@ public void AddService(string name, object service) } /// - public object GetService(string name) + public object? GetService(string name) { return _Service.TryGetValue(name, out var service) ? service : null; } - public bool TryGetType(object o, out string type, out string path) + public ITargetBindingResult? Bind(TargetObject targetObject) { - if (_Context != null && _Context.TargetObject.Value == o) - { - var binding = _Context.TargetBinder.Result(Name); - type = binding.TargetType; - path = binding.TargetTypePath; - return true; - } - else if (_Context != null) - { - var binding = _Context.TargetBinder.Using(Name).Bind(o); - type = binding.TargetType; - path = binding.TargetTypePath; - return true; - } - type = null; - path = null; - return false; + return _TargetBinder?.Bind(targetObject); } - public bool TryGetName(object o, out string name, out string path) + public ITargetBindingResult? Bind(object targetObject) { - if (_Context != null && _Context.TargetObject.Value == o) - { - var binding = _Context.TargetBinder.Result(Name); - name = binding.TargetName; - path = binding.TargetNamePath; - return true; - } - else if (_Context != null) + return _TargetBinder?.Bind(targetObject); + } + + /// + public bool TryGetType(object o, out string? type, out string? path) + { + if (_TargetBinder != null) { - var binding = _Context.TargetBinder.Using(Name).Bind(o); - name = binding.TargetName; - path = binding.TargetNamePath; + var result = _TargetBinder.Bind(o); + type = result.TargetType; + path = result.TargetTypePath; return true; } - name = null; + type = null; path = null; return false; } - public bool TryGetScope(object o, out string[] scope) + /// + public bool TryGetName(object o, out string? name, out string? path) { - if (_Context != null && _Context.TargetObject.Value == o) + if (_TargetBinder != null) { - scope = _Context.TargetObject.Scope; + var result = _TargetBinder.Bind(o); + name = result.TargetName; + path = result.TargetNamePath; return true; } - scope = null; + name = null; + path = null; return false; } @@ -170,3 +161,5 @@ public void Dispose() GC.SuppressFinalize(this); } } + +#nullable restore diff --git a/src/PSRule/Runtime/LanguageScopeSet.cs b/src/PSRule/Runtime/LanguageScopeSet.cs index de64c44ee2..89457d3fbc 100644 --- a/src/PSRule/Runtime/LanguageScopeSet.cs +++ b/src/PSRule/Runtime/LanguageScopeSet.cs @@ -10,15 +10,13 @@ namespace PSRule.Runtime; /// internal sealed class LanguageScopeSet : IDisposable { - private readonly RunspaceContext _Context; private readonly Dictionary _Scopes; private ILanguageScope _Current; private bool _Disposed; - public LanguageScopeSet(RunspaceContext context) + public LanguageScopeSet() { - _Context = context; _Scopes = new Dictionary(StringComparer.OrdinalIgnoreCase); Import(null, out _Current); } @@ -93,7 +91,7 @@ internal bool Import(string name, out ILanguageScope scope) if (_Scopes.TryGetValue(GetScopeName(name), out scope)) return false; - scope = new LanguageScope(_Context, name); + scope = new LanguageScope(name); Add(scope); return true; } diff --git a/src/PSRule/Runtime/RunspaceContext.cs b/src/PSRule/Runtime/RunspaceContext.cs index 0f061d8d14..3dda929de2 100644 --- a/src/PSRule/Runtime/RunspaceContext.cs +++ b/src/PSRule/Runtime/RunspaceContext.cs @@ -10,6 +10,7 @@ using PSRule.Pipeline; using PSRule.Resources; using PSRule.Rules; +using PSRule.Runtime.Binding; using static PSRule.Pipeline.PipelineContext; namespace PSRule.Runtime; @@ -92,7 +93,7 @@ internal RunspaceContext(PipelineContext pipeline, IPipelineWriter writer) _RuleTimer = new Stopwatch(); _Reason = []; _Conventions = []; - _LanguageScopes = new LanguageScopeSet(this); + _LanguageScopes = new LanguageScopeSet(); _Scope = new Stack(); } @@ -548,22 +549,20 @@ internal void EnterLanguageScope(ISourceFile file) { // TODO: Look at scope caching, and a scope stack. - if (Source != null && Source.File == file) - return; - if (!file.Exists()) throw new FileNotFoundException(PSRuleResources.ScriptNotFound, file.Path); _LanguageScopes.UseScope(file.Module); - // Change scope - //Pipeline.Baseline.ResetScope(moduleName: source.Module); + if (TargetObject != null && LanguageScope != null) + Binding = LanguageScope.Bind(TargetObject); + Source = new SourceScope(file); } internal void ExitLanguageScope(ISourceFile file) { - // Look at scope poping and validation. + // Look at scope popping and validation. Source = null; } @@ -575,7 +574,6 @@ internal void EnterTargetObject(TargetObject targetObject) { _ObjectNumber++; TargetObject = targetObject; - TargetBinder?.Bind(TargetObject); if (Pipeline.ContentCache.Count > 0) Pipeline.ContentCache.Clear(); @@ -587,6 +585,7 @@ public void ExitTargetObject() { RunConventionProcess(); TargetObject = null; + Binding = null; } public bool TrySelector(string name) @@ -613,7 +612,7 @@ public bool TrySelector(ResourceId id) /// public RuleRecord EnterRuleBlock(RuleBlock ruleBlock) { - Binding = TargetBinder?.Result(ruleBlock.Info.ModuleName); + EnterLanguageScope(ruleBlock.Source); _RuleErrors = 0; RuleBlock = ruleBlock; @@ -641,7 +640,7 @@ public RuleRecord EnterRuleBlock(RuleBlock ruleBlock) /// /// Exit the rule block scope. /// - public void ExitRuleBlock() + public void ExitRuleBlock(RuleBlock ruleBlock) { // Stop rule execution time _RuleTimer.Stop(); @@ -663,6 +662,8 @@ public void ExitRuleBlock() RuleBlock = null; _RuleErrors = 0; _Reason.Clear(); + + ExitLanguageScope(ruleBlock.Source); } internal void Import(IConvention resource) @@ -786,21 +787,6 @@ public void Begin() { Pipeline.Begin(this); - var builder = new TargetBinderBuilder( - Pipeline.BindTargetName, - Pipeline.BindTargetType, - Pipeline.BindField, - Pipeline.Option.Input.TargetType); - - HashSet? _TypeFilter = null; - if (Pipeline.Option.Input.TargetType != null && Pipeline.Option.Input.TargetType.Length > 0) - _TypeFilter = new HashSet(Pipeline.Option.Input.TargetType, StringComparer.OrdinalIgnoreCase); - - foreach (var languageScope in _LanguageScopes.Get()) - { - builder.With(new TargetBinder.TargetBindingContext(languageScope.Name, languageScope.Binding, Pipeline.BindTargetName, Pipeline.BindTargetType, Pipeline.BindField, _TypeFilter)); - } - TargetBinder = builder.Build(); RunConventionInitialize(); } @@ -810,6 +796,20 @@ public void End(IEnumerable output) RunConventionEnd(); } + /// + /// Try to bind the scope of the object. + /// + public bool TryGetScope(object o, out string[]? scope) + { + if (TargetObject != null && TargetObject.Value == o) + { + scope = TargetObject.Scope; + return true; + } + scope = null; + return false; + } + public string? GetLocalizedPath(string file, out string? culture) { culture = null; diff --git a/tests/PSRule.Tests/Binding/TargetBinderTests.cs b/tests/PSRule.Tests/Binding/TargetBinderTests.cs new file mode 100644 index 0000000000..1a0e00bc19 --- /dev/null +++ b/tests/PSRule.Tests/Binding/TargetBinderTests.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; +using Newtonsoft.Json.Linq; +using PSRule.Configuration; +using PSRule.Pipeline; +using PSRule.Runtime.Binding; +using static Xunit.Assert; + +namespace PSRule.Binding; + +#nullable enable + +public sealed class TargetBinderTests +{ + private static readonly BindingOption Module1_Binding = new() + { + TargetName = ["name"], + TargetType = ["type"] + }; + + private static readonly BindingOption Module2_Binding = new() + { + TargetName = ["AlternativeName"], + TargetType = ["type"] + }; + + private static readonly BindingOption Module3_Binding = new() + { + TargetName = ["name"], + TargetType = ["type"], + PreferTargetInfo = true + }; + + + [Fact] + public void BindTargetObject() + { + var targetObject = GetTargetObject(); + + var m0 = GetBinder(null).Bind(targetObject); + Equal("Name1", m0.TargetName); + Equal("System.Management.Automation.PSCustomObject", m0.TargetType); + + var m1 = GetBinder(Module1_Binding).Bind(targetObject); + Equal("Name1", m1.TargetName); + Equal("Type1", m1.TargetType); + + var m2 = GetBinder(Module2_Binding).Bind(targetObject); + Equal("Name2", m2.TargetName); + Equal("Type1", m2.TargetType); + + // With specified type + targetObject = GetTargetObject(targetType: "ManualType"); + + m1 = GetBinder(Module1_Binding).Bind(targetObject); + Equal("Name1", m1.TargetName); + Equal("Type1", m1.TargetType); + + var m3 = GetBinder(Module3_Binding).Bind(targetObject); + Equal("Name1", m3.TargetName); + Equal("ManualType", m3.TargetType); + } + + [Fact] + public void BindJObject() + { + var targetObject = new TargetObject(PSObject.AsPSObject(JToken.Parse("{ \"name\": \"Name1\", \"type\": \"Type1\", \"AlternativeName\": \"Name2\", \"AlternativeType\": \"Type2\" }"))); + + var m0 = GetBinder(null).Bind(targetObject); + Equal("Name1", m0.TargetName); + Equal("System.Management.Automation.PSCustomObject", m0.TargetType); + + var m1 = GetBinder(Module1_Binding).Bind(targetObject); + Equal("Name1", m1.TargetName); + Equal("Type1", m1.TargetType); + + var m2 = GetBinder(Module2_Binding).Bind(targetObject); + Equal("Name2", m2.TargetName); + Equal("Type1", m2.TargetType); + } + + #region Helper methods + + private static TargetObject GetTargetObject(string? targetType = null) + { + var pso = new PSObject(); + pso.Properties.Add(new PSNoteProperty("name", "Name1")); + pso.Properties.Add(new PSNoteProperty("type", "Type1")); + pso.Properties.Add(new PSNoteProperty("AlternativeName", "Name2")); + pso.Properties.Add(new PSNoteProperty("AlternativeType", "Type2")); + return new TargetObject(pso, targetType: targetType); + } + + private static ITargetBinder GetBinder(BindingOption? bindingOption) + { + var builder = new TargetBinderBuilder(PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, null); + return builder.Build(bindingOption); + } + + #endregion Helper methods +} + +#nullable restore diff --git a/tests/PSRule.Tests/MockLanguageScope.cs b/tests/PSRule.Tests/MockLanguageScope.cs index aa775a020d..05c18d73c4 100644 --- a/tests/PSRule.Tests/MockLanguageScope.cs +++ b/tests/PSRule.Tests/MockLanguageScope.cs @@ -7,6 +7,7 @@ using PSRule.Definitions.Rules; using PSRule.Pipeline; using PSRule.Runtime; +using PSRule.Runtime.Binding; namespace PSRule; @@ -21,15 +22,25 @@ public MockLanguageScope(string name) public string Name { get; } - public BindingOption Binding => throw new System.NotImplementedException(); + public BindingOption Binding => throw new NotImplementedException(); - public string[] Culture => throw new System.NotImplementedException(); + public string[] Culture => throw new NotImplementedException(); public void AddService(string name, object service) { } + public ITargetBindingResult Bind(TargetObject targetObject) + { + throw new NotImplementedException(); + } + + public ITargetBindingResult Bind(object targetObject) + { + throw new NotImplementedException(); + } + public void Configure(OptionContext context) { throw new NotImplementedException(); @@ -40,34 +51,34 @@ public void Dispose() } + public StringComparer GetBindingComparer() + { + throw new NotImplementedException(); + } + public IResourceFilter GetFilter(ResourceKind kind) { - throw new System.NotImplementedException(); + throw new NotImplementedException(); } public object GetService(string name) { - throw new System.NotImplementedException(); + throw new NotImplementedException(); } public bool TryConfigurationValue(string key, out object value) { - throw new System.NotImplementedException(); + throw new NotImplementedException(); } public bool TryGetName(object o, out string name, out string path) { - throw new System.NotImplementedException(); - } - - public bool TryGetScope(object o, out string[] scope) - { - throw new System.NotImplementedException(); + throw new NotImplementedException(); } public bool TryGetType(object o, out string type, out string path) { - throw new System.NotImplementedException(); + throw new NotImplementedException(); } public void WithFilter(IResourceFilter resourceFilter) diff --git a/tests/PSRule.Tests/OptionContextTests.cs b/tests/PSRule.Tests/OptionContextTests.cs index d224ad847c..b5082ea804 100644 --- a/tests/PSRule.Tests/OptionContextTests.cs +++ b/tests/PSRule.Tests/OptionContextTests.cs @@ -24,7 +24,7 @@ public void Build() var builder = new OptionContextBuilder(GetOption()); // Check empty scope - var testScope = new LanguageScope(null, "Empty"); + var testScope = new LanguageScope("Empty"); testScope.Configure(builder.Build(testScope.Name)); Assert.Equal(new string[] { "en-ZZ" }, testScope.Culture); } @@ -35,7 +35,7 @@ public void Order() // Create option context var builder = new OptionContextBuilder(GetOption()); - var localScope = new LanguageScope(null, null); + var localScope = new LanguageScope(null); localScope.Configure(builder.Build(null)); var ruleFilter = localScope.GetFilter(ResourceKind.Rule) as RuleFilter; diff --git a/tests/PSRule.Tests/RulesTests.cs b/tests/PSRule.Tests/RulesTests.cs index d8ac191bd8..0e28560832 100644 --- a/tests/PSRule.Tests/RulesTests.cs +++ b/tests/PSRule.Tests/RulesTests.cs @@ -60,7 +60,7 @@ public void ReadYamlRule() [Fact] public void ReadYamlSubSelectorRule() { - var context = new RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, new OptionContextBuilder(), null), new TestWriter(GetOption())); + var context = new RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, GetOptionBuilder(), null), new TestWriter(GetOption())); context.Init(GetSource("FromFileSubSelector.Rule.yaml")); context.Begin(); @@ -132,7 +132,7 @@ public void ReadYamlSubSelectorRule() [Fact] public void EvaluateYamlRule() { - var context = new RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, new OptionContextBuilder(), null), new TestWriter(GetOption())); + var context = new RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, GetOptionBuilder(), null), new TestWriter(GetOption())); context.Init(GetSource()); context.Begin(); ImportSelectors(context); @@ -195,7 +195,7 @@ public void EvaluateYamlRule() [Fact] public void RuleWithObjectPath() { - var context = new RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, new OptionContextBuilder(), null), new TestWriter(GetOption())); + var context = new RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, GetOptionBuilder(), null), new TestWriter(GetOption())); context.Init(GetSource()); context.Begin(); ImportSelectors(context); @@ -263,7 +263,7 @@ public void ReadJsonRule() [Fact] public void ReadJsonSubSelectorRule() { - var context = new RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, new OptionContextBuilder(), null), new TestWriter(GetOption())); + var context = new RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, GetOptionBuilder(), null), new TestWriter(GetOption())); context.Init(GetSource("FromFileSubSelector.Rule.jsonc")); context.Begin(); @@ -341,6 +341,11 @@ private static PSRuleOption GetOption() return new PSRuleOption(); } + private static OptionContextBuilder GetOptionBuilder() + { + return new OptionContextBuilder(option: GetOption(), bindTargetName: PipelineHookActions.BindTargetName, bindTargetType: PipelineHookActions.BindTargetType, bindField: PipelineHookActions.BindField); + } + private static Source[] GetSource(string path = "FromFile.Rule.yaml") { var builder = new SourcePipelineBuilder(null, null); diff --git a/tests/PSRule.Tests/SelectorTests.cs b/tests/PSRule.Tests/SelectorTests.cs index 2ea819a585..cf23d27b87 100644 --- a/tests/PSRule.Tests/SelectorTests.cs +++ b/tests/PSRule.Tests/SelectorTests.cs @@ -967,6 +967,7 @@ public void ContainsExpression(string type, string path) ); context.EnterTargetObject(new TargetObject(actual1)); + context.EnterLanguageScope(withName.Source); Assert.True(withName.Match(actual1)); context.EnterTargetObject(new TargetObject(actual2)); @@ -1873,7 +1874,7 @@ private static PSObject GetObject(params (string name, object value)[] propertie private static SelectorVisitor GetSelectorVisitor(string name, Source[] source, out RunspaceContext context) { - var builder = new OptionContextBuilder(GetOption()); + var builder = new OptionContextBuilder(GetOption(), bindTargetName: PipelineHookActions.BindTargetName, bindTargetType: PipelineHookActions.BindTargetType, bindField: PipelineHookActions.BindField); context = new RunspaceContext(PipelineContext.New(GetOption(), null, null, PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, builder, null), null); context.Init(source); context.Begin(); diff --git a/tests/PSRule.Tests/TargetBinderTests.cs b/tests/PSRule.Tests/TargetBinderTests.cs deleted file mode 100644 index 92411232cf..0000000000 --- a/tests/PSRule.Tests/TargetBinderTests.cs +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Management.Automation; -using Newtonsoft.Json.Linq; -using PSRule.Configuration; -using PSRule.Definitions.ModuleConfigs; -using PSRule.Pipeline; - -namespace PSRule; - -public sealed class TargetBinderTests -{ - [Fact] - public void BindTargetObject() - { - var binder = GetBinder(); - var targetObject = GetTargetObject(); - binder.Bind(targetObject); - - var m1 = binder.Result("Module1"); - Assert.Equal("Name1", m1.TargetName); - Assert.Equal("Type1", m1.TargetType); - - var m2 = binder.Result("Module2"); - Assert.Equal("Name2", m2.TargetName); - Assert.Equal("Type1", m2.TargetType); - - var m0 = binder.Result("."); - Assert.Equal("Name1", m0.TargetName); - Assert.Equal("System.Management.Automation.PSCustomObject", m0.TargetType); - - // With specified type - targetObject = GetTargetObject(targetType: "ManualType"); - binder.Bind(targetObject); - - m1 = binder.Result("Module1"); - Assert.Equal("Name1", m1.TargetName); - Assert.Equal("Type1", m1.TargetType); - - var m3 = binder.Result("Module3"); - Assert.Equal("Name1", m3.TargetName); - Assert.Equal("ManualType", m3.TargetType); - } - - [Fact] - public void BindJObject() - { - var binder = GetBinder(); - var targetObject = new TargetObject(PSObject.AsPSObject(JToken.Parse("{ \"name\": \"Name1\", \"type\": \"Type1\", \"AlternativeName\": \"Name2\", \"AlternativeType\": \"Type2\" }"))); - binder.Bind(targetObject); - - var m1 = binder.Result("Module1"); - Assert.Equal("Name1", m1.TargetName); - Assert.Equal("Type1", m1.TargetType); - - var m2 = binder.Result("Module2"); - Assert.Equal("Name2", m2.TargetName); - Assert.Equal("Type1", m2.TargetType); - - var m0 = binder.Result("."); - Assert.Equal("Name1", m0.TargetName); - Assert.Equal("System.Management.Automation.PSCustomObject", m0.TargetType); - } - - #region Helper methods - - private static TargetObject GetTargetObject(string targetType = null) - { - var pso = new PSObject(); - pso.Properties.Add(new PSNoteProperty("name", "Name1")); - pso.Properties.Add(new PSNoteProperty("type", "Type1")); - pso.Properties.Add(new PSNoteProperty("AlternativeName", "Name2")); - pso.Properties.Add(new PSNoteProperty("AlternativeType", "Type2")); - return new TargetObject(pso, targetType: targetType); - } - - private static ITargetBinder GetBinder() - { - var builder = new TargetBinderBuilder(PipelineHookActions.BindTargetName, PipelineHookActions.BindTargetType, PipelineHookActions.BindField, null); - var option = new OptionContextBuilder(); - - option.ModuleConfig("Module1", new ModuleConfigV1Spec - { - Binding = new BindingOption - { - TargetName = ["name"], - TargetType = ["type"] - } - }); - - option.ModuleConfig("Module2", new ModuleConfigV1Spec - { - Binding = new BindingOption - { - TargetName = ["AlternativeName"], - TargetType = ["type"] - } - }); - - option.ModuleConfig("Module3", new ModuleConfigV1Spec - { - Binding = new BindingOption - { - TargetName = ["name"], - TargetType = ["type"], - PreferTargetInfo = true - } - }); - - var scopes = new Runtime.LanguageScopeSet(null); - - scopes.Import("Module1", out var module1); - module1.Configure(option.Build(module1.Name)); - builder.With(module1); - - scopes.Import("Module2", out var module2); - module2.Configure(option.Build(module2.Name)); - builder.With(module2); - - scopes.Import("Module3", out var module3); - module3.Configure(option.Build(module3.Name)); - builder.With(module3); - - scopes.Import(".", out var local); - local.Configure(option.Build(local.Name)); - builder.With(local); - return builder.Build(); - } - - #endregion Helper methods -}