Source/Private/Use-ClassAccessors.ps1
<#
.SYNOPSIS Implements class getter and setter accessors. .DESCRIPTION The [Use-ClassAccessors][1] cmdlet updates script property of a class from the getter and setter methods. Which are also known as [accessors or mutator methods][2]. The getter and setter methods should use the following syntax: ### getter syntax [<type>] get_<property name>() { return <variable> } ### setter syntax set_<property name>(<variable>) { <code> } > [!NOTE] > A **setter** accessor requires a **getter** accessor to implement the related property. > [!NOTE] > In most cases, you might want to hide the getter and setter methods using the [`hidden` keyword][3] > on the getter and setter methods. .EXAMPLE # Using class accessors The following example defines a getter and setter for a `value` property and a _readonly_ property for the type of the type of the contained value. Class ExampleClass { hidden $_Value hidden [Object] get_Value() { return $this._Value } hidden set_Value($Value) { $this._Value = $Value } hidden [Type]get_Type() { if ($Null -eq $this.Value) { return $Null } else { return $this._Value.GetType() } } } .\Use-ClassAccessors.ps1 # -Force $Example = [ExampleClass]::new() $Example.Value = 42 # Set value to 42 $Example.Value # Returns 42 $Example.Type # Returns [Int] type info $Example.Type = 'Something' # Throws readonly error .PARAMETER ClassName Specifies the name of the class that contain the accessors. Default: All class in the (current) script (see also: [Script] parameter) .PARAMETER PropertyName Specifies the property name to update with the accessors. Default: All properties in the given class or classes (see also: [ClassName] parameter) .PARAMETER Script Specifies the script (block or path) that contains the class source. Default: the script where this command is invoked .PARAMETER Force Indicates that the cmdlet reloads the specified accessors, even if the accessors already have been defined for the concerned class. .LINK [1]: https://github.com/iRon7/Use-ClassAccessors "Online Help" [2]: https://en.wikipedia.org/wiki/Mutator_method "Mutator method" [3]: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_classes#hidden-keyword "Hidden keyword in classes" #> using namespace System.Management.Automation using namespace System.Management.Automation.Language function Use-ClassAccessors { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('InjectionRisk.Create', '', Justification = 'script blocks are created from class methods')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'false positives')] [OutputType([System.Void])] [CmdletBinding()] param( [Parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] [string[]]$ClassName, [Parameter(ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] [string]$PropertyName, [ValidateNotNullOrEmpty()] $Script, [switch]$Force ) begin { function StopError($Exception, $Id = 'IncorrectArgument', [ErrorCategory]$Group = [ErrorCategory]::SyntaxError, $Object){ if ($Exception -is [ErrorRecord]) { $Exception = $Exception.Exception } elseif ($Exception -isnot [Exception]) { $Exception = [ArgumentException]$Exception } $PSCmdlet.ThrowTerminatingError([ErrorRecord]::new($Exception, $Id, $Group, $Object)) } } process { $Callers = Get-PSCallStack | Select-Object -Skip 1 if (-Not $Script) { $Script = $Callers.InvocationInfo.MyCommand.where({ $_.CommandType -eq 'ExternalScript' }, 'first').ScriptBlock } if ($Script -is [ScriptBlock]) { $Ast = [System.Management.Automation.Language.Parser]::ParseInput($Script, [ref]$Null, [ref]$Null) } elseif ($Script) { $PathInfo = Resolve-Path $Script -ErrorAction SilentlyContinue if (-Not $PathInfo) { StopError "Cannot find path '$Script' because it does not exist." } $Errors = $Null $Ast = [Parser]::ParseFile($PathInfo.Path, [ref]$Null, [ref]$Errors) if ($Errors) { StopError $Errors[0].Message } } else { StopError 'This Cmdlet should be called from within the script that contains the concerned classes or the script parameter should be provided.' } foreach ($Class in $Ast.EndBlock.Statements.where{ $_.IsClass -and (-not $ClassName -or $_.Name -in $ClassName) }) { $PropertyAccessors = @{} $Accessors = $Class.Members.where{ $_ -is [FunctionMemberAst] -and $_.IsPublic -and -not $_.IsConstructor -and -not $_.IsStatic -and -Not $PropertyName -or $_.Name -like '?et_$Property$Name' } foreach ($Accessor in $Accessors) { if ($Accessor.Name -like 'get_*') { if ($Accessor.Parameters.Count -eq 0) { $MemberName = $Accessor.Name.SubString(4) $ReturnType = $Accessor.ReturnType.TypeName.Name -as [Type] if ($Null -eq $ReturnType -or $ReturnType.FullName -eq 'System.Object') { $Expression = $Accessor.Body.EndBlock.Extent.Text } else { $Expression = ",[$ReturnType](& { $($Accessor.Body.EndBlock.Extent.Text) })" } if (-not $PropertyAccessors.Contains($MemberName)) { $PropertyAccessors[$MemberName] = @{} } $PropertyAccessors[$MemberName].Value = [ScriptBlock]::Create($Expression) } else { Write-Warning "The method '$($Accessor.Name)' is skipped as it is not parameter-less." } } if ($Accessor.Name -like 'set_*') { if ($Accessor.Parameters.Count -eq 1) { $MemberName = $Accessor.Name.SubString(4) $Expression = "param($($Accessor.Parameters[0].Extent.Text)) $($Accessor.Body.EndBlock.Extent.Text)" if (-not $PropertyAccessors.Contains($MemberName)) { $PropertyAccessors[$MemberName] = @{} } $PropertyAccessors[$MemberName].SecondValue = [ScriptBlock]::Create($Expression) } else { Write-Warning "The method '$($Accessor.Name)' is skipped as it does not have a single parameter" } } } foreach ($MemberName in $PropertyAccessors.get_Keys()) { $TypeData = $PropertyAccessors[$MemberName] if ($TypeData.Contains('Value')) { $TypeData.TypeName = $Class.Name $TypeData.MemberType = 'ScriptProperty' $TypeData.MemberName = $MemberName $TypeData.Force = $Force Update-TypeData @TypeData } else { Write-Warning "A 'set_$MemberName()' accessor requires a 'get_$MemberName()' accessor." } } } } } |