From 3b6bba3464be194a76d0a41b6615b1b5d3007cc8 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Thu, 18 Dec 2025 23:59:37 -0500 Subject: [PATCH 01/12] Add Invoke-Member and Get-AssemblyLoadContext --- module/ClassExplorer.psd1 | 4 +- module/ClassExplorer.psm1 | 2 +- src/ClassExplorer/ALCPoly.cs | 137 +++++++ .../Commands/FindMemberCommand.cs | 8 + .../Commands/GetAssemblyLoadContextCommand.cs | 95 +++++ .../Commands/InvokeMemberCommand.cs | 384 ++++++++++++++++++ src/ClassExplorer/IEnumerationCallback.cs | 2 + src/ClassExplorer/MemberSearch.cs | 3 + .../NamespaceArgumentCompleter.cs | 2 + src/ClassExplorer/PipelineEmitter.cs | 61 +++ src/ClassExplorer/ReflectionSearch.cs | 6 +- src/ClassExplorer/ReflectionSearchOptions.cs | 2 + src/ClassExplorer/SR.resx | 12 + src/ClassExplorer/Search.cs | 2 + src/ClassExplorer/TypeArgumentCompleter.cs | 4 + .../TypeFullNameArgumentCompleter.cs | 2 + src/ClassExplorer/TypeHelpers.cs | 24 ++ .../TypeNameArgumentCompleter.cs | 2 + src/ClassExplorer/TypeSearch.cs | 24 +- 19 files changed, 769 insertions(+), 7 deletions(-) create mode 100644 src/ClassExplorer/ALCPoly.cs create mode 100644 src/ClassExplorer/Commands/GetAssemblyLoadContextCommand.cs create mode 100644 src/ClassExplorer/Commands/InvokeMemberCommand.cs diff --git a/module/ClassExplorer.psd1 b/module/ClassExplorer.psd1 index cb19582..3485803 100644 --- a/module/ClassExplorer.psd1 +++ b/module/ClassExplorer.psd1 @@ -48,13 +48,13 @@ ProcessorArchitecture = 'None' FunctionsToExport = @() # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. -CmdletsToExport = 'Find-Member', 'Find-Type', 'Get-Assembly', 'Get-Parameter', 'Format-MemberSignature' +CmdletsToExport = 'Find-Member', 'Find-Type', 'Get-Assembly', 'Get-Parameter', 'Format-MemberSignature', 'Invoke-Member', 'Get-AssemblyLoadContext' # Variables to export from this module VariablesToExport = @() # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. -AliasesToExport = @('fit', 'fime', 'gasm', 'gpa') +AliasesToExport = @('fit', 'fime', 'gasm', 'gpa', 'im', 'galc') # List of all files packaged with this module # FileList = @() diff --git a/module/ClassExplorer.psm1 b/module/ClassExplorer.psm1 index d16ac31..5ddce13 100644 --- a/module/ClassExplorer.psm1 +++ b/module/ClassExplorer.psm1 @@ -11,4 +11,4 @@ if (-not $env:CLASS_EXPLORER_TRUE_CHARACTER) { Update-FormatData -PrependPath $PSScriptRoot\ClassExplorer.format.ps1xml Update-TypeData -PrependPath $PSScriptRoot\ClassExplorer.types.ps1xml -ErrorAction Ignore -Export-ModuleMember -Cmdlet Find-Type, Find-Member, Format-MemberSignature, Get-Assembly, Get-Parameter -Alias * +Export-ModuleMember -Cmdlet Find-Type, Find-Member, Format-MemberSignature, Get-Assembly, Get-Parameter, Invoke-Member, Get-AssemblyLoadContext -Alias * diff --git a/src/ClassExplorer/ALCPoly.cs b/src/ClassExplorer/ALCPoly.cs new file mode 100644 index 0000000..bf7119b --- /dev/null +++ b/src/ClassExplorer/ALCPoly.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Reflection; +using System.Runtime.Loader; + +#if NETFRAMEWORK +namespace System.Runtime.Loader +{ + internal sealed class AssemblyLoadContext + { + public static AssemblyLoadContext Default { get; } = new(); + public bool IsCollectible => false; + + public IEnumerable Assemblies => AppDomain.CurrentDomain.GetAssemblies(); + + public string Name => "Default"; + } +} +#endif + +namespace ClassExplorer +{ + internal static class ALC + { + public static IEnumerable SafeGetAssemblies(AssemblyLoadContext alc) + { + IEnumerator? enumerator = null; + try + { + try + { + enumerator = alc.Assemblies?.GetEnumerator(); + } + catch + { + } + + if (enumerator is null) + { + yield break; + } + + while (true) + { + try + { + if (!enumerator.MoveNext()) + { + yield break; + } + } + catch + { + yield break; + } + + Assembly? assembly = null; + try { assembly = enumerator.Current; } catch { } + if (assembly is not null) + { + yield return assembly; + } + } + } + finally + { + (enumerator as IDisposable)?.Dispose(); + } + } + + public static string SafeGetName(AssemblyLoadContext alc) + { + string? name = null; + try + { + name = alc.Name; + } + catch + { + } + + return name ?? alc.GetType().FullName; + } + + public static void AssertSupported(PSCmdlet cmdlet) + { +#if NETFRAMEWORK + cmdlet.ThrowTerminatingError( + new ErrorRecord( + new NotSupportedException("AssemblyLoadContext does not exist in .NET Framework."), + "ALCNotSupported", + ErrorCategory.NotEnabled, + null)); +#endif + } + + public static IEnumerable GetAll() + { +#if NETFRAMEWORK + return [AssemblyLoadContext.Default]; +#else + return AssemblyLoadContext.All; +#endif + } + + public static AssemblyLoadContext GetLoadContext(Assembly assembly) + { +#if NETFRAMEWORK + return AssemblyLoadContext.Default; +#else + return AssemblyLoadContext.GetLoadContext(assembly); +#endif + } + + public static AssemblyLoadContext GetDefault() + { +#if NETFRAMEWORK + return AssemblyLoadContext.Default; +#else + return AssemblyLoadContext.Default; +#endif + } + + public static bool IsSupported + { + get + { +#if NETFRAMEWORK + return false; +#else + return true; +#endif + } + } + } +} diff --git a/src/ClassExplorer/Commands/FindMemberCommand.cs b/src/ClassExplorer/Commands/FindMemberCommand.cs index 5f00734..30a8ec1 100644 --- a/src/ClassExplorer/Commands/FindMemberCommand.cs +++ b/src/ClassExplorer/Commands/FindMemberCommand.cs @@ -181,6 +181,14 @@ public SwitchParameter Extension set => _options.Extension = value; } + [Parameter(DontShow = true, ValueFromPipelineByPropertyName = true)] + [Alias("__ce_Instance")] + public object? PropagatedInstance + { + get => _options.Source; + set => _options.Source = value; + } + [Parameter] [Alias("sig")] public ScriptBlockStringOrType? Signature { get; set; } diff --git a/src/ClassExplorer/Commands/GetAssemblyLoadContextCommand.cs b/src/ClassExplorer/Commands/GetAssemblyLoadContextCommand.cs new file mode 100644 index 0000000..f80a28b --- /dev/null +++ b/src/ClassExplorer/Commands/GetAssemblyLoadContextCommand.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.Management.Automation; +using System.Reflection; +using System.Runtime.Loader; + +namespace ClassExplorer.Commands; + +[Cmdlet(VerbsCommon.Get, "AssemblyLoadContext")] +[Alias("galc")] +public sealed class GetAssemblyLoadContextCommand : PSCmdlet +{ + [Parameter(Position = 0)] + [ValidateNotNullOrEmpty] + [SupportsWildcards] + public string? Name { get; set; } + + [Parameter] + public SwitchParameter Default { get; set; } + + [Parameter(ValueFromPipeline = true)] + public PSObject? InputObject { get; set; } + + private HashSet? _processedAssemblies; + + private HashSet? _processedAlcs; + + private StringMatcher? _nameMatcher; + + protected override void BeginProcessing() + { + ALC.AssertSupported(this); + + if (Name is [..]) + { + _nameMatcher = StringMatcher.Create(Name); + } + } + + protected override void ProcessRecord() + { + if (!MyInvocation.ExpectingInput || InputObject is null) + { + return; + } + + Assembly? assembly = TypeHelpers.GetReflectedAssembly(InputObject.BaseObject); + if (assembly is null || !(_processedAssemblies ??= new()).Add(assembly)) + { + return; + } + + AssemblyLoadContext alc = ALC.GetLoadContext(assembly); + if (!(_processedAlcs ??= new()).Add(alc)) + { + return; + } + + if (_nameMatcher?.IsMatch(ALC.SafeGetName(alc)) is false) + { + return; + } + + WriteObject(alc); + } + + protected override void EndProcessing() + { + if (MyInvocation.ExpectingInput) + { + return; + } + + if (Default) + { + WriteObject(ALC.GetDefault()); + return; + } + + if (_nameMatcher is null) + { + WriteObject(ALC.GetAll(), enumerateCollection: true); + return; + } + + foreach (AssemblyLoadContext alc in ALC.GetAll()) + { + if (!_nameMatcher.IsMatch(ALC.SafeGetName(alc))) + { + continue; + } + + WriteObject(alc); + } + } +} diff --git a/src/ClassExplorer/Commands/InvokeMemberCommand.cs b/src/ClassExplorer/Commands/InvokeMemberCommand.cs new file mode 100644 index 0000000..7f54fb6 --- /dev/null +++ b/src/ClassExplorer/Commands/InvokeMemberCommand.cs @@ -0,0 +1,384 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Management.Automation; +using System.Management.Automation.Internal; +using System.Reflection; +using System.Runtime.CompilerServices; +using ClassExplorer.Internal; + +using PSAllowNull = System.Management.Automation.AllowNullAttribute; + +namespace ClassExplorer.Commands; + +[Cmdlet(VerbsLifecycle.Invoke, "Member")] +[Alias("ivm")] +public sealed class InvokeMemberCommand : PSCmdlet +{ + + [Parameter(Mandatory = true, ValueFromPipeline = true)] + [ValidateNotNull] + public MemberInfo? InputObject { get; set; } + + [Parameter(ValueFromPipelineByPropertyName = true)] + [PSAllowNull, AllowEmptyCollection, AllowEmptyString] + [Alias("__ce_Instance")] + public object? Instance { get; set; } + + + [Parameter(ValueFromRemainingArguments = true)] + [PSAllowNull, AllowEmptyCollection, AllowEmptyString] + public object?[]? ArgumentList { get; set; } + + [Parameter] + public SwitchParameter SkipPSObjectUnwrap { get; set; } + + protected override unsafe void ProcessRecord() + { + Poly.Assert(InputObject is not null); + object?[] args = ArgumentList ??= []; + object?[] newArgs = new object?[args.Length]; + args.CopyTo(newArgs); + args = newArgs; + + if (InputObject is FieldInfo field) + { + if (args is []) + { + WriteResult(field.GetValue(MaybeUnwrapTarget(Instance))); + return; + } + + field.SetValue(MaybeUnwrapTarget(Instance), GetSingleArg(args, field.FieldType)); + return; + } + + if (InputObject is PropertyInfo property) + { + if (args is []) + { + WriteResult(property.GetValue(MaybeUnwrapTarget(Instance))); + return; + } + + if (property.GetIndexParameters().Length == args.Length) + { + InputObject = property.GetGetMethod(nonPublic: true); + } + else + { + InputObject = property.GetSetMethod(nonPublic: true); + } + + if (InputObject is null) + { + ThrowTerminatingError( + new ErrorRecord( + new InvalidOperationException(SR.Format(SR.MemberNotWritable, FormatMemberInfo(property))), + nameof(SR.MemberNotWritable), + ErrorCategory.InvalidOperation, + property)); + return; + } + } + + + + if (InputObject is not MethodBase methodBase) + { + ThrowTerminatingError( + new ErrorRecord( + new PSArgumentException(SR.Format(SR.MemberTypeNotSupported, InputObject.GetType())), + nameof(SR.MemberTypeNotSupported), + ErrorCategory.InvalidOperation, + InputObject)); + + return; + } + + ParameterInfo[] parameters = methodBase.GetParameters(); + if (parameters.Length < args.Length) + { + ThrowIncorrectArgumentCount(methodBase, parameters.Length, args.Length); + return; + } + + object?[] methodArgs = args; + if (parameters.Length != args.Length) + { + methodArgs = new object?[parameters.Length]; + args.CopyTo(methodArgs); + } + + List? refs = null; + for (int i = 0; i < parameters.Length; i++) + { + ParameterInfo parameter = parameters[i]; + if (args.Length <= i) + { + if (!parameter.ParameterType.IsByRef) + { + if (parameter.HasDefaultValue) + { + methodArgs[i] = parameter.RawDefaultValue; + } + else + { + ThrowIncorrectArgumentCount(methodBase, parameters.Length, args.Length); + } + } + } + + ConvertTo( + new ArrayIndexRef(i, methodArgs), + parameter.ParameterType, + parameter, + ref refs); + } + + object? result = AutomationNull.Value; + if (methodBase is ConstructorInfo ctor) + { + if (ctor.IsStatic) + { + RuntimeHelpers.RunClassConstructor((ctor.DeclaringType ?? ctor.ReflectedType)!.TypeHandle); + } + else + { + result = ctor.Invoke(methodArgs); + } + } + else if (methodBase is MethodInfo method) + { + if (method.ReturnType == typeof(void)) + { + method.Invoke(MaybeUnwrapTarget(Instance), methodArgs); + } + else + { + result = method.Invoke(MaybeUnwrapTarget(Instance), methodArgs); + } + } + + if (refs is null) + { + WriteResult(result); + return; + } + + for (int i = refs.Count - 1; i >= 0; i--) + { + RefInfo info = refs[i]; + if (info.PSRef is null) + { + continue; + } + + info.PSRef.Value = info.ArrayRef.Value; + refs.RemoveAt(i); + } + + if (refs is []) + { + WriteResult(result); + return; + } + + PSObject pso = new PSObject(); + if (result != AutomationNull.Value) + { + pso.Properties.Add(new PSNoteProperty("Result", result)); + } + + foreach (RefInfo info in refs) + { + object? currentValue = info.ArrayRef.Value; + if (currentValue is Pointer ptr) + { + currentValue = (nint)Pointer.Unbox(ptr); + } + + pso.Properties.Add( + new PSNoteProperty( + info.Parameter?.Name ?? info.ArrayRef.Index.ToString(), + currentValue)); + } + + WriteObject(pso); + } + + private object? MaybeUnwrapTarget(object? value) + { + Poly.Assert(InputObject is not null); + bool isStatic = InputObject switch + { + MethodBase m => m.IsStatic, + PropertyInfo m => (m.GetGetMethod(nonPublic: true) ?? m.GetSetMethod(nonPublic: true))?.IsStatic ?? false, + FieldInfo m => m.IsStatic, + EventInfo m => (m.GetAddMethod(nonPublic: true) ?? m.GetRemoveMethod(nonPublic: true))?.IsStatic ?? false, + _ => false, + }; + + if (isStatic) + { + return null; + } + + return MaybeUnwrap(value); + } + + private object? MaybeUnwrap(object? value) + { + if (SkipPSObjectUnwrap) + { + return value; + } + + if (value is PSObject { BaseObject: object unwrappedValue }) + { + return unwrappedValue; + } + + return value; + } + + private unsafe void WriteResult(object? value) + { + if (value == AutomationNull.Value) + { + return; + } + + if (value is Pointer ptr) + { + WriteObject((nint)Pointer.Unbox(ptr)); + return; + } + + WriteObject(value); + } + + private object? GetSingleArg(object?[] args, Type type) + { + Poly.Assert(InputObject is not null); + if (args is { Length: > 1 }) + { + ThrowIncorrectArgumentCount(InputObject, expected: 1, args.Length); + } + + ConvertTo(new(0, args), type); + return args[0]; + } + + [DoesNotReturn] + private void ThrowIncorrectArgumentCount(MemberInfo member, int expected, int actual) + { + ThrowTerminatingError( + new ErrorRecord( + new PSArgumentException( + SR.Format( + SR.WrongArgumentCount, + expected, + actual, + FormatMemberInfo(member)), + nameof(ArgumentList)), + nameof(SR.WrongArgumentCount), + ErrorCategory.InvalidArgument, + ArgumentList)); + + // unreachable + throw null!; + } + + private string FormatMemberInfo(MemberInfo member) + { + var writer = new SignatureWriter(_Colors.Instance) + { + NoColor = true, + Simple = true, + }; + + return writer.WriteMember(member).ToString(); + } + + private void ConvertTo(ArrayIndexRef target, Type type, ParameterInfo? parameter = null) + { + List? refs = null; + ConvertTo(target, type, parameter, ref refs); + } + + private unsafe void ConvertTo(ArrayIndexRef target, Type type, ParameterInfo? parameter, ref List? refs) + { + if (type.IsByRef) + { + if (target.Value is PSReference psRef) + { + (refs ??= new()).Add(new(target, parameter, psRef)); + target.Value = psRef.Value; + } + else + { + (refs ??= new()).Add(new(target, parameter, psRef: null)); + } + + type = type.GetElementType()!; + } + + if (type.IsPointer) + { + if (target.Value is null) + { + target.Value = Pointer.Box(null, type); + return; + } + + target.Value = Pointer.Box((void*)LanguagePrimitives.ConvertTo(target.Value), type); + return; + } + + if (target.Value is null) + { + if (!type.IsValueType) + { + return; + } + + target.Value = Activator.CreateInstance(type); + return; + } + + if (type.IsAssignableFrom(target.Value.GetType())) + { + if (target.Value is PSObject && type != typeof(PSObject)) + { + target.Value = MaybeUnwrap(target.Value); + } + + return; + } + + target.Value = LanguagePrimitives.ConvertTo(target.Value, type); + } +} + +internal readonly struct RefInfo(ArrayIndexRef arrayRef, ParameterInfo? parameter, PSReference? psRef) +{ + public readonly ArrayIndexRef ArrayRef = arrayRef; + + public readonly ParameterInfo? Parameter = parameter; + + public readonly PSReference? PSRef = psRef; +} + +internal readonly struct ArrayIndexRef(int index, object?[] array) +{ + public readonly int Index = index; + + public readonly object?[] Array = array; + + public object? Value + { + get => Array[Index]; + set => Array[Index] = value; + } +} diff --git a/src/ClassExplorer/IEnumerationCallback.cs b/src/ClassExplorer/IEnumerationCallback.cs index 70c48b3..bb7ec15 100644 --- a/src/ClassExplorer/IEnumerationCallback.cs +++ b/src/ClassExplorer/IEnumerationCallback.cs @@ -3,4 +3,6 @@ namespace ClassExplorer; internal interface IEnumerationCallback { void Invoke(T value); + + void Invoke(T value, object? source); } diff --git a/src/ClassExplorer/MemberSearch.cs b/src/ClassExplorer/MemberSearch.cs index 6a21244..1546035 100644 --- a/src/ClassExplorer/MemberSearch.cs +++ b/src/ClassExplorer/MemberSearch.cs @@ -42,6 +42,8 @@ public void Invoke(Type value) { _parent.SearchSingleType(value); } + + public void Invoke(Type value, object? instance) => Invoke(value); } public override void SearchSingleObject(PSObject pso) @@ -105,6 +107,7 @@ public override void SearchSingleObject(PSObject pso) return; } + _options.Source = pso.BaseObject; SearchSingleType(targetType); } diff --git a/src/ClassExplorer/NamespaceArgumentCompleter.cs b/src/ClassExplorer/NamespaceArgumentCompleter.cs index 43664d1..6af6eda 100644 --- a/src/ClassExplorer/NamespaceArgumentCompleter.cs +++ b/src/ClassExplorer/NamespaceArgumentCompleter.cs @@ -82,6 +82,8 @@ public void Invoke(Type value) CompletionResultType.ParameterValue, @namespace)); } + + public void Invoke(Type value, object? source) => Invoke(value); } } } diff --git a/src/ClassExplorer/PipelineEmitter.cs b/src/ClassExplorer/PipelineEmitter.cs index f7131a1..5374399 100644 --- a/src/ClassExplorer/PipelineEmitter.cs +++ b/src/ClassExplorer/PipelineEmitter.cs @@ -1,12 +1,73 @@ +using System; +using System.Linq.Expressions; using System.Management.Automation; +using System.Reflection; namespace ClassExplorer; internal readonly struct PipelineEmitter : IEnumerationCallback { + private static readonly Func? s_asPSObject; + + private static readonly Action? s_setHidden; + + static PipelineEmitter() + { + MethodInfo? asPso = typeof(PSObject).GetMethod( + "AsPSObject", + BindingFlags.NonPublic | BindingFlags.Static, + binder: null, + [typeof(object), typeof(bool)], + modifiers: null); + + if (asPso is null) + { + return; + } + + s_asPSObject = (Func)asPso.CreateDelegate(typeof(Func)); + + MethodInfo? setIsHidden = typeof(PSNoteProperty) + .GetProperty("IsHidden", BindingFlags.NonPublic | BindingFlags.Instance) + ?.GetSetMethod(nonPublic: true); + + if (setIsHidden is null) + { + return; + } + + ParameterExpression instance = Expression.Parameter(typeof(PSNoteProperty), "instance"); + s_setHidden = Expression.Lambda>( + Expression.Call( + instance, + setIsHidden, + Expression.Constant(true, typeof(bool))), + "set_IsHidden", + [instance]) + .Compile(); + } + private readonly PSCmdlet _cmdlet; public PipelineEmitter(PSCmdlet cmdlet) => _cmdlet = cmdlet; public void Invoke(T value) => _cmdlet.WriteObject(value, enumerateCollection: false); + + public void Invoke(T value, object? instance) + { + PSObject pso; + if (s_asPSObject is null || s_setHidden is null) + { + pso = PSObject.AsPSObject(value); + pso.Properties.Add(new PSNoteProperty("__ce_Instance", instance)); + _cmdlet.WriteObject(pso, enumerateCollection: false); + return; + } + + pso = s_asPSObject(value, true); + PSNoteProperty instanceProp = new("__ce_Instance", instance); + s_setHidden(instanceProp); + pso.Properties.Add(instanceProp); + _cmdlet.WriteObject(pso, enumerateCollection: false); + } } diff --git a/src/ClassExplorer/ReflectionSearch.cs b/src/ClassExplorer/ReflectionSearch.cs index 7b911b3..783966e 100644 --- a/src/ClassExplorer/ReflectionSearch.cs +++ b/src/ClassExplorer/ReflectionSearch.cs @@ -93,13 +93,13 @@ private void InitCommonFilters(List> filters) protected static bool AggregateFilter(TMemberType member, object? state, bool isPipeFilter = false) { ReflectionSearch context = - Unsafe.As>(state); + Unsafe.As>(state)!; try { if (context._filters.Length is 0) { - context._callback.Invoke(member); + context._callback.Invoke(member, context._options.Source); return false; } @@ -134,7 +134,7 @@ protected static bool AggregateFilter(TMemberType member, object? state, bool is return false; } - context._callback.Invoke(member); + context._callback.Invoke(member, context._options.Source); return false; } catch (PipelineStoppedException) diff --git a/src/ClassExplorer/ReflectionSearchOptions.cs b/src/ClassExplorer/ReflectionSearchOptions.cs index 90d86c3..8f83bf4 100644 --- a/src/ClassExplorer/ReflectionSearchOptions.cs +++ b/src/ClassExplorer/ReflectionSearchOptions.cs @@ -22,4 +22,6 @@ internal abstract class ReflectionSearchOptions public AccessView AccessView { get; set; } public ScriptBlockStringOrType? Decoration { get; set; } + + public object? Source { get; set; } } diff --git a/src/ClassExplorer/SR.resx b/src/ClassExplorer/SR.resx index 70b4b01..a34212c 100644 --- a/src/ClassExplorer/SR.resx +++ b/src/ClassExplorer/SR.resx @@ -179,4 +179,16 @@ Message: {0} Input string was not in the correct format. Expected "0", "0..3", "..3" or "0.." where "0" is the start of the range and 3 is the end of the range. + + Expected {0} arguments but received {1} for member "{2}". + + + The member type "{0}" is not supported by "Invoke-Member". + + + Unable to set the value of the member {0}. + + + Expected signature of kind "{0}" but found a "{1}" signature. + diff --git a/src/ClassExplorer/Search.cs b/src/ClassExplorer/Search.cs index 70146bb..30128a1 100644 --- a/src/ClassExplorer/Search.cs +++ b/src/ClassExplorer/Search.cs @@ -43,6 +43,8 @@ public void Invoke(Type value) _result.Value = value; throw new CancelSearchException(); } + + public void Invoke(Type value, object? source) => Invoke(value); } #pragma warning disable RCS1194 diff --git a/src/ClassExplorer/TypeArgumentCompleter.cs b/src/ClassExplorer/TypeArgumentCompleter.cs index 4a9aedb..ee2c78c 100644 --- a/src/ClassExplorer/TypeArgumentCompleter.cs +++ b/src/ClassExplorer/TypeArgumentCompleter.cs @@ -36,6 +36,8 @@ public static IEnumerable GetTypesForCompletion(string wordToComplete) public ListBuilder(List types) => _types = types; public void Invoke(Type value) => _types.Add(value); + + public void Invoke(Type value, object? source) => Invoke(value); } /// @@ -96,6 +98,8 @@ public void Invoke(Type value) CompletionResultType.ParameterValue, tip)); } + + public void Invoke(Type value, object? source) => Invoke(value); } } } diff --git a/src/ClassExplorer/TypeFullNameArgumentCompleter.cs b/src/ClassExplorer/TypeFullNameArgumentCompleter.cs index 891bda7..189d5d1 100644 --- a/src/ClassExplorer/TypeFullNameArgumentCompleter.cs +++ b/src/ClassExplorer/TypeFullNameArgumentCompleter.cs @@ -80,6 +80,8 @@ public void Invoke(Type value) CompletionResultType.ParameterValue, tip)); } + + public void Invoke(Type value, object? source) => Invoke(value); } } } diff --git a/src/ClassExplorer/TypeHelpers.cs b/src/ClassExplorer/TypeHelpers.cs index 24bd9a4..0a1b26e 100644 --- a/src/ClassExplorer/TypeHelpers.cs +++ b/src/ClassExplorer/TypeHelpers.cs @@ -1,9 +1,33 @@ using System; +using System.Management.Automation; +using System.Reflection; namespace ClassExplorer; internal static class TypeHelpers { + public static Assembly? GetReflectedAssembly(object? member) + { + if (member is null) + { + return null; + } + + if (member is PSObject pso) + { + member = pso.BaseObject; + } + + return member switch + { + Type m => m.Assembly, + MethodInfo m => m.ReflectedType?.Assembly ?? m.Module?.Assembly, + MemberInfo m => m.ReflectedType?.Assembly, + Assembly a => a, + _ => member?.GetType()?.Assembly, + }; + } + // So if you have a structure like: // // class A diff --git a/src/ClassExplorer/TypeNameArgumentCompleter.cs b/src/ClassExplorer/TypeNameArgumentCompleter.cs index 8b2355d..ad7b717 100644 --- a/src/ClassExplorer/TypeNameArgumentCompleter.cs +++ b/src/ClassExplorer/TypeNameArgumentCompleter.cs @@ -69,6 +69,8 @@ public void Invoke(Type value) CompletionResultType.ParameterValue, tip)); } + + public void Invoke(Type value, object? source) => Invoke(value); } } } diff --git a/src/ClassExplorer/TypeSearch.cs b/src/ClassExplorer/TypeSearch.cs index 35d9bad..0ba88fa 100644 --- a/src/ClassExplorer/TypeSearch.cs +++ b/src/ClassExplorer/TypeSearch.cs @@ -3,6 +3,7 @@ using System.Management.Automation; using System.Reflection; using System.Runtime.CompilerServices; +using System.Runtime.Loader; using ClassExplorer.Signatures; namespace ClassExplorer; @@ -37,18 +38,39 @@ private void ProcessAssembly(Assembly assembly) foreach (Module module in modules) { + Type[]? types = null; try { - module.FindTypes(static (m, fc) => AggregateFilter(m, fc), this); + types = module.GetTypes(); } catch (ReflectionTypeLoadException) { } + + if (types is null or []) + { + continue; + } + + foreach (Type type in types) + { + AggregateFilter(type, this); + } } } public override void SearchSingleObject(PSObject pso) { + if (pso.BaseObject is AssemblyLoadContext alc) + { + foreach (Assembly assemblyFromAlc in ALC.SafeGetAssemblies(alc)) + { + ProcessAssembly(assemblyFromAlc); + } + + return; + } + if (pso.BaseObject is Assembly assembly) { ProcessAssembly(assembly); From 1da1fad99435250ae9688a82bcbd7f3c034e3b2c Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Fri, 19 Dec 2025 00:08:01 -0500 Subject: [PATCH 02/12] Forgot to save alias change --- module/ClassExplorer.psd1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/ClassExplorer.psd1 b/module/ClassExplorer.psd1 index 3485803..86a489d 100644 --- a/module/ClassExplorer.psd1 +++ b/module/ClassExplorer.psd1 @@ -54,7 +54,7 @@ CmdletsToExport = 'Find-Member', 'Find-Type', 'Get-Assembly', 'Get-Parameter', ' VariablesToExport = @() # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. -AliasesToExport = @('fit', 'fime', 'gasm', 'gpa', 'im', 'galc') +AliasesToExport = @('fit', 'fime', 'gasm', 'gpa', 'ivm', 'galc') # List of all files packaged with this module # FileList = @() From 20f75e3f0d645fd2b5a5b45f82955a0597851d7e Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Fri, 19 Dec 2025 17:15:18 -0500 Subject: [PATCH 03/12] Update workflow versions --- .github/workflows/build.yml | 8 ++++---- .github/workflows/publish.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0eec8a5..7daced5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,22 +29,22 @@ jobs: steps: - name: Check Version run: $PSVersionTable - - uses: actions/checkout@v1 + - uses: actions/checkout@v6 - name: Test and Build run: ./build.ps1 -Force -Configuration Release - - uses: actions/upload-artifact@v1 + - uses: actions/upload-artifact@v4 if: matrix.os == 'windows-latest' with: name: ClassExplorer path: ./Release/ClassExplorer - - uses: actions/upload-artifact@v1 + - uses: actions/upload-artifact@v4 if: matrix.os != 'windows-latest' with: name: ClassExplorer-${{ matrix.os }} path: ./Release/ClassExplorer - name: Upload Test Results if: always() - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Unit Test Results (${{ matrix.os }}) path: ./TestResults/Pester.xml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0260cc6..fb0a536 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -19,14 +19,14 @@ jobs: name: Publish runs-on: windows-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v6 - name: Test and Build run: ./build.ps1 -Force -Publish -Configuration Release env: GALLERY_API_KEY: ${{ secrets.GALLERY_API_KEY }} - name: Upload Test Results if: always() - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Unit Test Results path: ./TestResults/Pester.xml From e3f7df8c702ef63050fc1f40d634fff04b2dcfa7 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Fri, 19 Dec 2025 19:12:03 -0500 Subject: [PATCH 04/12] Fix test and skip code coverage --- ClassExplorer.build.ps1 | 45 ++++++++++++++++---------------------- test/Find-Member.Tests.ps1 | 25 +++++++++++++++------ 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/ClassExplorer.build.ps1 b/ClassExplorer.build.ps1 index bc09f76..596e120 100644 --- a/ClassExplorer.build.ps1 +++ b/ClassExplorer.build.ps1 @@ -10,16 +10,6 @@ param( [switch] $Force ) -$moduleName = 'ClassExplorer' -$testModuleManifestSplat = @{ - ErrorAction = 'Ignore' - WarningAction = 'Ignore' - Path = "$PSScriptRoot\module\$moduleName.psd1" -} - -$manifest = Test-ModuleManifest @testModuleManifestSplat -$moduleVersion = $manifest.Version - $tools = "$PSScriptRoot\tools" $script:GetDotNet = Get-Command $tools\GetDotNet.ps1 $script:AssertModule = Get-Command $tools\AssertRequiredModule.ps1 @@ -93,6 +83,7 @@ task GetProjectInfo { $manifest = Test-ModuleManifest @testModuleManifestSplat $script:ModuleVersion = $manifest.Version + $script:ReleasePath = "./Release/$script:ModuleName/$script:ModuleVersion" $script:_IsWindows = $true $runtimeInfoType = 'System.Runtime.InteropServices.RuntimeInformation' -as [type] try { @@ -132,7 +123,7 @@ task Clean { } task BuildDocs -If { Test-Path ./docs/$PSCulture/*.md } { - $releaseDocs = "./Release/ClassExplorer/$moduleVersion" + $releaseDocs = $script:ReleasePath $null = New-Item $releaseDocs/$PSCulture -ItemType Directory -Force -ErrorAction Ignore $null = New-ExternalHelp -Path ./docs/$PSCulture -OutputPath $releaseDocs/$PSCulture @@ -153,15 +144,15 @@ task CopyToRelease { $modern = $script:ModernTarget $legacy = $script:LegacyTarget - $releasePath = "./Release/ClassExplorer/$version" - if (-not (Test-Path -LiteralPath $releasePath)) { - $null = New-Item $releasePath -ItemType Directory + $release = $script:ReleasePath + if (-not (Test-Path -LiteralPath $release)) { + $null = New-Item $release -ItemType Directory } - Copy-Item -Path ./module/* -Destination $releasePath -Recurse -Force + Copy-Item -Path ./module/* -Destination $release -Recurse -Force if ($script:_IsWindows) { - $null = New-Item $releasePath/bin/Legacy -Force -ItemType Directory + $null = New-Item $release/bin/Legacy -Force -ItemType Directory $legacyFiles = ( 'ClassExplorer.dll', 'ClassExplorer.pdb', @@ -172,17 +163,17 @@ task CopyToRelease { 'System.Runtime.CompilerServices.Unsafe.dll') foreach ($file in $legacyFiles) { - Copy-Item -Force -LiteralPath ./artifacts/publish/ClassExplorer/${Configuration}_$legacy/$file -Destination $releasePath/bin/Legacy + Copy-Item -Force -LiteralPath (GetArtifactPath -FileName $file -Legacy) -Destination $release/bin/Legacy } } - $null = New-Item $releasePath/bin/Modern -Force -ItemType Directory + $null = New-Item $release/bin/Modern -Force -ItemType Directory $modernFiles = ( 'ClassExplorer.dll', 'ClassExplorer.pdb', 'ClassExplorer.deps.json') foreach ($file in $modernFiles) { - Copy-Item -Force -LiteralPath ./artifacts/publish/ClassExplorer/${Configuration}_$modern/$file -Destination $releasePath/bin/Modern + Copy-Item -Force -LiteralPath (GetArtifactPath -FileName $file) -Destination $release/bin/Modern } } @@ -210,19 +201,21 @@ task DoTest -If { Test-Path ./test/*.ps1 } { $powershellCommand = 'pwsh' } - $powershell = (Get-Command -CommandType Application $powershellCommand).Source + $powershell = Get-Command -CommandType Application $powershellCommand | Select-Object -First 1 -ExpandProperty Source + # Can't be bothered atm, gotta modernize this at some point. + $GenerateCodeCoverage = $false if ($GenerateCodeCoverage.IsPresent) { # OpenCover needs full pdb's. I'm very open to suggestions for streamlining this... # & $dotnet clean & $dotnet publish --configuration $Configuration --framework $script:LegacyTarget --verbosity quiet --nologo /p:DebugType=Full $moduleName = $Settings.Name - $release = '{0}\bin\Desktop\{1}' -f $Folders.Release, $moduleName - $coverage = '{0}\net471\{1}' -f $Folders.Build, $moduleName + $release = "$script:ReleasePath/bin/Legacy/$script:ModuleName" + $coverage = GetArtifactPath -Legacy $script:ModuleName - Rename-Item "$release.pdb" -NewName "$moduleName.pdb.tmp" - Rename-Item "$release.dll" -NewName "$moduleName.dll.tmp" + Rename-Item "$release.pdb" -NewName "$script:ModuleName.pdb.tmp" + Rename-Item "$release.dll" -NewName "$script:ModuleName.dll.tmp" Copy-Item "$coverage.pdb" "$release.pdb" Copy-Item "$coverage.dll" "$release.dll" @@ -236,8 +229,8 @@ task DoTest -If { Test-Path ./test/*.ps1 } { Remove-Item "$release.pdb" Remove-Item "$release.dll" - Rename-Item "$release.pdb.tmp" -NewName "$moduleName.pdb" - Rename-Item "$release.dll.tmp" -NewName "$moduleName.dll" + Rename-Item "$release.pdb.tmp" -NewName "$script:ModuleName.pdb" + Rename-Item "$release.dll.tmp" -NewName "$script:ModuleName.dll" } else { & $powershell -NoProfile -EncodedCommand $encodedCommand } diff --git a/test/Find-Member.Tests.ps1 b/test/Find-Member.Tests.ps1 index 4e5f0fd..931843d 100644 --- a/test/Find-Member.Tests.ps1 +++ b/test/Find-Member.Tests.ps1 @@ -165,13 +165,24 @@ Describe 'Find-Member cmdlet tests' { } It 'gets members for passed objects only once per type' { - $powershells = [powershell]::Create(), [powershell]::Create() - try { - $result = $powershells | Find-Member - } finally { - $powershells.ForEach('Dispose') - } + $type = compile ' + public static void StaticMethod() { } + + public static int StaticProperty { get; set; } + + public static int StaticField; + + public static event System.EventHandler StaticEvent; + + public void InstanceMethod() { } + + public int InstanceProperty { get; set; } + + public int InstanceField; + + public event System.EventHandler InstanceEvent;' - $result.Where{ $_.Name -eq 'Create' }.Count | Should -Be 3 + $instances = $type::new(), $type::new() + $instances | Find-Member | Where-Object Name -eq InstanceMethod | Should -HaveCount 1 } } From 0f4841c2c28916964d520b1d88b187b4934afc0e Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Fri, 19 Dec 2025 19:39:23 -0500 Subject: [PATCH 05/12] Fix artifact path casing --- ClassExplorer.build.ps1 | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ClassExplorer.build.ps1 b/ClassExplorer.build.ps1 index 596e120..4bd7014 100644 --- a/ClassExplorer.build.ps1 +++ b/ClassExplorer.build.ps1 @@ -51,6 +51,8 @@ function GetArtifactPath { $target = $legacyTarget } + # dotnet always uses lower case for the path now for some reason + $target = $target.ToLowerInvariant() if (-not $FileName) { return "./artifacts/publish/$moduleName/${config}_${target}" } @@ -140,10 +142,6 @@ task BuildDll { } task CopyToRelease { - $version = $script:ModuleVersion - $modern = $script:ModernTarget - $legacy = $script:LegacyTarget - $release = $script:ReleasePath if (-not (Test-Path -LiteralPath $release)) { $null = New-Item $release -ItemType Directory From b7b95276449f5b612e8497b8544d63366bce8fd9 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Fri, 19 Dec 2025 19:42:25 -0500 Subject: [PATCH 06/12] Fix the casing of the correct variable this time --- ClassExplorer.build.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ClassExplorer.build.ps1 b/ClassExplorer.build.ps1 index 4bd7014..30b4a01 100644 --- a/ClassExplorer.build.ps1 +++ b/ClassExplorer.build.ps1 @@ -52,7 +52,7 @@ function GetArtifactPath { } # dotnet always uses lower case for the path now for some reason - $target = $target.ToLowerInvariant() + $config = $config.ToLowerInvariant() if (-not $FileName) { return "./artifacts/publish/$moduleName/${config}_${target}" } From 69bf2090b7966ebd800ecdbcec235bc6d45578dd Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Sun, 21 Dec 2025 14:49:09 -0500 Subject: [PATCH 07/12] Add tests for Invoke-Member --- ClassExplorer.build.ps1 | 38 ++++++---- test/Invoke-Member.Tests.ps1 | 136 +++++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 14 deletions(-) create mode 100644 test/Invoke-Member.Tests.ps1 diff --git a/ClassExplorer.build.ps1 b/ClassExplorer.build.ps1 index 30b4a01..09c9450 100644 --- a/ClassExplorer.build.ps1 +++ b/ClassExplorer.build.ps1 @@ -99,7 +99,7 @@ task AssertDotNet { $script:dotnet = & $GetDotNet -Unix:(-not $script:_IsWindows) } -task AssertOpenCover -If { $GenerateCodeCoverage.IsPresent } { +task AssertOpenCover -If { $true } { if (-not $script:_IsWindows) { Write-Warning 'Generating code coverage from .NET core is currently unsupported, disabling code coverage generation.' $script:GenerateCodeCoverage = $false @@ -176,18 +176,27 @@ task CopyToRelease { } task DoTest -If { Test-Path ./test/*.ps1 } { - if (-not $script:_IsWindows) { - $scriptString = ' - $projectPath = "{0}" - Invoke-Pester "$projectPath" -OutputFormat NUnitXml -OutputFile "$projectPath\testresults\pester.xml" - ' -f $PSScriptRoot - } else { - $scriptString = ' - Set-ExecutionPolicy Bypass -Force -Scope Process - $projectPath = "{0}" - Invoke-Pester "$projectPath" -OutputFormat NUnitXml -OutputFile "$projectPath\testresults\pester.xml" - ' -f $PSScriptRoot - } + + $scriptString = @( + if (-not $script:_IsWindows) { + 'Set-ExecutionPolicy Bypass -Force -Scope Process' + '' + } + + '$projectPath = ''{0}''' -f $PSScriptRoot + '$config = New-PesterConfiguration @{' + ' Run = @{' + ' Path = $projectPath' + ' }' + ' TestResult = @{' + ' Enable = $true' + ' OutputFormat = ''NUnitXml''' + ' OutputPath = "$projectPath\testresults.pester.xml"' + ' }' + '}' + '' + 'Invoke-Pester -Configuration $config' + ) -join [Environment]::NewLine $encodedCommand = [convert]::ToBase64String( @@ -199,7 +208,8 @@ task DoTest -If { Test-Path ./test/*.ps1 } { $powershellCommand = 'pwsh' } - $powershell = Get-Command -CommandType Application $powershellCommand | Select-Object -First 1 -ExpandProperty Source + $powershell = Get-Command -CommandType Application $powershellCommand | + Select-Object -First 1 -ExpandProperty Source # Can't be bothered atm, gotta modernize this at some point. $GenerateCodeCoverage = $false diff --git a/test/Invoke-Member.Tests.ps1 b/test/Invoke-Member.Tests.ps1 new file mode 100644 index 0000000..4619a7c --- /dev/null +++ b/test/Invoke-Member.Tests.ps1 @@ -0,0 +1,136 @@ + +$moduleName = 'ClassExplorer' +$manifestPath = "$PSScriptRoot\..\Release\$moduleName" + +Import-Module $manifestPath +Import-Module $PSScriptRoot\shared.psm1 + +Describe 'Invoke-Member' { + Context 'Basics' { + Context 'Static' { + It 'property' { + $test = compile ' + public static int Member { get; set; }' + + $test | Find-Member Member | Invoke-Member 10 + $test | Find-Member Member | Invoke-Member | Should -Be 10 + } + + It 'field' { + $test = compile ' + public static int Member;' + + $test | Find-Member Member | Invoke-Member 10 + $test | Find-Member Member | Invoke-Member | Should -Be 10 + } + + It 'method' { + $test = compile ' + public static int Member() => 10;' + + $test | Find-Member Member | Invoke-Member | Should -Be 10 + } + + It 'method with args' { + $test = compile ' + public static int Member(int x, int y) => x + y;' + + $test | Find-Member Member | Invoke-Member 5 5 | Should -Be 10 + } + } + + Context 'Instance' { + It 'property' { + $test = compile ' + public int Member { get; set; }' + + $test = $test::new() + + $test | Find-Member Member | Invoke-Member 10 + $test | Find-Member Member | Invoke-Member | Should -Be 10 + } + + It 'field' { + $test = compile ' + public int Member;' + + $test = $test::new() + + $test | Find-Member Member | Invoke-Member 10 + $test | Find-Member Member | Invoke-Member | Should -Be 10 + } + + It 'method' { + $test = compile ' + public int Member() => 10;' + + $test = $test::new() + + $test | Find-Member Member | Invoke-Member | Should -Be 10 + } + + It 'method with args' { + $test = compile ' + public int Member(int x, int y) => x + y;' + + $test = $test::new() + + $test | Find-Member Member | Invoke-Member 5 5 | Should -Be 10 + } + } + } + + Context 'Ref handling' { + It 'emits as a property when out parameter not specified' { + $test = compile ' + public int Member(int value, out int outValue) + { + outValue = value + 10; + return value + 20; + }' + + $result = $test::new() | Find-Member Member | Invoke-Member 10 + $result.Result | Should -Be 30 + $result.outValue | Should -Be 20 + } + + # yesh that's a long test name + It 'emits as a property when out parameter not specified or specified as non-psref and writes to psref' { + $test = compile ' + public int Member(int value, out int outValue, out int outValue2, out int outValue3) + { + outValue = value + 11; + outValue2 = value + 12; + outValue3 = value + 13; + return value + 20; + }' + + $outValue2 = 0 + $result = $test::new() | Find-Member Member | Invoke-Member 10 0 ([ref] $outValue2) + $result.Result | Should -Be 30 + $result.outValue | Should -Be 21 + $result.psobject.Properties['outValue2'] | Should -BeNullOrEmpty + $outValue2 | Should -Be 22 + $result.outValue3 | Should -Be 23 + } + + It 'emits return value as is if all refs are specified as psrefs' { + $test = compile ' + public int Member(int value, out int outValue, out int outValue2, out int outValue3) + { + outValue = value + 11; + outValue2 = value + 12; + outValue3 = value + 13; + return value + 20; + }' + + $outValue1 = $outValue2 = $outValue3 = 0 + $result = $test::new() | Find-Member Member | Invoke-Member 10 ([ref] $outValue1) ([ref] $outValue2) ([ref] $outValue3) + $outValue1 | Should -Be 21 + $outValue2 | Should -Be 22 + $outValue3 | Should -Be 23 + $result | Should -Be 30 + $result | Should -BeOfType int + } + } +} From 9daa9555fce9df5d94973f6073a106b8cb12c13d Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Sun, 21 Dec 2025 14:52:50 -0500 Subject: [PATCH 08/12] Reverse the `-If` to skip open cover --- ClassExplorer.build.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ClassExplorer.build.ps1 b/ClassExplorer.build.ps1 index 09c9450..2901bfc 100644 --- a/ClassExplorer.build.ps1 +++ b/ClassExplorer.build.ps1 @@ -99,7 +99,7 @@ task AssertDotNet { $script:dotnet = & $GetDotNet -Unix:(-not $script:_IsWindows) } -task AssertOpenCover -If { $true } { +task AssertOpenCover -If { $false } { if (-not $script:_IsWindows) { Write-Warning 'Generating code coverage from .NET core is currently unsupported, disabling code coverage generation.' $script:GenerateCodeCoverage = $false From a2122db65fab81f4b7d771b64a2d31f874275ffc Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Sun, 21 Dec 2025 16:14:09 -0500 Subject: [PATCH 09/12] Add docs for `Invoke-Member` --- docs/en-US/ClassExplorer.md | 16 ++- docs/en-US/Find-Member.md | 1 + docs/en-US/Find-Type.md | 1 + docs/en-US/Format-MemberSignature.md | 1 + docs/en-US/Get-Assembly.md | 1 + docs/en-US/Get-Parameter.md | 1 + docs/en-US/Invoke-Member.md | 175 +++++++++++++++++++++++++++ 7 files changed, 191 insertions(+), 5 deletions(-) create mode 100644 docs/en-US/Invoke-Member.md diff --git a/docs/en-US/ClassExplorer.md b/docs/en-US/ClassExplorer.md index 653b4bd..c183cf9 100644 --- a/docs/en-US/ClassExplorer.md +++ b/docs/en-US/ClassExplorer.md @@ -22,20 +22,26 @@ Type signatures are a custom query language built into PowerShell type expressio ### [Find-Member](Find-Member.md) -The Find-Member cmdlet searches the AppDomain for members that fit specified criteria. You can search the entire AppDomain, search in specific types, or filter an existing list of members. +The `Find-Member` cmdlet searches the AppDomain for members that fit specified criteria. You can search the entire AppDomain, search in specific types, or filter an existing list of members. ### [Find-Type](Find-Type.md) -The Find-Type cmdlet searches the AppDomain for .NET classes that match specified criteria. +The `Find-Type` cmdlet searches the AppDomain for .NET classes that match specified criteria. ### [Get-Assembly](Get-Assembly.md) -The Get-Assembly cmdlet gets assemblies loaded in the AppDomain. +The `Get-Assembly` cmdlet gets assemblies loaded in the AppDomain. ### [Get-Parameter](Get-Parameter.md) -The Get-Parameter cmdlet gets parameter info from a member. +The `Get-Parameter` cmdlet gets parameter info from a member. ### [Format-MemberSignature](Format-MemberSignature.md) -The Format-MemberSignature cmdlet uses the input reflection objects to generate reference library style C# pseudo code. Use this cmdlet to get a more in depth look at specific member including attribute decorations, generic type constraints, and more. +The `Format-MemberSignature` cmdlet uses the input reflection objects to generate reference library style C# pseudo code. Use this cmdlet to get a more in depth look at specific member including attribute decorations, generic type constraints, and more. + +### [Invoke-Member](Invoke-Member.md) + +The `Invoke-Member` cmdlet takes a reflection info (`System.Reflection.MemberInfo`) object and +facilitates seamless invocation in a pipeline. `Invoke-Member` will handle any necessary +conversions, unwrapping of psobjects, and streamlined `ref` handling for interactive use. diff --git a/docs/en-US/Find-Member.md b/docs/en-US/Find-Member.md index 854aad9..19ad4ec 100644 --- a/docs/en-US/Find-Member.md +++ b/docs/en-US/Find-Member.md @@ -677,3 +677,4 @@ Matched MemberInfo objects will be returned to the pipeline. [Get-Assembly](Get-Assembly.md) [Get-Parameter](Get-Parameter.md) [Format-MemberSignature](Format-MemberSignature.md) +[Invoke-Member](Invoke-Member.md) diff --git a/docs/en-US/Find-Type.md b/docs/en-US/Find-Type.md index cc72a9f..730a903 100644 --- a/docs/en-US/Find-Type.md +++ b/docs/en-US/Find-Type.md @@ -447,3 +447,4 @@ Matched Type objected will be returned to the pipeline. [Get-Assembly](Get-Assembly.md) [Get-Parameter](Get-Parameter.md) [Format-MemberSignature](Format-MemberSignature.md) +[Invoke-Member](Invoke-Member.md) diff --git a/docs/en-US/Format-MemberSignature.md b/docs/en-US/Format-MemberSignature.md index 792cf7d..4d3d5fb 100644 --- a/docs/en-US/Format-MemberSignature.md +++ b/docs/en-US/Format-MemberSignature.md @@ -212,3 +212,4 @@ The formatted display string will be returned to the pipeline. [Find-Member](Find-Member.md) [Get-Assembly](Get-Assembly.md) [Get-Parameter](Get-Parameter.md) +[Invoke-Member](Invoke-Member.md) diff --git a/docs/en-US/Get-Assembly.md b/docs/en-US/Get-Assembly.md index 2d4e82b..6326927 100644 --- a/docs/en-US/Get-Assembly.md +++ b/docs/en-US/Get-Assembly.md @@ -85,3 +85,4 @@ Matched Assembly objects will be returned to the pipeline. [Find-Member](Find-Member.md) [Get-Parameter](Get-Parameter.md) [Format-MemberSignature](Format-MemberSignature.md) +[Invoke-Member](Invoke-Member.md) diff --git a/docs/en-US/Get-Parameter.md b/docs/en-US/Get-Parameter.md index 26e840a..b98f1e7 100644 --- a/docs/en-US/Get-Parameter.md +++ b/docs/en-US/Get-Parameter.md @@ -86,3 +86,4 @@ Matched parameters will be returned to the pipeline. [Find-Member](Find-Member.md) [Get-Assembly](Get-Assembly.md) [Format-MemberSignature](Format-MemberSignature.md) +[Invoke-Member](Invoke-Member.md) diff --git a/docs/en-US/Invoke-Member.md b/docs/en-US/Invoke-Member.md new file mode 100644 index 0000000..f990e0a --- /dev/null +++ b/docs/en-US/Invoke-Member.md @@ -0,0 +1,175 @@ +--- +external help file: ClassExplorer.dll-Help.xml +online version: https://github.com/SeeminglyScience/ClassExplorer/blob/master/docs/en-US/Invoke-Member.md +schema: 2.0.0 +--- + +# Invoke-Member + +## SYNOPSIS +Invokes a member specified as reflection info. + +## SYNTAX + +``` +Invoke-Member -InputObject [-Instance ] [-ArgumentList ] [-SkipPSObjectUnwrap] + [] +``` + +## DESCRIPTION +The `Invoke-Member` cmdlet takes a reflection info (`System.Reflection.MemberInfo`) object and +facilitates seamless invocation in a pipeline. `Invoke-Member` will handle any necessary +conversions, unwrapping of psobjects, and streamlined `ref` handling for interactive use. + +## EXAMPLES + +### -------------------------- EXAMPLE 1 -------------------------- + +```powershell +$ExecutionContext | Find-Member -IncludeNonPublic _context | Invoke-Member + +# System.Management.Automation.ExecutionContext +``` + +Gets the value of the private field `_context` from the `$ExecutionContext` +variable. `Find-Member` passes the source object as a hidden property (omitted +from the psobject member resurrection table) to allow `Invoke-Member` to work +without an intermediate variable. + +### -------------------------- EXAMPLE 2 -------------------------- + +```powershell +[DateTimeOffset]::Now | Find-Member Deconstruct | Invoke-Member + +# date time offset +# ---- ---- ------ +# 12/21/2025 3:36 PM -05:00:00 +``` + +The method `DateTimeOffset.Deconstruct` has the following signature: + +`public void Deconstruct(out DateOnly date, out TimeOnly time, out TimeSpan offset);` + +When invoked via `Invoke-Member`, a note property is created for each `out` parameter +that is not specified as a `[ref]`. If the declared return type for the method was +not void, a fourth property would be created with the name `Result` and the value +of that property would be the return value. + +### -------------------------- EXAMPLE 3 -------------------------- + +```powershell +$dict = [System.Collections.Generic.Dictionary[string, int]]::new() +$dict['mytest'] = 10 +$myValue = 0 +$dict | Find-Member TryGetValue | Invoke-Member mytest ([ref] $myValue) +# True + +$myValue +# 10 +``` + +Invoking a method with `out` parameters will emit the return value as is if +all `out` parameters are specified with `[ref]` values. + +## PARAMETERS + +### -ArgumentList +Specifies the arguments that should be passed during the invocation of the +member. This can be a value if attempting to set a field or property value, or +arguments to pass to a method. + +Typically passed as `ValueFromRemainingArguments` but can be specified directly +if desired. + +```yaml +Type: Object[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -InputObject +Specifies the member to invoke, ideally as output from the `Find-Member` command, +but not strictly required. + +```yaml +Type: MemberInfo +Parameter Sets: (All) +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: False +``` + +### -Instance +Specifies the instance to pass during the invocation of the member. If using +the `Find-Member` command you will not need to specify this directly. + +```yaml +Type: Object +Parameter Sets: (All) +Aliases: __ce_Instance + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -SkipPSObjectUnwrap +PowerShell will invisibly wrap objects in a `PSObject` instance to facilitate +certain operations like member/parameter binding, ETS member storage, etc. The +.NET type system does not understand `PSObject` and will generally throw stating +the type does not match it's expectation. + +The `Invoke-Member` command will strip this wrapper from specified arguments +automatically by default before attempting invocation. In a small handful of +cases this behavior may not be desired, and can be disabled with this parameter. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### System.Reflection.MemberInfo + +You can pipe reflection members to this command, ideally via an upstream invocation +of the `Find-Member` command. + +## OUTPUTS + +### System.Object + +The raw return value (or aggregate PSObject when `out` parameters are involved) will +be emitted to the pipeline. + +## NOTES + +## RELATED LINKS + +[Find-Type](Find-Type.md) +[Find-Member](Find-Member.md) +[Get-Assembly](Get-Assembly.md) +[Get-Parameter](Get-Parameter.md) +[Format-MemberSignature](Format-MemberSignature.md) From 62267aedb786c83f89de10ea006892e9f866f361 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Mon, 22 Dec 2025 14:44:40 -0500 Subject: [PATCH 10/12] Fix up galc and add some comments --- src/ClassExplorer/ALCPoly.cs | 4 +-- .../Commands/GetAssemblyCommand.cs | 7 ++++++ .../Commands/GetAssemblyLoadContextCommand.cs | 8 +++++- .../Commands/InvokeMemberCommand.cs | 23 ++++++++++++++--- src/ClassExplorer/Internal/_Format.cs | 13 ++++++++++ src/ClassExplorer/PipelineEmitter.cs | 25 +++++++++++-------- src/ClassExplorer/SR.resx | 3 +++ src/ClassExplorer/Signatures/RefSignature.cs | 4 +-- .../Signatures/SignatureExtensions.cs | 5 ++++ 9 files changed, 73 insertions(+), 19 deletions(-) diff --git a/src/ClassExplorer/ALCPoly.cs b/src/ClassExplorer/ALCPoly.cs index bf7119b..8fdf74d 100644 --- a/src/ClassExplorer/ALCPoly.cs +++ b/src/ClassExplorer/ALCPoly.cs @@ -80,7 +80,7 @@ public static string SafeGetName(AssemblyLoadContext alc) { } - return name ?? alc.GetType().FullName; + return name ?? alc.GetType().FullName ?? ""; } public static void AssertSupported(PSCmdlet cmdlet) @@ -104,7 +104,7 @@ public static IEnumerable GetAll() #endif } - public static AssemblyLoadContext GetLoadContext(Assembly assembly) + public static AssemblyLoadContext? GetLoadContext(Assembly assembly) { #if NETFRAMEWORK return AssemblyLoadContext.Default; diff --git a/src/ClassExplorer/Commands/GetAssemblyCommand.cs b/src/ClassExplorer/Commands/GetAssemblyCommand.cs index 52dc9b9..cdaba08 100644 --- a/src/ClassExplorer/Commands/GetAssemblyCommand.cs +++ b/src/ClassExplorer/Commands/GetAssemblyCommand.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Management.Automation; using System.Reflection; +using System.Runtime.Loader; namespace ClassExplorer.Commands { @@ -94,6 +95,12 @@ protected override void ProcessRecord() return; } + if (InputObject.BaseObject is AssemblyLoadContext alc) + { + WriteObject(ALC.SafeGetAssemblies(alc), enumerateCollection: true); + return; + } + WriteObject(InputObject.BaseObject.GetType().Assembly, enumerateCollection: false); } } diff --git a/src/ClassExplorer/Commands/GetAssemblyLoadContextCommand.cs b/src/ClassExplorer/Commands/GetAssemblyLoadContextCommand.cs index f80a28b..283f6da 100644 --- a/src/ClassExplorer/Commands/GetAssemblyLoadContextCommand.cs +++ b/src/ClassExplorer/Commands/GetAssemblyLoadContextCommand.cs @@ -7,6 +7,7 @@ namespace ClassExplorer.Commands; [Cmdlet(VerbsCommon.Get, "AssemblyLoadContext")] [Alias("galc")] +[OutputType(typeof(AssemblyLoadContext))] public sealed class GetAssemblyLoadContextCommand : PSCmdlet { [Parameter(Position = 0)] @@ -49,7 +50,12 @@ protected override void ProcessRecord() return; } - AssemblyLoadContext alc = ALC.GetLoadContext(assembly); + AssemblyLoadContext? alc = ALC.GetLoadContext(assembly); + if (alc is null) + { + return; + } + if (!(_processedAlcs ??= new()).Add(alc)) { return; diff --git a/src/ClassExplorer/Commands/InvokeMemberCommand.cs b/src/ClassExplorer/Commands/InvokeMemberCommand.cs index 7f54fb6..4bef5ab 100644 --- a/src/ClassExplorer/Commands/InvokeMemberCommand.cs +++ b/src/ClassExplorer/Commands/InvokeMemberCommand.cs @@ -6,7 +6,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using ClassExplorer.Internal; - +using ClassExplorer.Signatures; using PSAllowNull = System.Management.Automation.AllowNullAttribute; namespace ClassExplorer.Commands; @@ -33,6 +33,19 @@ public sealed class InvokeMemberCommand : PSCmdlet [Parameter] public SwitchParameter SkipPSObjectUnwrap { get; set; } + protected override void BeginProcessing() + { + if (SessionState.LanguageMode is not PSLanguageMode.FullLanguage) + { + ThrowTerminatingError( + new ErrorRecord( + new PSInvalidOperationException(SR.InvalidLanguageMode), + nameof(SR.InvalidLanguageMode), + ErrorCategory.InvalidOperation, + targetObject: null)); + } + } + protected override unsafe void ProcessRecord() { Poly.Assert(InputObject is not null); @@ -82,8 +95,6 @@ protected override unsafe void ProcessRecord() } } - - if (InputObject is not MethodBase methodBase) { ThrowTerminatingError( @@ -166,11 +177,17 @@ protected override unsafe void ProcessRecord() return; } + // Go through refs and remove any that link to a `PSReference` or are for a `in` parameter. for (int i = refs.Count - 1; i >= 0; i--) { RefInfo info = refs[i]; if (info.PSRef is null) { + if (info.Parameter?.IsDecoratedReadOnly() ?? false) + { + refs.RemoveAt(i); + } + continue; } diff --git a/src/ClassExplorer/Internal/_Format.cs b/src/ClassExplorer/Internal/_Format.cs index 90ec9d0..20f16ad 100644 --- a/src/ClassExplorer/Internal/_Format.cs +++ b/src/ClassExplorer/Internal/_Format.cs @@ -61,6 +61,19 @@ public static string FullType(Type value, int maxLength = -1) .ToString(); } + [Hidden, EditorBrowsable(EditorBrowsableState.Never)] + public static string FullType(string value, int maxLength = -1) + { + SignatureWriter w = GetWriter(maxLength); + int lastPeriod = value.LastIndexOf('.'); + if (lastPeriod is -1 || lastPeriod == value.Length - 1) + { + return w.TypeInfo(value).ToString(); + } + + return w.Namespace(value[..lastPeriod]).Dot().TypeInfo(value[(lastPeriod + 1)..]).ToString(); + } + [Hidden, EditorBrowsable(EditorBrowsableState.Never)] public static string Namespace(string value, int maxLength = -1) { diff --git a/src/ClassExplorer/PipelineEmitter.cs b/src/ClassExplorer/PipelineEmitter.cs index 5374399..8ed3e8f 100644 --- a/src/ClassExplorer/PipelineEmitter.cs +++ b/src/ClassExplorer/PipelineEmitter.cs @@ -51,22 +51,25 @@ static PipelineEmitter() public PipelineEmitter(PSCmdlet cmdlet) => _cmdlet = cmdlet; - public void Invoke(T value) => _cmdlet.WriteObject(value, enumerateCollection: false); - - public void Invoke(T value, object? instance) + private static PSObject AsPSObject(object obj, bool storeTypeNameAndInstanceMembersLocally) { - PSObject pso; - if (s_asPSObject is null || s_setHidden is null) + if (s_asPSObject is null) { - pso = PSObject.AsPSObject(value); - pso.Properties.Add(new PSNoteProperty("__ce_Instance", instance)); - _cmdlet.WriteObject(pso, enumerateCollection: false); - return; + return PSObject.AsPSObject(obj); } - pso = s_asPSObject(value, true); + return s_asPSObject(obj, storeTypeNameAndInstanceMembersLocally); + } + + public void Invoke(T value) => _cmdlet.WriteObject(value, enumerateCollection: false); + + public void Invoke(T value, object? instance) + { + // Avoid saving to the PSObject member resurrection table if possible as + // .NET caches `MemberInfo` objects. + PSObject pso = AsPSObject(value!, storeTypeNameAndInstanceMembersLocally: true); PSNoteProperty instanceProp = new("__ce_Instance", instance); - s_setHidden(instanceProp); + s_setHidden?.Invoke(instanceProp); pso.Properties.Add(instanceProp); _cmdlet.WriteObject(pso, enumerateCollection: false); } diff --git a/src/ClassExplorer/SR.resx b/src/ClassExplorer/SR.resx index a34212c..ed5ea0f 100644 --- a/src/ClassExplorer/SR.resx +++ b/src/ClassExplorer/SR.resx @@ -191,4 +191,7 @@ Message: {0} Expected signature of kind "{0}" but found a "{1}" signature. + + The 'Invoke-Member' command cannot be used outside of 'PSLanguageMode.FullLanguage'. + diff --git a/src/ClassExplorer/Signatures/RefSignature.cs b/src/ClassExplorer/Signatures/RefSignature.cs index d4626b8..c8969d1 100644 --- a/src/ClassExplorer/Signatures/RefSignature.cs +++ b/src/ClassExplorer/Signatures/RefSignature.cs @@ -33,14 +33,14 @@ private static bool DoesRefKindMatch(RefKind kind, ParameterInfo parameter) return kind is RefKind.Out; } - if (parameter.IsDefined("System.Runtime.CompilerServices.IsReadOnlyAttribute")) + if (parameter.IsDecoratedReadOnly()) { return kind is RefKind.In; } if (parameter.Position is -1) { - if (parameter.IsDefined("System.Runtime.CompilerServices.IsReadOnlyAttribute")) + if (parameter.IsDecoratedReadOnly()) { return kind is RefKind.In; } diff --git a/src/ClassExplorer/Signatures/SignatureExtensions.cs b/src/ClassExplorer/Signatures/SignatureExtensions.cs index 67a20fe..5aa207c 100644 --- a/src/ClassExplorer/Signatures/SignatureExtensions.cs +++ b/src/ClassExplorer/Signatures/SignatureExtensions.cs @@ -47,6 +47,11 @@ public static bool IsDefined(this MemberInfo member, string fullName) return member.IsDefined(type, true); } + public static bool IsDecoratedReadOnly(this ParameterInfo parameter) + { + return parameter.IsDefined("System.Runtime.CompilerServices.IsReadOnlyAttribute"); + } + public static bool IsDefined(this ParameterInfo parameter, string fullName) { Type? type = parameter.Member.Module.GetType(fullName) ?? Type.GetType(fullName); From edcbc54170c0055b61a82c4f8a4a05b4855efc93 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Mon, 22 Dec 2025 14:45:19 -0500 Subject: [PATCH 11/12] Add galc formatting --- module/ClassExplorer.format.ps1xml | 63 ++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/module/ClassExplorer.format.ps1xml b/module/ClassExplorer.format.ps1xml index 750a6ec..6e591a5 100644 --- a/module/ClassExplorer.format.ps1xml +++ b/module/ClassExplorer.format.ps1xml @@ -538,5 +538,68 @@ return '' + + System.Runtime.Loader.AssemblyLoadContext + + System.Runtime.Loader.AssemblyLoadContext + + + + + + 15 + + + + 45 + + + + + + + + + + + if ($_.Name) { + return [ClassExplorer.Internal._Format]::MemberName($_.Name) + } + + return [ClassExplorer.Internal._Format]::MemberName('<Unnamed>') + + + + + [ClassExplorer.Internal._Format]::FullType($_.GetType()) + + + + + $assemblies = @($_.Assemblies) + if ($assemblies.Length -eq 0) { + return '{0} assemblies' -f [ClassExplorer.Internal._Format]::Number(0) + } + + if ($assemblies.Length -eq 1) { + return '{0} assembly ({1})' -f ( + [ClassExplorer.Internal._Format]::Number(1), + [ClassExplorer.Internal._Format]::FullType($assemblies[0].GetName().Name)) + } + + $names = foreach ($assembly in $assemblies) { + [ClassExplorer.Internal._Format]::FullType($assembly.GetName().Name) + } + + return '{0} assemblies ({1})' -f ( + [ClassExplorer.Internal._Format]::Number($assemblies.Length), + ($names -join ', ')) + + + + + + + From ef5e3d2b7fa215ddd47077f61ca431be2c0c3840 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Mon, 22 Dec 2025 14:52:19 -0500 Subject: [PATCH 12/12] Add docs for galc --- docs/en-US/ClassExplorer.md | 7 ++ docs/en-US/Find-Member.md | 1 + docs/en-US/Find-Type.md | 1 + docs/en-US/Format-MemberSignature.md | 1 + docs/en-US/Get-Assembly.md | 1 + docs/en-US/Get-AssemblyLoadContext.md | 170 ++++++++++++++++++++++++++ docs/en-US/Get-Parameter.md | 1 + docs/en-US/Invoke-Member.md | 1 + 8 files changed, 183 insertions(+) create mode 100644 docs/en-US/Get-AssemblyLoadContext.md diff --git a/docs/en-US/ClassExplorer.md b/docs/en-US/ClassExplorer.md index c183cf9..bbe5dca 100644 --- a/docs/en-US/ClassExplorer.md +++ b/docs/en-US/ClassExplorer.md @@ -45,3 +45,10 @@ The `Format-MemberSignature` cmdlet uses the input reflection objects to generat The `Invoke-Member` cmdlet takes a reflection info (`System.Reflection.MemberInfo`) object and facilitates seamless invocation in a pipeline. `Invoke-Member` will handle any necessary conversions, unwrapping of psobjects, and streamlined `ref` handling for interactive use. + +### [Get-AssemblyLoadContext](Get-AssemblyLoadContext.md) + +The `Get-AssemblyLoadContext` cmdlet gets all currently active assembly load +contexts (ALCs), or the relevant ALCs if any parameters are specified. + +This command is only supported in PowerShell 7+ diff --git a/docs/en-US/Find-Member.md b/docs/en-US/Find-Member.md index 19ad4ec..727acee 100644 --- a/docs/en-US/Find-Member.md +++ b/docs/en-US/Find-Member.md @@ -678,3 +678,4 @@ Matched MemberInfo objects will be returned to the pipeline. [Get-Parameter](Get-Parameter.md) [Format-MemberSignature](Format-MemberSignature.md) [Invoke-Member](Invoke-Member.md) +[Get-AssemblyLoadContext](Get-AssemblyLoadContext.md) diff --git a/docs/en-US/Find-Type.md b/docs/en-US/Find-Type.md index 730a903..a94dac7 100644 --- a/docs/en-US/Find-Type.md +++ b/docs/en-US/Find-Type.md @@ -448,3 +448,4 @@ Matched Type objected will be returned to the pipeline. [Get-Parameter](Get-Parameter.md) [Format-MemberSignature](Format-MemberSignature.md) [Invoke-Member](Invoke-Member.md) +[Get-AssemblyLoadContext](Get-AssemblyLoadContext.md) diff --git a/docs/en-US/Format-MemberSignature.md b/docs/en-US/Format-MemberSignature.md index 4d3d5fb..51ad4db 100644 --- a/docs/en-US/Format-MemberSignature.md +++ b/docs/en-US/Format-MemberSignature.md @@ -213,3 +213,4 @@ The formatted display string will be returned to the pipeline. [Get-Assembly](Get-Assembly.md) [Get-Parameter](Get-Parameter.md) [Invoke-Member](Invoke-Member.md) +[Get-AssemblyLoadContext](Get-AssemblyLoadContext.md) diff --git a/docs/en-US/Get-Assembly.md b/docs/en-US/Get-Assembly.md index 6326927..048d48b 100644 --- a/docs/en-US/Get-Assembly.md +++ b/docs/en-US/Get-Assembly.md @@ -86,3 +86,4 @@ Matched Assembly objects will be returned to the pipeline. [Get-Parameter](Get-Parameter.md) [Format-MemberSignature](Format-MemberSignature.md) [Invoke-Member](Invoke-Member.md) +[Get-AssemblyLoadContext](Get-AssemblyLoadContext.md) diff --git a/docs/en-US/Get-AssemblyLoadContext.md b/docs/en-US/Get-AssemblyLoadContext.md new file mode 100644 index 0000000..820bee4 --- /dev/null +++ b/docs/en-US/Get-AssemblyLoadContext.md @@ -0,0 +1,170 @@ +--- +external help file: ClassExplorer.dll-Help.xml +online version: https://github.com/SeeminglyScience/ClassExplorer/blob/master/docs/en-US/Get-AssemblyLoadContext.md +schema: 2.0.0 +--- + +# Get-AssemblyLoadContext + +## SYNOPSIS +Gets all loaded assembly load contexts. + +## SYNTAX + +``` +Get-AssemblyLoadContext [[-Name] ] [-Default] [-InputObject ] + [] +``` + +## DESCRIPTION +The `Get-AssemblyLoadContext` cmdlet gets all currently active assembly load +contexts (ALCs), or the relevant ALCs if any parameters are specified. + +This command is only supported in PowerShell 7+ + +## EXAMPLES + +### -------------------------- EXAMPLE 1 -------------------------- +```powershell +Get-AssemblyLoadContext + +# Definition ImplementingType Assemblies +# ---------- ---------------- ---------- +# Default System.Runtime.Loader.DefaultAssemblyLoadCon… 140 assemblies (… +# PowerShellRun.CustomAssemblyLoadContext 0 assemblies +# Yayaml Yayaml.LoadContext 1 assembly (Yaya… +``` + +Gets all active assembly load contexts. + +### -------------------------- EXAMPLE 2 -------------------------- +```powershell +Get-AssemblyLoadContext -Default + +# Definition ImplementingType Assemblies +# ---------- ---------------- ---------- +# Default System.Runtime.Loader.DefaultAssemblyLoadCon… 140 assemblies (… +``` + +Gets only the default ALC. + +### -------------------------- EXAMPLE 3 -------------------------- +```powershell +Get-AssemblyLoadContext *yam* + +# Definition ImplementingType Assemblies +# ---------- ---------------- ---------- +# Yayaml Yayaml.LoadContext 1 assembly (Yaya… +``` + +Gets specifically the ALCs whose name match the wildcard pattern. + +### -------------------------- EXAMPLE 4 -------------------------- +```powershell +Find-Type ConvertToYamlCommand | Get-AssemblyLoadContext + +# Definition ImplementingType Assemblies +# ---------- ---------------- ---------- +# Yayaml Yayaml.LoadContext 1 assembly (Yaya… +``` + +Gets the ALC associated with the type passed as pipeline input. + +### -------------------------- EXAMPLE 5 -------------------------- +```powershell +Get-AssemblyLoadContext Yayaml | Find-Type + +# Namespace: Yayaml.Module +# +# Access Modifiers Name +# ------ --------- ---- +# public sealed class AddYamlFormatCommand : PSCmdlet +# public sealed class ConvertFromYamlCommand : PSCmdlet +# public sealed class ConvertToYamlCommand : PSCmdlet +# public sealed class NewYamlSchemaCommand : PSCmdlet +# public class YamlSchemaCompletionsAttribute : ArgumentCom… +# … +``` + +Uses the piped ALC as a search base for `Find-Type`. You can also use `Get-Assembly` +this way. `Find-Member` however, will return the members for the ALC type itself. +If you want to use the ALC as a search base, pipe to `Find-Type` first. + +## PARAMETERS + +### -Default +Specifies that the default assembly load context should be emitted. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -InputObject +Specifies a reflection object that should be queried for association with an +assembly load context. If the object has such an association, the ALC will be +emitted to the pipeline as output. + +```yaml +Type: PSObject +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: False +``` + +### -Name +Specifies the name (or wildcard pattern) of the assembly load context that should +be emitted to the pipeline as output. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 0 +Default value: None +Accept pipeline input: False +Accept wildcard characters: True +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### System.Reflection.Assembly, System.Type, PSObject + +If you pass reflection info objects (`Assembly`, `Type`, `MemberInfo`) to this cmdlet it will return the associated assembly load context (if applicable) + +If you pass any other object to this cmdlet, it will return the assembly load context associated with its type (if applicable) + + +## OUTPUTS + +### System.Runtime.Loader.AssemblyLoadContext + +Matched assembly load contexts will be emitted to the pipeline as output. + +## NOTES + +## RELATED LINKS + +[Get-Assembly](Get-Assembly.md) +[Find-Type](Find-Type.md) +[Find-Member](Find-Member.md) +[Get-Parameter](Get-Parameter.md) +[Format-MemberSignature](Format-MemberSignature.md) +[Invoke-Member](Invoke-Member.md) diff --git a/docs/en-US/Get-Parameter.md b/docs/en-US/Get-Parameter.md index b98f1e7..09faaf4 100644 --- a/docs/en-US/Get-Parameter.md +++ b/docs/en-US/Get-Parameter.md @@ -87,3 +87,4 @@ Matched parameters will be returned to the pipeline. [Get-Assembly](Get-Assembly.md) [Format-MemberSignature](Format-MemberSignature.md) [Invoke-Member](Invoke-Member.md) +[Get-AssemblyLoadContext](Get-AssemblyLoadContext.md) diff --git a/docs/en-US/Invoke-Member.md b/docs/en-US/Invoke-Member.md index f990e0a..404f1fd 100644 --- a/docs/en-US/Invoke-Member.md +++ b/docs/en-US/Invoke-Member.md @@ -173,3 +173,4 @@ be emitted to the pipeline. [Get-Assembly](Get-Assembly.md) [Get-Parameter](Get-Parameter.md) [Format-MemberSignature](Format-MemberSignature.md) +[Get-AssemblyLoadContext](Get-AssemblyLoadContext.md)