From d2c419126c2147b733997894a93a38749d356625 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Tue, 12 Nov 2024 14:41:52 +1000 Subject: [PATCH] Refactoring of language scopes (#2601) --- docs/CHANGELOG-v3.md | 4 +- mkdocs.yml | 1 + src/PSRule/Host/HostHelper.cs | 81 ++++++++++++++----- src/PSRule/Pipeline/PipelineContext.cs | 7 ++ src/PSRule/Runtime/ILanguageScope.cs | 2 +- .../Runtime/ILanguageScopeCollection.cs | 13 +++ src/PSRule/Runtime/LanguageScope.cs | 14 ++-- src/PSRule/Runtime/LanguageScopeSet.cs | 34 ++------ src/PSRule/Runtime/RunspaceContext.cs | 31 +++---- tests/PSRule.Tests/FunctionTests.cs | 1 + tests/PSRule.Tests/PSRuleOptionTests.cs | 1 + tests/PSRule.Tests/SelectorTests.cs | 1 + 12 files changed, 122 insertions(+), 68 deletions(-) create mode 100644 src/PSRule/Runtime/ILanguageScopeCollection.cs diff --git a/docs/CHANGELOG-v3.md b/docs/CHANGELOG-v3.md index ca1f69e797..9b5d5aef3b 100644 --- a/docs/CHANGELOG-v3.md +++ b/docs/CHANGELOG-v3.md @@ -32,8 +32,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers [#2552](https://github.com/microsoft/PSRule/issues/2552) - Modules are automatically restored unless `--no-restore` is used with the `run` command. - Engineering: - - Bump YamlDotNet to v16.1.3. - [#1874](https://github.com/microsoft/PSRule/pull/1874) + - Bump YamlDotNet to v16.2.0. + [#2596](https://github.com/microsoft/PSRule/pull/2596) ## v3.0.0-B0275 (pre-release) diff --git a/mkdocs.yml b/mkdocs.yml index fe4c5264ca..71165f555e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -104,6 +104,7 @@ nav: - Index: concepts/cli/index.md - run: concepts/cli/run.md - module: concepts/cli/module.md + - restore: concepts/cli/restore.md - Assertion methods: concepts/PSRule/en-US/about_PSRule_Assert.md - Baselines: concepts/PSRule/en-US/about_PSRule_Baseline.md - Badges: concepts/PSRule/en-US/about_PSRule_Badges.md diff --git a/src/PSRule/Host/HostHelper.cs b/src/PSRule/Host/HostHelper.cs index dcbfdb9aef..259a6eed70 100644 --- a/src/PSRule/Host/HostHelper.cs +++ b/src/PSRule/Host/HostHelper.cs @@ -658,8 +658,7 @@ private static Baseline[] ToBaselineV1(IEnumerable blocks, Runsp private static SuppressionGroupV1[] ToSuppressionGroupV1(IEnumerable blocks, RunspaceContext context) { - if (blocks == null) - return Array.Empty(); + if (blocks == null) return []; // Index suppression groups by Id. var results = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -682,13 +681,12 @@ private static SuppressionGroupV1[] ToSuppressionGroupV1(IEnumerable blocks, RunspaceContext context) { - if (blocks == null) - return Array.Empty(); + if (blocks == null) return []; // Index configurations by Name. var results = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -766,39 +764,86 @@ private static void Import(IConvention[] blocks, RunspaceContext context) private static bool Match(RunspaceContext context, RuleBlock resource) { - var filter = context.LanguageScope.GetFilter(ResourceKind.Rule); - return filter == null || filter.Match(resource); + try + { + context.EnterLanguageScope(resource.Source); + var filter = context.LanguageScope.GetFilter(ResourceKind.Rule); + return filter == null || filter.Match(resource); + } + finally + { + context.ExitLanguageScope(resource.Source); + } } private static bool Match(RunspaceContext context, IRuleV1 resource) { - context.EnterLanguageScope(resource.Source); - var filter = context.LanguageScope.GetFilter(ResourceKind.Rule); - return filter == null || filter.Match(resource); + try + { + context.EnterLanguageScope(resource.Source); + var filter = context.LanguageScope.GetFilter(ResourceKind.Rule); + return filter == null || filter.Match(resource); + } + finally + { + context.ExitLanguageScope(resource.Source); + } } private static bool Match(RunspaceContext context, Baseline resource) { - var filter = context.LanguageScope.GetFilter(ResourceKind.Baseline); - return filter == null || filter.Match(resource); + try + { + context.EnterLanguageScope(resource.Source); + var filter = context.LanguageScope.GetFilter(ResourceKind.Baseline); + return filter == null || filter.Match(resource); + } + finally + { + context.ExitLanguageScope(resource.Source); + } } private static bool Match(RunspaceContext context, ScriptBlockConvention block) { - var filter = context.LanguageScope.GetFilter(ResourceKind.Convention); - return filter == null || filter.Match(block); + try + { + context.EnterLanguageScope(block.Source); + var filter = context.LanguageScope.GetFilter(ResourceKind.Convention); + return filter == null || filter.Match(block); + } + finally + { + context.ExitLanguageScope(block.Source); + } } private static bool Match(RunspaceContext context, SelectorV1 resource) { - var filter = context.LanguageScope.GetFilter(ResourceKind.Selector); - return filter == null || filter.Match(resource); + try + { + context.EnterLanguageScope(resource.Source); + var filter = context.LanguageScope.GetFilter(ResourceKind.Selector); + return filter == null || filter.Match(resource); + } + finally + { + context.ExitLanguageScope(resource.Source); + } } private static bool Match(RunspaceContext context, SuppressionGroupV1 suppressionGroup) { - var filter = context.LanguageScope.GetFilter(ResourceKind.SuppressionGroup); - return filter == null || filter.Match(suppressionGroup); + try + { + context.EnterLanguageScope(suppressionGroup.Source); + var filter = context.LanguageScope.GetFilter(ResourceKind.SuppressionGroup); + return filter == null || filter.Match(suppressionGroup); + } + finally + { + //context.ExitLanguageScope(suppressionGroup.Source); + } } private static IConvention[] Sort(RunspaceContext context, IConvention[] conventions) diff --git a/src/PSRule/Pipeline/PipelineContext.cs b/src/PSRule/Pipeline/PipelineContext.cs index bb72fa918b..7997c31c43 100644 --- a/src/PSRule/Pipeline/PipelineContext.cs +++ b/src/PSRule/Pipeline/PipelineContext.cs @@ -66,6 +66,11 @@ internal sealed class PipelineContext : IPipelineContext, IBindingContext public IPipelineWriter Writer { get; } + /// + /// A collection of languages scopes for this pipeline. + /// + public ILanguageScopeCollection LanguageScopes { get; } + private PipelineContext(PSRuleOption option, IHostContext hostContext, PipelineInputStream reader, IPipelineWriter writer, OptionContextBuilder optionBuilder, IList unresolved) { _OptionBuilder = optionBuilder; @@ -87,6 +92,7 @@ private PipelineContext(PSRuleOption option, IHostContext hostContext, PipelineI RunId = Environment.GetRunId() ?? ObjectHashAlgorithm.GetDigest(Guid.NewGuid().ToByteArray()); RunTime = Stopwatch.StartNew(); _DefaultOptionContext = _OptionBuilder?.Build(null); + LanguageScopes = new LanguageScopeSet(); } public static PipelineContext New(PSRuleOption option, IHostContext hostContext, PipelineInputStream reader, IPipelineWriter writer, OptionContextBuilder optionBuilder, IList unresolved) @@ -299,6 +305,7 @@ private void Dispose(bool disposing) ObjectHashAlgorithm?.Dispose(); _Runspace?.Dispose(); _PathExpressionCache.Clear(); + LanguageScopes.Dispose(); LocalizedDataCache.Clear(); ExpressionCache.Clear(); ContentCache.Clear(); diff --git a/src/PSRule/Runtime/ILanguageScope.cs b/src/PSRule/Runtime/ILanguageScope.cs index 8ad2e13b83..f0caba0788 100644 --- a/src/PSRule/Runtime/ILanguageScope.cs +++ b/src/PSRule/Runtime/ILanguageScope.cs @@ -25,7 +25,7 @@ internal interface ILanguageScope : IDisposable /// /// Get an ordered culture preference list which will be tries for finding help. /// - string[] Culture { get; } + string[]? Culture { get; } void Configure(OptionContext context); diff --git a/src/PSRule/Runtime/ILanguageScopeCollection.cs b/src/PSRule/Runtime/ILanguageScopeCollection.cs new file mode 100644 index 0000000000..9f440e01ba --- /dev/null +++ b/src/PSRule/Runtime/ILanguageScopeCollection.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Runtime; + +internal interface ILanguageScopeCollection : IDisposable +{ + IEnumerable Get(); + + bool TryScope(string name, out ILanguageScope scope); + + bool Import(string name); +} diff --git a/src/PSRule/Runtime/LanguageScope.cs b/src/PSRule/Runtime/LanguageScope.cs index 0ac67cd9e5..5a3bfbdd72 100644 --- a/src/PSRule/Runtime/LanguageScope.cs +++ b/src/PSRule/Runtime/LanguageScope.cs @@ -24,6 +24,7 @@ internal sealed class LanguageScope : ILanguageScope public LanguageScope(string name) { + _Configuration = new Dictionary(StringComparer.OrdinalIgnoreCase); Name = ResourceHelper.NormalizeScope(name); _Filter = []; _Service = []; @@ -33,7 +34,7 @@ public LanguageScope(string name) public string Name { [DebuggerStepThrough] get; } /// - public string[] Culture { [DebuggerStepThrough] get; [DebuggerStepThrough] private set; } + public string[]? Culture { [DebuggerStepThrough] get; [DebuggerStepThrough] private set; } public StringComparer GetBindingComparer() => _BindingComparer ?? StringComparer.OrdinalIgnoreCase; @@ -50,6 +51,7 @@ public void Configure(OptionContext context) if (context == null) throw new ArgumentNullException(nameof(context)); _Configuration = context.Configuration; + _Configuration ??= new Dictionary(); WithFilter(context.RuleFilter); WithFilter(context.ConventionFilter); _BindingComparer = context.Binding.GetComparer(); @@ -62,7 +64,7 @@ public void Configure(OptionContext context) /// public bool TryConfigurationValue(string key, out object? value) { - value = null; + value = default; return !string.IsNullOrEmpty(key) && _Configuration != null && _Configuration.TryGetValue(key, out value); } @@ -113,8 +115,8 @@ public bool TryGetType(object o, out string? type, out string? path) path = result.TargetTypePath; return true; } - type = null; - path = null; + type = default; + path = default; return false; } @@ -128,8 +130,8 @@ public bool TryGetName(object o, out string? name, out string? path) path = result.TargetNamePath; return true; } - name = null; - path = null; + name = default; + path = default; return false; } diff --git a/src/PSRule/Runtime/LanguageScopeSet.cs b/src/PSRule/Runtime/LanguageScopeSet.cs index 89457d3fbc..06e82dfa51 100644 --- a/src/PSRule/Runtime/LanguageScopeSet.cs +++ b/src/PSRule/Runtime/LanguageScopeSet.cs @@ -8,7 +8,7 @@ namespace PSRule.Runtime; /// /// A collection of . /// -internal sealed class LanguageScopeSet : IDisposable +internal sealed class LanguageScopeSet : ILanguageScopeCollection { private readonly Dictionary _Scopes; @@ -18,15 +18,7 @@ internal sealed class LanguageScopeSet : IDisposable public LanguageScopeSet() { _Scopes = new Dictionary(StringComparer.OrdinalIgnoreCase); - Import(null, out _Current); - } - - public ILanguageScope Current - { - get - { - return _Current; - } + Import(null); } #region IDisposable @@ -64,34 +56,22 @@ internal void Add(ILanguageScope languageScope) _Scopes.Add(languageScope.Name, languageScope); } - internal IEnumerable Get() + public IEnumerable Get() { return _Scopes.Values; } - /// - /// Switch to a specific language scope by name. - /// - /// The name of the language scope to switch to. - internal void UseScope(string name) - { - if (!_Scopes.TryGetValue(GetScopeName(name), out var scope)) - throw new Exception($"The specified scope '{name}' was not found."); - - _Current = scope; - } - - internal bool TryScope(string name, out ILanguageScope scope) + public bool TryScope(string name, out ILanguageScope scope) { return _Scopes.TryGetValue(GetScopeName(name), out scope); } - internal bool Import(string name, out ILanguageScope scope) + public bool Import(string name) { - if (_Scopes.TryGetValue(GetScopeName(name), out scope)) + if (_Scopes.ContainsKey(GetScopeName(name))) return false; - scope = new LanguageScope(name); + var scope = new LanguageScope(name); Add(scope); return true; } diff --git a/src/PSRule/Runtime/RunspaceContext.cs b/src/PSRule/Runtime/RunspaceContext.cs index 9a8b68008d..81a8e75207 100644 --- a/src/PSRule/Runtime/RunspaceContext.cs +++ b/src/PSRule/Runtime/RunspaceContext.cs @@ -65,10 +65,7 @@ internal sealed class RunspaceContext : IDisposable, ILogger, IScriptResourceDis private readonly List _Reason; private readonly List _Conventions; - /// - /// A collection of languages scopes for this pipeline. - /// - private readonly LanguageScopeSet _LanguageScopes; + private ILanguageScope? _CurrentLanguageScope; // Track whether Dispose has been called. private bool _Disposed; @@ -91,7 +88,6 @@ internal RunspaceContext(PipelineContext pipeline) _RuleTimer = new Stopwatch(); _Reason = []; _Conventions = []; - _LanguageScopes = new LanguageScopeSet(); _Scope = new Stack(); } @@ -105,12 +101,12 @@ internal RunspaceContext(PipelineContext pipeline) internal SourceScope? Source { get; private set; } - internal ILanguageScope LanguageScope + internal ILanguageScope? LanguageScope { [DebuggerStepThrough] get { - return _LanguageScopes.Current; + return _CurrentLanguageScope; } } @@ -524,7 +520,10 @@ public void EnterLanguageScope(ISourceFile file) if (!file.Exists()) throw new FileNotFoundException(PSRuleResources.ScriptNotFound, file.Path); - _LanguageScopes.UseScope(file.Module); + if (!Pipeline.LanguageScopes.TryScope(file.Module, out var scope)) + throw new Exception("Language scope is unknown."); + + _CurrentLanguageScope = scope; if (TargetObject != null && LanguageScope != null) Binding = LanguageScope.Bind(TargetObject); @@ -535,6 +534,7 @@ public void EnterLanguageScope(ISourceFile file) public void ExitLanguageScope(ISourceFile file) { // Look at scope popping and validation. + _CurrentLanguageScope = null; Source = null; } @@ -645,6 +645,8 @@ internal void Import(IConvention resource) internal void AddService(string id, object service) { + if (LanguageScope == null) throw new Exception("Can not call out of scope."); + ResourceHelper.ParseIdString(LanguageScope.Name, id, out var scopeName, out var name); if (!StringComparer.OrdinalIgnoreCase.Equals(LanguageScope.Name, scopeName) || string.IsNullOrEmpty(name)) return; @@ -654,8 +656,10 @@ internal void AddService(string id, object service) internal object? GetService(string id) { + if (LanguageScope == null) throw new Exception("Can not call out of scope."); + ResourceHelper.ParseIdString(LanguageScope.Name, id, out var scopeName, out var name); - return !_LanguageScopes.TryScope(scopeName, out var scope) || string.IsNullOrEmpty(name) ? null : scope.GetService(name!); + return !Pipeline.LanguageScopes.TryScope(scopeName, out var scope) || string.IsNullOrEmpty(name) ? null : scope.GetService(name!); } private void RunConventionInitialize() @@ -722,7 +726,7 @@ public void Init(Source[] source) foreach (var resource in resources.Where(r => r.Kind == ResourceKind.ModuleConfig).ToArray()) Pipeline.Import(this, resource); - foreach (var languageScope in _LanguageScopes.Get()) + foreach (var languageScope in Pipeline.LanguageScopes.Get()) Pipeline.UpdateLanguageScope(languageScope); foreach (var resource in resources) @@ -745,14 +749,14 @@ public void Init(Source[] source) foreach (var resource in resources.Where(r => r.Kind != ResourceKind.ModuleConfig).ToArray()) Pipeline.Import(this, resource); - foreach (var languageScope in _LanguageScopes.Get()) + foreach (var languageScope in Pipeline.LanguageScopes.Get()) Pipeline.UpdateLanguageScope(languageScope); } private void InitLanguageScopes(Source[] source) { for (var i = 0; source != null && i < source.Length; i++) - _LanguageScopes.Import(source[i].Scope, out _); + Pipeline.LanguageScopes.Import(source[i].Scope); } public void Begin() @@ -788,7 +792,7 @@ public bool TryGetScope(object o, out string[]? scope) if (string.IsNullOrEmpty(Source?.File.HelpPath)) return null; - var cultures = LanguageScope.Culture; + var cultures = LanguageScope?.Culture; if (!_RaisedUsingInvariantCulture && (cultures == null || cultures.Length == 0)) { this.Throw(_InvariantCulture, PSRuleResources.UsingInvariantCulture); @@ -907,7 +911,6 @@ private void Dispose(bool disposing) if (_Conventions[i] is IDisposable d) d.Dispose(); } - _LanguageScopes.Dispose(); } _Disposed = true; } diff --git a/tests/PSRule.Tests/FunctionTests.cs b/tests/PSRule.Tests/FunctionTests.cs index 23c607dba1..9a2cdb8fa7 100644 --- a/tests/PSRule.Tests/FunctionTests.cs +++ b/tests/PSRule.Tests/FunctionTests.cs @@ -625,6 +625,7 @@ private static ExpressionContext GetContext() context.Init(s); context.Begin(); context.PushScope(Runtime.RunspaceScope.Precondition); + context.EnterLanguageScope(s[0].File[0]); context.EnterTargetObject(new TargetObject(targetObject)); return result; } diff --git a/tests/PSRule.Tests/PSRuleOptionTests.cs b/tests/PSRule.Tests/PSRuleOptionTests.cs index 38a31c9434..cf6e5888a7 100644 --- a/tests/PSRule.Tests/PSRuleOptionTests.cs +++ b/tests/PSRule.Tests/PSRuleOptionTests.cs @@ -95,6 +95,7 @@ private Runtime.Configuration GetConfigurationHelper(PSRuleOption option) var context = new Runtime.RunspaceContext(PipelineContext.New(option, null, null, new TestWriter(GetOption()), builder, null)); context.Init(null); context.Begin(); + context.EnterLanguageScope(GetSource()[0].File[0]); return new Runtime.Configuration(context); } diff --git a/tests/PSRule.Tests/SelectorTests.cs b/tests/PSRule.Tests/SelectorTests.cs index 6bec2edda3..9941a4c909 100644 --- a/tests/PSRule.Tests/SelectorTests.cs +++ b/tests/PSRule.Tests/SelectorTests.cs @@ -1873,6 +1873,7 @@ private SelectorVisitor GetSelectorVisitor(string name, Source[] source, out Run context.Init(source); context.Begin(); var selector = HostHelper.GetSelectorForTests(source, context).ToArray().FirstOrDefault(s => s.Name == name); + context.EnterLanguageScope(selector.Source); return new SelectorVisitor(context, selector.Id, selector.Source, selector.Spec.If); }