From 89e2eb22f6ab17ebd51cc46d195f953c551b9e67 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Thu, 27 May 2021 14:26:35 +1000 Subject: [PATCH] Add source location of pipeline objects #729 (#730) --- docs/CHANGELOG-v1.md | 5 ++-- src/PSRule/Common/PSObjectExtensions.cs | 30 +++++++++++++++++++ src/PSRule/Data/TargetSourceInfo.cs | 34 ++++++++++++++++++++-- src/PSRule/Pipeline/PipelineReader.cs | 4 +++ src/PSRule/Runtime/PSRuleMemberInfo.cs | 3 ++ tests/PSRule.Tests/PSRule.Common.Tests.ps1 | 13 +++++++++ tests/PSRule.Tests/PipelineTests.cs | 1 + tests/PSRule.Tests/SourceTests.cs | 31 ++++++++++++++++++++ 8 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 tests/PSRule.Tests/SourceTests.cs diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md index e10b77b69d..fdd7d02068 100644 --- a/docs/CHANGELOG-v1.md +++ b/docs/CHANGELOG-v1.md @@ -22,8 +22,9 @@ What's changed since pre-release v1.4.0-B2105019: What's changed since pre-release v1.4.0-B2105004: - General improvements: - - Source location of object from input files are included in rule records. [#624](https://github.com/microsoft/PSRule/issues/624) - - Currently only JSON and YAML files support source locations. + - Source location of objects are included in results. + - Source location of objects from JSON and YAML input files are read automatically. [#624](https://github.com/microsoft/PSRule/issues/624) + - Source location of objects from the pipeline are read from properties. [#729](https://github.com/microsoft/PSRule/issues/729) - Improved support for version constraints by: - Constraints can include prerelease versions of other matching versions. [#714](https://github.com/microsoft/PSRule/issues/714) - Constraints support using a `@prerelease` or `@pre` to include prerelease versions. [#717](https://github.com/microsoft/PSRule/issues/717) diff --git a/src/PSRule/Common/PSObjectExtensions.cs b/src/PSRule/Common/PSObjectExtensions.cs index dac7e028a1..0b87d2685d 100644 --- a/src/PSRule/Common/PSObjectExtensions.cs +++ b/src/PSRule/Common/PSObjectExtensions.cs @@ -14,6 +14,8 @@ namespace PSRule { internal static class PSObjectExtensions { + private const string PROPERTY_SOURCE = "source"; + public static T PropertyValue(this PSObject o, string propertyName) { if (o.BaseObject is Hashtable hashtable) @@ -53,6 +55,18 @@ public static bool HasNoteProperty(this PSObject o) return false; } + public static bool TryProperty(this PSObject o, string name, out T value) + { + value = default; + var pValue = ConvertValue(o.Properties[name]?.Value); + if (pValue is T tValue) + { + value = tValue; + return true; + } + return false; + } + public static string ToJson(this PSObject o) { var settings = new JsonSerializerSettings { Formatting = Formatting.None, TypeNameHandling = TypeNameHandling.None, MaxDepth = 1024, Culture = CultureInfo.InvariantCulture }; @@ -93,6 +107,22 @@ public static TargetSourceInfo[] GetSourceInfo(this PSObject o) return targetInfo.Source.ToArray(); } + public static void ConvertTargetInfoProperty(this PSObject o) + { + if (o == null || !TryProperty(o, PSRuleTargetInfo.PropertyName, out PSObject value)) + return; + + UseTargetInfo(o, out PSRuleTargetInfo targetInfo); + if (TryProperty(value, PROPERTY_SOURCE, out Array sources)) + { + for (var i = 0; i < sources.Length; i++) + { + var source = TargetSourceInfo.Create(sources.GetValue(i)); + targetInfo.WithSource(source); + } + } + } + private static T ConvertValue(object value) { if (value == null) diff --git a/src/PSRule/Data/TargetSourceInfo.cs b/src/PSRule/Data/TargetSourceInfo.cs index 3a4ea4d38f..a2120cb4f5 100644 --- a/src/PSRule/Data/TargetSourceInfo.cs +++ b/src/PSRule/Data/TargetSourceInfo.cs @@ -4,11 +4,16 @@ using Newtonsoft.Json; using System; using System.IO; +using System.Management.Automation; namespace PSRule.Data { public sealed class TargetSourceInfo { + private const string PROPERTY_FILE = "file"; + private const string PROPERTY_LINE = "line"; + private const string PROPERTY_POSITION = "position"; + public TargetSourceInfo() { // Do nothing @@ -29,13 +34,13 @@ internal TargetSourceInfo(Uri uri) File = uri.AbsoluteUri; } - [JsonProperty(PropertyName = "file")] + [JsonProperty(PropertyName = PROPERTY_FILE)] public string File { get; internal set; } - [JsonProperty(PropertyName = "line")] + [JsonProperty(PropertyName = PROPERTY_LINE)] public int? Line { get; internal set; } - [JsonProperty(PropertyName = "position")] + [JsonProperty(PropertyName = PROPERTY_POSITION)] public int? Position { get; internal set; } public bool Equals(TargetSourceInfo other) @@ -57,5 +62,28 @@ public override int GetHashCode() return hash; } } + + public static TargetSourceInfo Create(object o) + { + if (o is PSObject pso) + return Create(pso); + + return null; + } + + public static TargetSourceInfo Create(PSObject o) + { + var result = new TargetSourceInfo(); + if (o.TryProperty(PROPERTY_FILE, out string file)) + result.File = file; + + if (o.TryProperty(PROPERTY_LINE, out int line)) + result.Line = line; + + if (o.TryProperty(PROPERTY_POSITION, out int position)) + result.Position = position; + + return result; + } } } diff --git a/src/PSRule/Pipeline/PipelineReader.cs b/src/PSRule/Pipeline/PipelineReader.cs index 04a882de26..c03501a11e 100644 --- a/src/PSRule/Pipeline/PipelineReader.cs +++ b/src/PSRule/Pipeline/PipelineReader.cs @@ -32,6 +32,7 @@ public void Enqueue(PSObject sourceObject, bool skipExpansion = false) if (_Input == null || skipExpansion) { + sourceObject.ConvertTargetInfoProperty(); _Queue.Enqueue(sourceObject); return; } @@ -42,7 +43,10 @@ public void Enqueue(PSObject sourceObject, bool skipExpansion = false) return; foreach (var item in input) + { + sourceObject.ConvertTargetInfoProperty(); _Queue.Enqueue(item); + } } public bool TryDequeue(out PSObject sourceObject) diff --git a/src/PSRule/Runtime/PSRuleMemberInfo.cs b/src/PSRule/Runtime/PSRuleMemberInfo.cs index d175719c05..910b751e8f 100644 --- a/src/PSRule/Runtime/PSRuleMemberInfo.cs +++ b/src/PSRule/Runtime/PSRuleMemberInfo.cs @@ -70,6 +70,9 @@ internal void Combine(PSRuleTargetInfo targetInfo) internal void WithSource(TargetSourceInfo source) { + if (source == null) + return; + if (Source.Count == 0) { Source.Add(source); diff --git a/tests/PSRule.Tests/PSRule.Common.Tests.ps1 b/tests/PSRule.Tests/PSRule.Common.Tests.ps1 index 6bf8c59f47..5ea1215208 100644 --- a/tests/PSRule.Tests/PSRule.Common.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Common.Tests.ps1 @@ -36,6 +36,15 @@ Describe 'Invoke-PSRule' -Tag 'Invoke-PSRule','Common' { $testObject = [PSCustomObject]@{ Name = 'TestObject1' Value = 1 + '_PSRule' = [PSCustomObject]@{ + source = @( + [PSCustomObject]@{ + file = 'source.json' + Line = 100 + Position = 1000 + } + ) + } } $testObject.PSObject.TypeNames.Insert(0, 'TestType'); @@ -48,6 +57,10 @@ Describe 'Invoke-PSRule' -Tag 'Invoke-PSRule','Common' { $result.TargetName | Should -Be 'TestObject1'; $result.Info.Annotations.culture | Should -Be 'en-ZZ'; $result.Recommendation | Should -Be 'This is a recommendation.'; + $result.Source | Should -Not -BeNullOrEmpty; + $result.Source[0].File | Should -Be 'source.json'; + $result.Source[0].Line | Should -Be 100; + $result.Source[0].Position | Should -Be 1000; Assert-VerifiableMock; } finally { diff --git a/tests/PSRule.Tests/PipelineTests.cs b/tests/PSRule.Tests/PipelineTests.cs index 5b5c5371a6..117aba257c 100644 --- a/tests/PSRule.Tests/PipelineTests.cs +++ b/tests/PSRule.Tests/PipelineTests.cs @@ -95,6 +95,7 @@ public void PipelineWithSource() var builder = PipelineBuilder.Invoke(GetSource(), option, null, null); builder.InputPath(new string[] { "./**/ObjectFromFile.json" }); var pipeline = builder.Build(); + Assert.NotNull(pipeline); pipeline.Begin(); pipeline.End(); } diff --git a/tests/PSRule.Tests/SourceTests.cs b/tests/PSRule.Tests/SourceTests.cs new file mode 100644 index 0000000000..982fbd0ca8 --- /dev/null +++ b/tests/PSRule.Tests/SourceTests.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; +using Xunit; + +namespace PSRule +{ + public sealed class SourceTests + { + [Fact] + public void TargetSourceInfo() + { + var source = new PSObject(); + source.Properties.Add(new PSNoteProperty("file", "file.json")); + source.Properties.Add(new PSNoteProperty("line", 100)); + source.Properties.Add(new PSNoteProperty("position", 1000)); + var info = new PSObject(); + info.Properties.Add(new PSNoteProperty("source", new PSObject[] { source })); + var o = new PSObject(); + o.Properties.Add(new PSNoteProperty("_PSRule", info)); + o.ConvertTargetInfoProperty(); + + var actual = o.GetSourceInfo(); + Assert.NotNull(actual); + Assert.Equal("file.json", actual[0].File); + Assert.Equal(100, actual[0].Line); + Assert.Equal(1000, actual[0].Position); + } + } +}