This commit is contained in:
Bernie White 2024-09-04 09:18:53 +10:00 коммит произвёл GitHub
Родитель 1fd2d7c334
Коммит e05ff57c29
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
12 изменённых файлов: 558 добавлений и 553 удалений

Просмотреть файл

@ -7,8 +7,11 @@ using System.ComponentModel;
namespace PSRule.Configuration;
/// <summary>
/// Options that affect property binding of TargetName and TargetType.
/// Options that configure property binding.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/ps-rule/options"/>.
/// </remarks>
public sealed class BindingOption : IEquatable<BindingOption>, IBindingOption
{
private const bool DEFAULT_IGNORECASE = true;

Просмотреть файл

@ -5,6 +5,67 @@ using PSRule.Options;
namespace PSRule.Configuration;
/// <summary>
/// Options that configure property binding.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/ps-rule/options"/>.
/// </remarks>
public interface IBindingOption : IOption
{
/// <summary>
/// One or more custom fields to bind.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/ps-rule/options#bindingfield"/>.
/// </remarks>
FieldMap Field { get; }
/// <summary>
/// Determines if custom binding uses ignores case when matching properties.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/ps-rule/options#bindingignorecase"/>.
/// </remarks>
bool? IgnoreCase { get; }
/// <summary>
/// Configures the separator to use for building a qualified name.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/ps-rule/options#bindingnameseparator"/>.
/// </remarks>
string NameSeparator { get; }
/// <summary>
/// Determines if binding prefers target info provided by the object over custom configuration.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/ps-rule/options#bindingprefertargetinfo"/>.
/// </remarks>
bool? PreferTargetInfo { get; }
/// <summary>
/// Property names to use to bind TargetName.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/ps-rule/options#bindingtargetname"/>.
/// </remarks>
string[] TargetName { get; }
/// <summary>
/// Property names to use to bind TargetType.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/ps-rule/options#bindingtargettype"/>.
/// </remarks>
string[] TargetType { get; }
/// <summary>
/// Determines if a qualified TargetName is used.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/ps-rule/options#bindingusequalifiedname"/>.
/// </remarks>
bool? UseQualifiedName { get; }
}

Просмотреть файл

@ -63,42 +63,6 @@ internal sealed class GetTargetPipelineBuilder : PipelineBuilderBase, IGetTarget
/// <inheritdoc/>
protected override PipelineInputStream PrepareReader()
{
if (!string.IsNullOrEmpty(Option.Input.ObjectPath))
{
AddVisitTargetObjectAction((sourceObject, next) =>
{
return PipelineReceiverActions.ReadObjectPath(sourceObject, next, Option.Input.ObjectPath, true);
});
}
if (Option.Input.Format == InputFormat.Yaml)
{
AddVisitTargetObjectAction((sourceObject, next) =>
{
return PipelineReceiverActions.ConvertFromYaml(sourceObject, next);
});
}
else if (Option.Input.Format == InputFormat.Json)
{
AddVisitTargetObjectAction((sourceObject, next) =>
{
return PipelineReceiverActions.ConvertFromJson(sourceObject, next);
});
}
else if (Option.Input.Format == InputFormat.Markdown)
{
AddVisitTargetObjectAction((sourceObject, next) =>
{
return PipelineReceiverActions.ConvertFromMarkdown(sourceObject, next);
});
}
else if (Option.Input.Format == InputFormat.PowerShellData)
{
AddVisitTargetObjectAction((sourceObject, next) =>
{
return PipelineReceiverActions.ConvertFromPowerShellData(sourceObject, next);
});
}
return new PipelineInputStream(VisitTargetObject, _InputPath, GetInputObjectSourceFilter(), Option);
return new PipelineInputStream(_InputPath, GetInputObjectSourceFilter(), Option);
}
}

Просмотреть файл

@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using PSRule.Configuration;
namespace PSRule.Pipeline;
/// <summary>
/// A helper to build a PSRule pipeline.
/// </summary>
public interface IPipelineBuilder
{
/// <summary>
/// Configure the pipeline with options.
/// </summary>
IPipelineBuilder Configure(PSRuleOption option);
/// <summary>
/// Configure the pipeline to use a specific baseline.
/// </summary>
/// <param name="baseline">A baseline option or the name of a baseline.</param>
void Baseline(BaselineOption baseline);
/// <summary>
/// Build the pipeline.
/// </summary>
/// <param name="writer">Optionally specify a custom writer which will handle output processing.</param>
IPipeline Build(IPipelineWriter writer = null);
}

Просмотреть файл

@ -5,6 +5,8 @@ using PSRule.Data;
namespace PSRule.Pipeline;
#nullable enable
internal interface IPipelineReader
{
int Count { get; }

Просмотреть файл

@ -131,42 +131,6 @@ internal abstract class InvokePipelineBuilderBase : PipelineBuilderBase, IInvoke
protected override PipelineInputStream PrepareReader()
{
if (!string.IsNullOrEmpty(Option.Input.ObjectPath))
{
AddVisitTargetObjectAction((sourceObject, next) =>
{
return PipelineReceiverActions.ReadObjectPath(sourceObject, next, Option.Input.ObjectPath, true);
});
}
if (Option.Input.Format == InputFormat.Yaml)
{
AddVisitTargetObjectAction((sourceObject, next) =>
{
return PipelineReceiverActions.ConvertFromYaml(sourceObject, next);
});
}
else if (Option.Input.Format == InputFormat.Json)
{
AddVisitTargetObjectAction((sourceObject, next) =>
{
return PipelineReceiverActions.ConvertFromJson(sourceObject, next);
});
}
else if (Option.Input.Format == InputFormat.Markdown)
{
AddVisitTargetObjectAction((sourceObject, next) =>
{
return PipelineReceiverActions.ConvertFromMarkdown(sourceObject, next);
});
}
else if (Option.Input.Format == InputFormat.PowerShellData)
{
AddVisitTargetObjectAction((sourceObject, next) =>
{
return PipelineReceiverActions.ConvertFromPowerShellData(sourceObject, next);
});
}
return new PipelineInputStream(VisitTargetObject, _InputPath, GetInputObjectSourceFilter(), Option);
return new PipelineInputStream(_InputPath, GetInputObjectSourceFilter(), Option);
}
}

Просмотреть файл

@ -1,15 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections;
using System.Globalization;
using PSRule.Configuration;
using PSRule.Data;
using PSRule.Definitions;
using PSRule.Definitions.Baselines;
using PSRule.Options;
using PSRule.Pipeline.Output;
using PSRule.Resources;
namespace PSRule.Pipeline;
@ -164,453 +156,3 @@ public static class PipelineBuilder
return pipeline;
}
}
/// <summary>
/// A helper to build a PSRule pipeline.
/// </summary>
public interface IPipelineBuilder
{
/// <summary>
/// Configure the pipeline with options.
/// </summary>
IPipelineBuilder Configure(PSRuleOption option);
/// <summary>
/// Configure the pipeline to use a specific baseline.
/// </summary>
/// <param name="baseline">A baseline option or the name of a baseline.</param>
void Baseline(Configuration.BaselineOption baseline);
/// <summary>
/// Build the pipeline.
/// </summary>
/// <param name="writer">Optionally specify a custom writer which will handle output processing.</param>
IPipeline Build(IPipelineWriter writer = null);
}
internal abstract class PipelineBuilderBase : IPipelineBuilder
{
private const string ENGINE_MODULE_NAME = "PSRule";
protected readonly PSRuleOption Option;
protected readonly Source[] Source;
protected readonly IHostContext HostContext;
protected BindTargetMethod BindTargetNameHook;
protected BindTargetMethod BindTargetTypeHook;
protected BindTargetMethod BindFieldHook;
protected VisitTargetObject VisitTargetObject;
private string[] _Include;
private Hashtable _Tag;
private Configuration.BaselineOption _Baseline;
private string[] _Convention;
private PathFilter _InputFilter;
private PipelineWriter _Writer;
private readonly HostPipelineWriter _Output;
private const int MIN_JSON_INDENT = 0;
private const int MAX_JSON_INDENT = 4;
protected PipelineBuilderBase(Source[] source, IHostContext hostContext)
{
Option = new PSRuleOption();
Source = source;
_Output = new HostPipelineWriter(hostContext, Option, ShouldProcess);
HostContext = hostContext;
BindTargetNameHook = PipelineHookActions.BindTargetName;
BindTargetTypeHook = PipelineHookActions.BindTargetType;
BindFieldHook = PipelineHookActions.BindField;
VisitTargetObject = PipelineReceiverActions.PassThru;
}
/// <summary>
/// Determines if the pipeline is executing in a remote PowerShell session.
/// </summary>
public bool InSession => HostContext != null && HostContext.InSession;
/// <inheritdoc/>
public void Name(string[] name)
{
if (name == null || name.Length == 0)
return;
_Include = name;
}
/// <inheritdoc/>
public void Tag(Hashtable tag)
{
if (tag == null || tag.Count == 0)
return;
_Tag = tag;
}
/// <inheritdoc/>
public void Convention(string[] convention)
{
if (convention == null || convention.Length == 0)
return;
_Convention = convention;
}
/// <inheritdoc/>
public virtual IPipelineBuilder Configure(PSRuleOption option)
{
if (option == null)
return this;
Option.Baseline = new Options.BaselineOption(option.Baseline);
Option.Binding = new BindingOption(option.Binding);
Option.Convention = new ConventionOption(option.Convention);
Option.Execution = GetExecutionOption(option.Execution);
Option.Input = new InputOption(option.Input);
Option.Input.Format ??= InputOption.Default.Format;
Option.Output = new OutputOption(option.Output);
Option.Output.Outcome ??= OutputOption.Default.Outcome;
Option.Output.Banner ??= OutputOption.Default.Banner;
Option.Output.Style = GetStyle(option.Output.Style ?? OutputOption.Default.Style.Value);
Option.Repository = GetRepository(option.Repository);
return this;
}
/// <inheritdoc/>
public abstract IPipeline Build(IPipelineWriter writer = null);
/// <inheritdoc/>
public void Baseline(Configuration.BaselineOption baseline)
{
if (baseline == null)
return;
_Baseline = baseline;
}
/// <summary>
/// Require correct module versions for pipeline execution.
/// </summary>
protected bool RequireModules()
{
var result = true;
if (Option.Requires.TryGetValue(ENGINE_MODULE_NAME, out var requiredVersion))
{
var engineVersion = Engine.GetVersion();
if (GuardModuleVersion(ENGINE_MODULE_NAME, engineVersion, requiredVersion))
result = false;
}
for (var i = 0; Source != null && i < Source.Length; i++)
{
if (Source[i].Module != null && Option.Requires.TryGetValue(Source[i].Module.Name, out requiredVersion))
{
if (GuardModuleVersion(Source[i].Module.Name, Source[i].Module.Version, requiredVersion))
result = false;
}
}
return result;
}
/// <summary>
/// Require sources for pipeline execution.
/// </summary>
protected bool RequireSources()
{
if (Source == null || Source.Length == 0)
{
PrepareWriter().WarnRulePathNotFound();
return false;
}
return true;
}
private bool GuardModuleVersion(string moduleName, string moduleVersion, string requiredVersion)
{
if (!TryModuleVersion(moduleVersion, requiredVersion))
{
var writer = PrepareWriter();
writer.ErrorRequiredVersionMismatch(moduleName, moduleVersion, requiredVersion);
writer.End(new DefaultPipelineResult(null, BreakLevel.None) { HadErrors = true });
return true;
}
return false;
}
private static bool TryModuleVersion(string moduleVersion, string requiredVersion)
{
return SemanticVersion.TryParseVersion(moduleVersion, out var version) &&
SemanticVersion.TryParseConstraint(requiredVersion, out var constraint) &&
constraint.Equals(version);
}
protected PipelineContext PrepareContext(BindTargetMethod bindTargetName, BindTargetMethod bindTargetType, BindTargetMethod bindField)
{
var unresolved = new List<ResourceRef>();
if (_Baseline is Configuration.BaselineOption.BaselineRef baselineRef)
unresolved.Add(new BaselineRef(ResolveBaselineGroup(baselineRef.Name), ScopeType.Explicit));
return PipelineContext.New(
option: Option,
hostContext: HostContext,
reader: PrepareReader(),
bindTargetName: bindTargetName,
bindTargetType: bindTargetType,
bindField: bindField,
optionBuilder: GetOptionBuilder(),
unresolved: unresolved
);
}
protected string[] ResolveBaselineGroup(string[] name)
{
for (var i = 0; name != null && i < name.Length; i++)
name[i] = ResolveBaselineGroup(name[i]);
return name;
}
protected string ResolveBaselineGroup(string name)
{
if (name == null || name.Length < 2 || !name.StartsWith("@") ||
Option == null || Option.Baseline == null || Option.Baseline.Group == null ||
Option.Baseline.Group.Count == 0)
return name;
var key = name.Substring(1);
if (!Option.Baseline.Group.TryGetValue(key, out var baselines) || baselines.Length == 0)
throw new PipelineConfigurationException("Baseline.Group", string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.PSR0003, key));
var writer = PrepareWriter();
writer.WriteVerbose($"Using baseline group '{key}': {baselines[0]}");
return baselines[0];
}
protected virtual PipelineInputStream PrepareReader()
{
return new PipelineInputStream(null, null, GetInputObjectSourceFilter(), Option);
}
protected virtual PipelineWriter PrepareWriter()
{
if (_Writer != null)
return _Writer;
var output = GetOutput();
_Writer = Option.Output.Format switch
{
OutputFormat.Csv => new CsvOutputWriter(output, Option, ShouldProcess),
OutputFormat.Json => new JsonOutputWriter(output, Option, ShouldProcess),
OutputFormat.NUnit3 => new NUnit3OutputWriter(output, Option, ShouldProcess),
OutputFormat.Yaml => new YamlOutputWriter(output, Option, ShouldProcess),
OutputFormat.Markdown => new MarkdownOutputWriter(output, Option, ShouldProcess),
OutputFormat.Wide => new WideOutputWriter(output, Option, ShouldProcess),
OutputFormat.Sarif => new SarifOutputWriter(Source, output, Option, ShouldProcess),
_ => output,
};
return _Writer;
}
protected virtual PipelineWriter GetOutput(bool writeHost = false)
{
// Redirect to file instead
return !string.IsNullOrEmpty(Option.Output.Path)
? new FileOutputWriter(
inner: _Output,
option: Option,
encoding: Option.Output.GetEncoding(),
path: Option.Output.Path,
shouldProcess: HostContext.ShouldProcess,
writeHost: writeHost
)
: _Output;
}
protected static string[] GetCulture(string[] culture)
{
var result = new List<string>();
var parent = new List<string>();
var set = new HashSet<string>();
for (var i = 0; culture != null && i < culture.Length; i++)
{
var c = CultureInfo.CreateSpecificCulture(culture[i]);
if (!set.Contains(c.Name))
{
result.Add(c.Name);
set.Add(c.Name);
}
for (var p = c.Parent; !string.IsNullOrEmpty(p.Name); p = p.Parent)
{
if (!set.Contains(p.Name))
{
parent.Add(p.Name);
set.Add(p.Name);
}
}
}
if (parent.Count > 0)
result.AddRange(parent);
return result.Count == 0 ? null : result.ToArray();
}
protected static RepositoryOption GetRepository(RepositoryOption option)
{
var result = new RepositoryOption(option);
if (string.IsNullOrEmpty(result.Url) && GitHelper.TryRepository(out var url))
result.Url = url;
if (string.IsNullOrEmpty(result.BaseRef) && GitHelper.TryBaseRef(out var baseRef))
result.BaseRef = baseRef;
return result;
}
/// <summary>
/// Coalesce execution options with defaults.
/// </summary>
protected static ExecutionOption GetExecutionOption(ExecutionOption option)
{
var result = ExecutionOption.Combine(option, ExecutionOption.Default);
// Handle when preference is set to none. The default should be used.
result.AliasReference = result.AliasReference == ExecutionActionPreference.None ? ExecutionOption.Default.AliasReference.Value : result.AliasReference;
result.DuplicateResourceId = result.DuplicateResourceId == ExecutionActionPreference.None ? ExecutionOption.Default.DuplicateResourceId.Value : result.DuplicateResourceId;
result.InvariantCulture = result.InvariantCulture == ExecutionActionPreference.None ? ExecutionOption.Default.InvariantCulture.Value : result.InvariantCulture;
result.RuleExcluded = result.RuleExcluded == ExecutionActionPreference.None ? ExecutionOption.Default.RuleExcluded.Value : result.RuleExcluded;
result.RuleInconclusive = result.RuleInconclusive == ExecutionActionPreference.None ? ExecutionOption.Default.RuleInconclusive.Value : result.RuleInconclusive;
result.RuleSuppressed = result.RuleSuppressed == ExecutionActionPreference.None ? ExecutionOption.Default.RuleSuppressed.Value : result.RuleSuppressed;
result.SuppressionGroupExpired = result.SuppressionGroupExpired == ExecutionActionPreference.None ? ExecutionOption.Default.SuppressionGroupExpired.Value : result.SuppressionGroupExpired;
result.UnprocessedObject = result.UnprocessedObject == ExecutionActionPreference.None ? ExecutionOption.Default.UnprocessedObject.Value : result.UnprocessedObject;
return result;
}
protected PathFilter GetInputObjectSourceFilter()
{
return Option.Input.IgnoreObjectSource.GetValueOrDefault(InputOption.Default.IgnoreObjectSource.Value) ? GetInputFilter() : null;
}
protected PathFilter GetInputFilter()
{
if (_InputFilter == null)
{
var basePath = Environment.GetWorkingPath();
var ignoreGitPath = Option.Input.IgnoreGitPath ?? InputOption.Default.IgnoreGitPath.Value;
var ignoreRepositoryCommon = Option.Input.IgnoreRepositoryCommon ?? InputOption.Default.IgnoreRepositoryCommon.Value;
var builder = PathFilterBuilder.Create(basePath, Option.Input.PathIgnore, ignoreGitPath, ignoreRepositoryCommon);
builder.UseGitIgnore();
_InputFilter = builder.Build();
}
return _InputFilter;
}
private OptionContextBuilder GetOptionBuilder()
{
return new OptionContextBuilder(Option, _Include, _Tag, _Convention);
}
protected void ConfigureBinding(PSRuleOption option)
{
if (option.Pipeline.BindTargetName != null && option.Pipeline.BindTargetName.Count > 0)
{
// Do not allow custom binding functions to be used with constrained language mode
if (Option.Execution.LanguageMode == LanguageMode.ConstrainedLanguage)
throw new PipelineConfigurationException(optionName: "BindTargetName", message: PSRuleResources.ConstrainedTargetBinding);
foreach (var action in option.Pipeline.BindTargetName)
BindTargetNameHook = AddBindTargetAction(action, BindTargetNameHook);
}
if (option.Pipeline.BindTargetType != null && option.Pipeline.BindTargetType.Count > 0)
{
// Do not allow custom binding functions to be used with constrained language mode
if (Option.Execution.LanguageMode == LanguageMode.ConstrainedLanguage)
throw new PipelineConfigurationException(optionName: "BindTargetType", message: PSRuleResources.ConstrainedTargetBinding);
foreach (var action in option.Pipeline.BindTargetType)
BindTargetTypeHook = AddBindTargetAction(action, BindTargetTypeHook);
}
}
private static BindTargetMethod AddBindTargetAction(BindTargetFunc action, BindTargetMethod previous)
{
// Nest the previous write action in the new supplied action
// Execution chain will be: action -> previous -> previous..n
return (string[] propertyNames, bool caseSensitive, bool preferTargetInfo, object targetObject, out string path) =>
{
return action(propertyNames, caseSensitive, preferTargetInfo, targetObject, previous, out path);
};
}
private static BindTargetMethod AddBindTargetAction(BindTargetName action, BindTargetMethod previous)
{
return AddBindTargetAction((string[] propertyNames, bool caseSensitive, bool preferTargetInfo, object targetObject, BindTargetMethod next, out string path) =>
{
path = null;
var targetType = action(targetObject);
return string.IsNullOrEmpty(targetType) ? next(propertyNames, caseSensitive, preferTargetInfo, targetObject, out path) : targetType;
}, previous);
}
protected void AddVisitTargetObjectAction(VisitTargetObjectAction action)
{
// Nest the previous write action in the new supplied action
// Execution chain will be: action -> previous -> previous..n
var previous = VisitTargetObject;
VisitTargetObject = (targetObject) => action(targetObject, previous);
}
/// <summary>
/// Normalizes JSON indent range between minimum 0 and maximum 4.
/// </summary>
/// <param name="jsonIndent"></param>
/// <returns>The number of characters to indent.</returns>
protected static int NormalizeJsonIndentRange(int? jsonIndent)
{
if (jsonIndent.HasValue)
{
if (jsonIndent < MIN_JSON_INDENT)
return MIN_JSON_INDENT;
else if (jsonIndent > MAX_JSON_INDENT)
return MAX_JSON_INDENT;
return jsonIndent.Value;
}
return MIN_JSON_INDENT;
}
protected bool TryChangedFiles(out string[] files)
{
files = null;
if (!Option.Input.IgnoreUnchangedPath.GetValueOrDefault(InputOption.Default.IgnoreUnchangedPath.Value) ||
!GitHelper.TryGetChangedFiles(Option.Repository.BaseRef, "d", null, out files))
return false;
for (var i = 0; i < files.Length; i++)
HostContext.Verbose(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.UsingChangedFile, files[i]));
return true;
}
protected bool ShouldProcess(string target, string action)
{
return HostContext == null || HostContext.ShouldProcess(target, action);
}
protected static OutputStyle GetStyle(OutputStyle style)
{
if (style != OutputStyle.Detect)
return style;
if (Environment.IsAzurePipelines())
return OutputStyle.AzurePipelines;
if (Environment.IsGitHubActions())
return OutputStyle.GitHubActions;
return Environment.IsVisualStudioCode() ?
OutputStyle.VisualStudioCode :
OutputStyle.Client;
}
}

Просмотреть файл

@ -0,0 +1,440 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections;
using System.Globalization;
using PSRule.Configuration;
using PSRule.Data;
using PSRule.Definitions;
using PSRule.Definitions.Baselines;
using PSRule.Options;
using PSRule.Pipeline.Output;
using PSRule.Resources;
namespace PSRule.Pipeline;
internal abstract class PipelineBuilderBase : IPipelineBuilder
{
private const string ENGINE_MODULE_NAME = "PSRule";
protected readonly PSRuleOption Option;
protected readonly Source[] Source;
protected readonly IHostContext HostContext;
protected BindTargetMethod BindTargetNameHook;
protected BindTargetMethod BindTargetTypeHook;
protected BindTargetMethod BindFieldHook;
protected VisitTargetObject VisitTargetObject;
private string[] _Include;
private Hashtable _Tag;
private Configuration.BaselineOption _Baseline;
private string[] _Convention;
private PathFilter _InputFilter;
private PipelineWriter _Writer;
private readonly HostPipelineWriter _Output;
private const int MIN_JSON_INDENT = 0;
private const int MAX_JSON_INDENT = 4;
protected PipelineBuilderBase(Source[] source, IHostContext hostContext)
{
Option = new PSRuleOption();
Source = source;
_Output = new HostPipelineWriter(hostContext, Option, ShouldProcess);
HostContext = hostContext;
BindTargetNameHook = PipelineHookActions.BindTargetName;
BindTargetTypeHook = PipelineHookActions.BindTargetType;
BindFieldHook = PipelineHookActions.BindField;
}
/// <summary>
/// Determines if the pipeline is executing in a remote PowerShell session.
/// </summary>
public bool InSession => HostContext != null && HostContext.InSession;
/// <inheritdoc/>
public void Name(string[] name)
{
if (name == null || name.Length == 0)
return;
_Include = name;
}
/// <inheritdoc/>
public void Tag(Hashtable tag)
{
if (tag == null || tag.Count == 0)
return;
_Tag = tag;
}
/// <inheritdoc/>
public void Convention(string[] convention)
{
if (convention == null || convention.Length == 0)
return;
_Convention = convention;
}
/// <inheritdoc/>
public virtual IPipelineBuilder Configure(PSRuleOption option)
{
if (option == null)
return this;
Option.Baseline = new Options.BaselineOption(option.Baseline);
Option.Binding = new BindingOption(option.Binding);
Option.Convention = new ConventionOption(option.Convention);
Option.Execution = GetExecutionOption(option.Execution);
Option.Input = new InputOption(option.Input);
Option.Input.Format ??= InputOption.Default.Format;
Option.Output = new OutputOption(option.Output);
Option.Output.Outcome ??= OutputOption.Default.Outcome;
Option.Output.Banner ??= OutputOption.Default.Banner;
Option.Output.Style = GetStyle(option.Output.Style ?? OutputOption.Default.Style.Value);
Option.Repository = GetRepository(option.Repository);
return this;
}
/// <inheritdoc/>
public abstract IPipeline Build(IPipelineWriter writer = null);
/// <inheritdoc/>
public void Baseline(Configuration.BaselineOption baseline)
{
if (baseline == null)
return;
_Baseline = baseline;
}
/// <summary>
/// Require correct module versions for pipeline execution.
/// </summary>
protected bool RequireModules()
{
var result = true;
if (Option.Requires.TryGetValue(ENGINE_MODULE_NAME, out var requiredVersion))
{
var engineVersion = Engine.GetVersion();
if (GuardModuleVersion(ENGINE_MODULE_NAME, engineVersion, requiredVersion))
result = false;
}
for (var i = 0; Source != null && i < Source.Length; i++)
{
if (Source[i].Module != null && Option.Requires.TryGetValue(Source[i].Module.Name, out requiredVersion))
{
if (GuardModuleVersion(Source[i].Module.Name, Source[i].Module.Version, requiredVersion))
result = false;
}
}
return result;
}
/// <summary>
/// Require sources for pipeline execution.
/// </summary>
protected bool RequireSources()
{
if (Source == null || Source.Length == 0)
{
PrepareWriter().WarnRulePathNotFound();
return false;
}
return true;
}
private bool GuardModuleVersion(string moduleName, string moduleVersion, string requiredVersion)
{
if (!TryModuleVersion(moduleVersion, requiredVersion))
{
var writer = PrepareWriter();
writer.ErrorRequiredVersionMismatch(moduleName, moduleVersion, requiredVersion);
writer.End(new DefaultPipelineResult(null, BreakLevel.None) { HadErrors = true });
return true;
}
return false;
}
private static bool TryModuleVersion(string moduleVersion, string requiredVersion)
{
return SemanticVersion.TryParseVersion(moduleVersion, out var version) &&
SemanticVersion.TryParseConstraint(requiredVersion, out var constraint) &&
constraint.Equals(version);
}
protected PipelineContext PrepareContext(BindTargetMethod bindTargetName, BindTargetMethod bindTargetType, BindTargetMethod bindField)
{
var unresolved = new List<ResourceRef>();
if (_Baseline is Configuration.BaselineOption.BaselineRef baselineRef)
unresolved.Add(new BaselineRef(ResolveBaselineGroup(baselineRef.Name), ScopeType.Explicit));
return PipelineContext.New(
option: Option,
hostContext: HostContext,
reader: PrepareReader(),
bindTargetName: bindTargetName,
bindTargetType: bindTargetType,
bindField: bindField,
optionBuilder: GetOptionBuilder(),
unresolved: unresolved
);
}
protected string[] ResolveBaselineGroup(string[] name)
{
for (var i = 0; name != null && i < name.Length; i++)
name[i] = ResolveBaselineGroup(name[i]);
return name;
}
protected string ResolveBaselineGroup(string name)
{
if (name == null || name.Length < 2 || !name.StartsWith("@") ||
Option == null || Option.Baseline == null || Option.Baseline.Group == null ||
Option.Baseline.Group.Count == 0)
return name;
var key = name.Substring(1);
if (!Option.Baseline.Group.TryGetValue(key, out var baselines) || baselines.Length == 0)
throw new PipelineConfigurationException("Baseline.Group", string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.PSR0003, key));
var writer = PrepareWriter();
writer.WriteVerbose($"Using baseline group '{key}': {baselines[0]}");
return baselines[0];
}
protected virtual PipelineInputStream PrepareReader()
{
return new PipelineInputStream(null, GetInputObjectSourceFilter(), Option);
}
protected virtual PipelineWriter PrepareWriter()
{
if (_Writer != null)
return _Writer;
var output = GetOutput();
_Writer = Option.Output.Format switch
{
OutputFormat.Csv => new CsvOutputWriter(output, Option, ShouldProcess),
OutputFormat.Json => new JsonOutputWriter(output, Option, ShouldProcess),
OutputFormat.NUnit3 => new NUnit3OutputWriter(output, Option, ShouldProcess),
OutputFormat.Yaml => new YamlOutputWriter(output, Option, ShouldProcess),
OutputFormat.Markdown => new MarkdownOutputWriter(output, Option, ShouldProcess),
OutputFormat.Wide => new WideOutputWriter(output, Option, ShouldProcess),
OutputFormat.Sarif => new SarifOutputWriter(Source, output, Option, ShouldProcess),
_ => output,
};
return _Writer;
}
protected virtual PipelineWriter GetOutput(bool writeHost = false)
{
// Redirect to file instead
return !string.IsNullOrEmpty(Option.Output.Path)
? new FileOutputWriter(
inner: _Output,
option: Option,
encoding: Option.Output.GetEncoding(),
path: Option.Output.Path,
shouldProcess: HostContext.ShouldProcess,
writeHost: writeHost
)
: _Output;
}
protected static string[] GetCulture(string[] culture)
{
var result = new List<string>();
var parent = new List<string>();
var set = new HashSet<string>();
for (var i = 0; culture != null && i < culture.Length; i++)
{
var c = CultureInfo.CreateSpecificCulture(culture[i]);
if (!set.Contains(c.Name))
{
result.Add(c.Name);
set.Add(c.Name);
}
for (var p = c.Parent; !string.IsNullOrEmpty(p.Name); p = p.Parent)
{
if (!set.Contains(p.Name))
{
parent.Add(p.Name);
set.Add(p.Name);
}
}
}
if (parent.Count > 0)
result.AddRange(parent);
return result.Count == 0 ? null : result.ToArray();
}
protected static RepositoryOption GetRepository(RepositoryOption option)
{
var result = new RepositoryOption(option);
if (string.IsNullOrEmpty(result.Url) && GitHelper.TryRepository(out var url))
result.Url = url;
if (string.IsNullOrEmpty(result.BaseRef) && GitHelper.TryBaseRef(out var baseRef))
result.BaseRef = baseRef;
return result;
}
/// <summary>
/// Coalesce execution options with defaults.
/// </summary>
protected static ExecutionOption GetExecutionOption(ExecutionOption option)
{
var result = ExecutionOption.Combine(option, ExecutionOption.Default);
// Handle when preference is set to none. The default should be used.
result.AliasReference = result.AliasReference == ExecutionActionPreference.None ? ExecutionOption.Default.AliasReference.Value : result.AliasReference;
result.DuplicateResourceId = result.DuplicateResourceId == ExecutionActionPreference.None ? ExecutionOption.Default.DuplicateResourceId.Value : result.DuplicateResourceId;
result.InvariantCulture = result.InvariantCulture == ExecutionActionPreference.None ? ExecutionOption.Default.InvariantCulture.Value : result.InvariantCulture;
result.RuleExcluded = result.RuleExcluded == ExecutionActionPreference.None ? ExecutionOption.Default.RuleExcluded.Value : result.RuleExcluded;
result.RuleInconclusive = result.RuleInconclusive == ExecutionActionPreference.None ? ExecutionOption.Default.RuleInconclusive.Value : result.RuleInconclusive;
result.RuleSuppressed = result.RuleSuppressed == ExecutionActionPreference.None ? ExecutionOption.Default.RuleSuppressed.Value : result.RuleSuppressed;
result.SuppressionGroupExpired = result.SuppressionGroupExpired == ExecutionActionPreference.None ? ExecutionOption.Default.SuppressionGroupExpired.Value : result.SuppressionGroupExpired;
result.UnprocessedObject = result.UnprocessedObject == ExecutionActionPreference.None ? ExecutionOption.Default.UnprocessedObject.Value : result.UnprocessedObject;
return result;
}
protected PathFilter GetInputObjectSourceFilter()
{
return Option.Input.IgnoreObjectSource.GetValueOrDefault(InputOption.Default.IgnoreObjectSource.Value) ? GetInputFilter() : null;
}
protected PathFilter GetInputFilter()
{
if (_InputFilter == null)
{
var basePath = Environment.GetWorkingPath();
var ignoreGitPath = Option.Input.IgnoreGitPath ?? InputOption.Default.IgnoreGitPath.Value;
var ignoreRepositoryCommon = Option.Input.IgnoreRepositoryCommon ?? InputOption.Default.IgnoreRepositoryCommon.Value;
var builder = PathFilterBuilder.Create(basePath, Option.Input.PathIgnore, ignoreGitPath, ignoreRepositoryCommon);
builder.UseGitIgnore();
_InputFilter = builder.Build();
}
return _InputFilter;
}
private OptionContextBuilder GetOptionBuilder()
{
return new OptionContextBuilder(Option, _Include, _Tag, _Convention);
}
protected void ConfigureBinding(PSRuleOption option)
{
if (option.Pipeline.BindTargetName != null && option.Pipeline.BindTargetName.Count > 0)
{
// Do not allow custom binding functions to be used with constrained language mode
if (Option.Execution.LanguageMode == LanguageMode.ConstrainedLanguage)
throw new PipelineConfigurationException(optionName: "BindTargetName", message: PSRuleResources.ConstrainedTargetBinding);
foreach (var action in option.Pipeline.BindTargetName)
BindTargetNameHook = AddBindTargetAction(action, BindTargetNameHook);
}
if (option.Pipeline.BindTargetType != null && option.Pipeline.BindTargetType.Count > 0)
{
// Do not allow custom binding functions to be used with constrained language mode
if (Option.Execution.LanguageMode == LanguageMode.ConstrainedLanguage)
throw new PipelineConfigurationException(optionName: "BindTargetType", message: PSRuleResources.ConstrainedTargetBinding);
foreach (var action in option.Pipeline.BindTargetType)
BindTargetTypeHook = AddBindTargetAction(action, BindTargetTypeHook);
}
}
private static BindTargetMethod AddBindTargetAction(BindTargetFunc action, BindTargetMethod previous)
{
// Nest the previous write action in the new supplied action
// Execution chain will be: action -> previous -> previous..n
return (string[] propertyNames, bool caseSensitive, bool preferTargetInfo, object targetObject, out string path) =>
{
return action(propertyNames, caseSensitive, preferTargetInfo, targetObject, previous, out path);
};
}
private static BindTargetMethod AddBindTargetAction(BindTargetName action, BindTargetMethod previous)
{
return AddBindTargetAction((string[] propertyNames, bool caseSensitive, bool preferTargetInfo, object targetObject, BindTargetMethod next, out string path) =>
{
path = null;
var targetType = action(targetObject);
return string.IsNullOrEmpty(targetType) ? next(propertyNames, caseSensitive, preferTargetInfo, targetObject, out path) : targetType;
}, previous);
}
protected void AddVisitTargetObjectAction(VisitTargetObjectAction action)
{
// Nest the previous write action in the new supplied action
// Execution chain will be: action -> previous -> previous..n
var previous = VisitTargetObject;
VisitTargetObject = (targetObject) => action(targetObject, previous);
}
/// <summary>
/// Normalizes JSON indent range between minimum 0 and maximum 4.
/// </summary>
/// <param name="jsonIndent"></param>
/// <returns>The number of characters to indent.</returns>
protected static int NormalizeJsonIndentRange(int? jsonIndent)
{
if (jsonIndent.HasValue)
{
if (jsonIndent < MIN_JSON_INDENT)
return MIN_JSON_INDENT;
else if (jsonIndent > MAX_JSON_INDENT)
return MAX_JSON_INDENT;
return jsonIndent.Value;
}
return MIN_JSON_INDENT;
}
protected bool TryChangedFiles(out string[] files)
{
files = null;
if (!Option.Input.IgnoreUnchangedPath.GetValueOrDefault(InputOption.Default.IgnoreUnchangedPath.Value) ||
!GitHelper.TryGetChangedFiles(Option.Repository.BaseRef, "d", null, out files))
return false;
for (var i = 0; i < files.Length; i++)
HostContext.Verbose(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.UsingChangedFile, files[i]));
return true;
}
protected bool ShouldProcess(string target, string action)
{
return HostContext == null || HostContext.ShouldProcess(target, action);
}
protected static OutputStyle GetStyle(OutputStyle style)
{
if (style != OutputStyle.Detect)
return style;
if (Environment.IsAzurePipelines())
return OutputStyle.AzurePipelines;
if (Environment.IsGitHubActions())
return OutputStyle.GitHubActions;
return Environment.IsVisualStudioCode() ?
OutputStyle.VisualStudioCode :
OutputStyle.Client;
}
}

Просмотреть файл

@ -14,17 +14,15 @@ namespace PSRule.Pipeline;
/// <summary>
/// A stream of input objects that will be evaluated.
/// </summary>
internal sealed class PipelineInputStream
internal sealed class PipelineInputStream : IPipelineReader
{
private readonly VisitTargetObject _Input;
private readonly InputPathBuilder _InputPath;
private readonly PathFilter _InputFilter;
private readonly ConcurrentQueue<ITargetObject> _Queue;
private readonly EmitterCollection _EmitterCollection;
public PipelineInputStream(VisitTargetObject input, InputPathBuilder inputPath, PathFilter inputFilter, PSRuleOption option)
public PipelineInputStream(InputPathBuilder inputPath, PathFilter inputFilter, PSRuleOption option)
{
_Input = input;
_InputPath = inputPath;
_InputFilter = inputFilter;
_Queue = new ConcurrentQueue<ITargetObject>();
@ -35,12 +33,7 @@ internal sealed class PipelineInputStream
public bool IsEmpty => _Queue.IsEmpty;
/// <summary>
/// Add a new object into the stream.
/// </summary>
/// <param name="sourceObject">An object to process.</param>
/// <param name="targetType">A pre-bound type.</param>
/// <param name="skipExpansion">Determines if expansion is skipped.</param>
/// <inheritdoc/>
public void Enqueue(object sourceObject, string? targetType = null, bool skipExpansion = false)
{
if (sourceObject == null)
@ -55,11 +48,13 @@ internal sealed class PipelineInputStream
_EmitterCollection.Visit(sourceObject);
}
/// <inheritdoc/>
public bool TryDequeue(out ITargetObject sourceObject)
{
return _Queue.TryDequeue(out sourceObject);
}
/// <inheritdoc/>
public void Open()
{
if (_InputPath == null || _InputPath.Count == 0)
@ -97,11 +92,8 @@ internal sealed class PipelineInputStream
return true;
}
/// <summary>
/// Add a path to the list of inputs.
/// </summary>
/// <param name="path">The path of files to add.</param>
internal void Add(string path)
/// <inheritdoc/>
public void Add(string path)
{
_InputPath.Add(path);
}

Просмотреть файл

@ -109,6 +109,14 @@ public static class PipelineWriterExtensions
));
}
internal static void VerboseRuleDiscovery(this IPipelineWriter writer, string path)
{
if (writer == null || !writer.ShouldWriteVerbose() || string.IsNullOrEmpty(path))
return;
writer.WriteVerbose($"[PSRule][D] -- Discovering rules in: {path}");
}
private static string Format(string message, params object[] args)
{
return args == null || args.Length == 0 ? message : string.Format(Thread.CurrentThread.CurrentCulture, message, args);

Просмотреть файл

@ -673,16 +673,16 @@ internal sealed class RunspaceContext : IDisposable, ILogger
internal void AddService(string id, object service)
{
ResourceHelper.ParseIdString(LanguageScope.Name, id, out var scopeName, out var name);
if (!StringComparer.OrdinalIgnoreCase.Equals(LanguageScope.Name, scopeName))
if (!StringComparer.OrdinalIgnoreCase.Equals(LanguageScope.Name, scopeName) || string.IsNullOrEmpty(name))
return;
LanguageScope.AddService(name, service);
LanguageScope.AddService(name!, service);
}
internal object? GetService(string id)
{
ResourceHelper.ParseIdString(LanguageScope.Name, id, out var scopeName, out var name);
return !_LanguageScopes.TryScope(scopeName, out var scope) ? null : scope.GetService(name);
return !_LanguageScopes.TryScope(scopeName, out var scope) || string.IsNullOrEmpty(name) ? null : scope.GetService(name!);
}
private void RunConventionInitialize()

Просмотреть файл

@ -180,7 +180,7 @@ public sealed class PipelineTests
Environment.UseCurrentCulture(CultureInfo.InvariantCulture);
var context = PipelineContext.New(GetOption(), null, null, null, null, null, new OptionContextBuilder(), null);
var writer = new TestWriter(GetOption());
var pipeline = new GetRulePipeline(context, GetSource(), new PipelineInputStream(null, null, null, null), writer, false);
var pipeline = new GetRulePipeline(context, GetSource(), new PipelineInputStream(null, null, null), writer, false);
try
{
pipeline.Begin();
@ -202,7 +202,7 @@ public sealed class PipelineTests
option.Execution.InvariantCulture = ExecutionActionPreference.Ignore;
var context = PipelineContext.New(option, null, null, null, null, null, new OptionContextBuilder(), null);
var writer = new TestWriter(option);
var pipeline = new GetRulePipeline(context, GetSource(), new PipelineInputStream(null, null, null, null), writer, false);
var pipeline = new GetRulePipeline(context, GetSource(), new PipelineInputStream(null, null, null), writer, false);
try
{
pipeline.Begin();