Skip to content

Commit 532c4c2

Browse files
committed
WIP: Modern .NET calls them Generators
1 parent 8296a5b commit 532c4c2

9 files changed

Lines changed: 307 additions & 19 deletions
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
using namespace System.Management.Automation.Language
2+
using namespace System.Collections.Generic
3+
4+
5+
6+
7+
8+
# There should be an abstract class for ModuleBuilderGenerator that has a contract for this:
9+
10+
11+
# Should be called on a block to extract the (first) parameters from that block
12+
class ParameterExtractor : AstVisitor {
13+
[ParameterPosition[]]$Parameters = @()
14+
[int]$InsertLineNumber = -1
15+
[int]$InsertColumnNumber = -1
16+
[int]$InsertOffset = -1
17+
18+
ParameterExtractor([Ast]$Ast) {
19+
$ast.Visit($this)
20+
}
21+
22+
[AstVisitAction] VisitParamBlock([ParamBlockAst]$ast) {
23+
if ($Ast.Parameters) {
24+
$Text = $ast.Extent.Text -split "\r?\n"
25+
26+
$FirstLine = $ast.Extent.StartLineNumber
27+
$NextLine = 1
28+
$this.Parameters = @(
29+
foreach ($parameter in $ast.Parameters | Select-Object Name -Expand Extent) {
30+
[ParameterPosition]@{
31+
Name = $parameter.Name
32+
StartOffset = $parameter.StartOffset
33+
Text = if (($parameter.StartLineNumber - $FirstLine) -ge $NextLine) {
34+
Write-Debug "Extracted parameter $($Parameter.Name) with surrounding lines"
35+
# Take lines after the last parameter
36+
$Lines = @($Text[$NextLine..($parameter.EndLineNumber - $FirstLine)].Where{![string]::IsNullOrWhiteSpace($_)})
37+
# If the last line extends past the end of the parameter, trim that line
38+
if ($Lines.Length -gt 0 -and $parameter.EndColumnNumber -lt $Lines[-1].Length) {
39+
$Lines[-1] = $Lines[-1].SubString($parameter.EndColumnNumber)
40+
}
41+
# Don't return the commas, we'll add them back later
42+
($Lines -join "`n").TrimEnd(",")
43+
} else {
44+
Write-Debug "Extracted parameter $($Parameter.Name) text exactly"
45+
$parameter.Text.TrimEnd(",")
46+
}
47+
}
48+
$NextLine = 1 + $parameter.EndLineNumber - $FirstLine
49+
}
50+
)
51+
52+
$this.InsertLineNumber = $ast.Parameters[-1].Extent.EndLineNumber
53+
$this.InsertColumnNumber = $ast.Parameters[-1].Extent.EndColumnNumber
54+
$this.InsertOffset = $ast.Parameters[-1].Extent.EndOffset
55+
} else {
56+
$this.InsertLineNumber = $ast.Extent.EndLineNumber
57+
$this.InsertColumnNumber = $ast.Extent.EndColumnNumber - 1
58+
$this.InsertOffset = $ast.Extent.EndOffset - 1
59+
}
60+
return [AstVisitAction]::StopVisit
61+
}
62+
}
63+
64+
class AddParameter : ModuleBuilderGenerator {
65+
[System.Management.Automation.HiddenAttribute()]
66+
[ParameterExtractor]$AdditionalParameterCache
67+
68+
[ParameterExtractor]GetAdditional() {
69+
if (!$this.AdditionalParameterCache) {
70+
$this.AdditionalParameterCache = $this.Aspect
71+
}
72+
return $this.AdditionalParameterCache
73+
}
74+
75+
[AstVisitAction] VisitFunctionDefinition([FunctionDefinitionAst]$ast) {
76+
if (!$ast.Where($this.Where)) {
77+
return [AstVisitAction]::SkipChildren
78+
}
79+
$Existing = [ParameterExtractor]$ast
80+
$Additional = $this.GetAdditional().Parameters.Where{ $_.Name -notin $Existing.Parameters.Name }
81+
if (($Text = $Additional.Text -join ",`n`n")) {
82+
$Replacement = [TextReplace]@{
83+
StartOffset = $Existing.InsertOffset
84+
EndOffset = $Existing.InsertOffset
85+
Text = if ($Existing.Parameters.Count -gt 0) {
86+
",`n`n" + $Text
87+
} else {
88+
"`n" + $Text
89+
}
90+
}
91+
92+
Write-Debug "Adding parameters to $($ast.name): $($Additional.Name -join ', ')"
93+
$this.Replacements.Add($Replacement)
94+
}
95+
return [AstVisitAction]::SkipChildren
96+
}
97+
}
98+
99+
class MergeBlocks : ModuleBuilderGenerator {
100+
[System.Management.Automation.HiddenAttribute()]
101+
[NamedBlockAst]$BeginBlockTemplate
102+
103+
[System.Management.Automation.HiddenAttribute()]
104+
[NamedBlockAst]$ProcessBlockTemplate
105+
106+
[System.Management.Automation.HiddenAttribute()]
107+
[NamedBlockAst]$EndBlockTemplate
108+
109+
[List[TextReplace]]Generate([Ast]$ast) {
110+
if (!($this.BeginBlockTemplate = $this.Aspect.Find({ $args[0] -is [NamedBlockAst] -and $args[0].BlockKind -eq "Begin" }, $false))) {
111+
Write-Debug "No Aspect for BeginBlock"
112+
} else {
113+
Write-Debug "BeginBlock Aspect: $($this.BeginBlockTemplate)"
114+
}
115+
if (!($this.ProcessBlockTemplate = $this.Aspect.Find({ $args[0] -is [NamedBlockAst] -and $args[0].BlockKind -eq "Process" }, $false))) {
116+
Write-Debug "No Aspect for ProcessBlock"
117+
} else {
118+
Write-Debug "ProcessBlock Aspect: $($this.ProcessBlockTemplate)"
119+
}
120+
if (!($this.EndBlockTemplate = $this.Aspect.Find({ $args[0] -is [NamedBlockAst] -and $args[0].BlockKind -eq "End" }, $false))) {
121+
Write-Debug "No Aspect for EndBlock"
122+
} else {
123+
Write-Debug "EndBlock Aspect: $($this.EndBlockTemplate)"
124+
}
125+
126+
$ast.Visit($this)
127+
return $this.Replacements
128+
}
129+
130+
# The [Alias(...)] attribute on functions matters, but we can't export aliases that are defined inside a function
131+
[AstVisitAction] VisitFunctionDefinition([FunctionDefinitionAst]$ast) {
132+
if (!$ast.Where($this.Where)) {
133+
return [AstVisitAction]::SkipChildren
134+
}
135+
136+
if ($this.BeginBlockTemplate) {
137+
if ($ast.Body.BeginBlock) {
138+
$BeginExtent = $ast.Body.BeginBlock.Extent
139+
$BeginBlockText = ($BeginExtent.Text -replace "^begin[\s\r\n]*{|}[\s\r\n]*$", "`n").Trim("`r`n").TrimEnd("`r`n ")
140+
141+
$Replacement = [TextReplace]@{
142+
StartOffset = $BeginExtent.StartOffset
143+
EndOffset = $BeginExtent.EndOffset
144+
Text = $this.BeginBlockTemplate.Extent.Text.Replace("existingcode", $BeginBlockText)
145+
}
146+
147+
$this.Replacements.Add( $Replacement )
148+
} else {
149+
Write-Debug "$($ast.Name) Missing BeginBlock"
150+
}
151+
}
152+
153+
if ($this.ProcessBlockTemplate) {
154+
if ($ast.Body.ProcessBlock) {
155+
# In a "filter" function, the process block may contain the param block
156+
$ProcessBlockExtent = $ast.Body.ProcessBlock.Extent
157+
158+
if ($ast.Body.ProcessBlock.UnNamed -and $ast.Body.ParamBlock.Extent.Text) {
159+
# Trim the paramBlock out of the end block
160+
$ProcessBlockText = $ProcessBlockExtent.Text.Remove(
161+
$ast.Body.ParamBlock.Extent.StartOffset - $ProcessBlockExtent.StartOffset,
162+
$ast.Body.ParamBlock.Extent.EndOffset - $ast.Body.ParamBlock.Extent.StartOffset)
163+
$StartOffset = $ast.Body.ParamBlock.Extent.EndOffset
164+
} else {
165+
# Trim the `process {` ... `}` because we're inserting it into the template process
166+
$ProcessBlockText = ($ProcessBlockExtent.Text -replace "^process[\s\r\n]*{|}[\s\r\n]*$", "`n").Trim("`r`n").TrimEnd("`r`n ")
167+
$StartOffset = $ProcessBlockExtent.StartOffset
168+
}
169+
170+
$Replacement = [TextReplace]@{
171+
StartOffset = $StartOffset
172+
EndOffset = $ProcessBlockExtent.EndOffset
173+
Text = $this.ProcessBlockTemplate.Extent.Text.Replace("existingcode", $ProcessBlockText)
174+
}
175+
176+
$this.Replacements.Add( $Replacement )
177+
} else {
178+
Write-Debug "$($ast.Name) Missing ProcessBlock"
179+
}
180+
}
181+
182+
if ($this.EndBlockTemplate) {
183+
if ($ast.Body.EndBlock) {
184+
# The end block is a problem because it frequently contains the param block, which must be left alone
185+
$EndBlockExtent = $ast.Body.EndBlock.Extent
186+
187+
$EndBlockText = $EndBlockExtent.Text
188+
$StartOffset = $EndBlockExtent.StartOffset
189+
if ($ast.Body.EndBlock.UnNamed -and $ast.Body.ParamBlock.Extent.Text) {
190+
# Trim the paramBlock out of the end block
191+
$EndBlockText = $EndBlockExtent.Text.Remove(
192+
$ast.Body.ParamBlock.Extent.StartOffset - $EndBlockExtent.StartOffset,
193+
$ast.Body.ParamBlock.Extent.EndOffset - $ast.Body.ParamBlock.Extent.StartOffset)
194+
$StartOffset = $ast.Body.ParamBlock.Extent.EndOffset
195+
} else {
196+
# Trim the `end {` ... `}` because we're inserting it into the template end
197+
$EndBlockText = ($EndBlockExtent.Text -replace "^end[\s\r\n]*{|}[\s\r\n]*$", "`n").Trim("`r`n").TrimEnd("`r`n ")
198+
}
199+
200+
$Replacement = [TextReplace]@{
201+
StartOffset = $StartOffset
202+
EndOffset = $EndBlockExtent.EndOffset
203+
Text = $this.EndBlockTemplate.Extent.Text.Replace("existingcode", $EndBlockText)
204+
}
205+
206+
$this.Replacements.Add( $Replacement )
207+
} else {
208+
Write-Debug "$($ast.Name) Missing EndBlock"
209+
}
210+
}
211+
212+
return [AstVisitAction]::SkipChildren
213+
}
214+
}
215+

Source/Classes/20. ModuleBuilderAspect.ps1

Lines changed: 0 additions & 10 deletions
This file was deleted.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using namespace System.Management.Automation.Language
2+
using namespace System.Collections.Generic
3+
class TextReplace {
4+
[int]$StartOffset = 0
5+
[int]$EndOffset = 0
6+
[string]$Text = ''
7+
}
8+
9+
class ModuleBuilderGenerator {
10+
hidden [List[TextReplace]]$Replacements = @()
11+
12+
[void] Replace($StartOffset, $EndOffset, $Text) {
13+
$this.Replacements.Add([TextReplace]@{
14+
StartOffset = $StartOffset
15+
EndOffset = $EndOffset
16+
Text = $Text
17+
})
18+
}
19+
20+
[void] Insert($StartOffset, $Text) {
21+
$this.Replacements.Add([TextReplace]@{
22+
StartOffset = $StartOffset
23+
EndOffset = $StartOffset
24+
Text = $Text
25+
})
26+
}
27+
28+
[ScriptBlock]$Filter = { $true }
29+
30+
[Ast]$Ast
31+
32+
hidden [string]$Path
33+
34+
ModuleBuilderGenerator($Path) {
35+
$this.Path = $Path
36+
$this.Ast = ConvertToAst $Path
37+
38+
}
39+
40+
AddParameter([ScriptBlock]$FromScriptBlock) {
41+
[ParameterExtractor]$ExistingParameters = $this.Ast
42+
[ParameterExtractor]$AdditionalParameters = $FromScriptBlock.Ast
43+
44+
$Additional = $AdditionalParameters.Parameters.Where{ $_.Name -notin $ExistingParameters.Parameters.Name }
45+
if (($Text = $Additional.Text -join ",`n`n")) {
46+
$Replacement = [TextReplace]@{
47+
StartOffset = $ExistingParameters.InsertOffset
48+
EndOffset = $ExistingParameters.InsertOffset
49+
Text = if ($ExistingParameters.Parameters.Count -gt 0) {
50+
",`n`n" + $Text
51+
} else {
52+
"`n" + $Text
53+
}
54+
}
55+
56+
Write-Debug "Adding parameters to $($this.Ast.name): $($Additional.Name -join ', ')"
57+
$this.Replacements.Add($Replacement)
58+
}
59+
}
60+
61+
62+
63+
[List[TextReplace]]Generate([Ast]$ast) {
64+
$ast.Visit($this)
65+
return $this.Replacements
66+
}
67+
}

Source/Classes/21. ParameterExtractor.ps1

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
using namespace System.Management.Automation.Language
2+
using namespace System.Collections.Generic
3+
4+
class ParameterPosition {
5+
[string]$Name
6+
[int]$StartOffset
7+
[string]$Text
8+
}
9+
110
class ParameterExtractor : AstVisitor {
211
[ParameterPosition[]]$Parameters = @()
312
[int]$InsertLineNumber = -1
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
class AddParameterAspect : ModuleBuilderAspect {
1+
using namespace System.Management.Automation.Language
2+
using namespace System.Collections.Generic
3+
4+
class ParameterGenerator : ModuleBuilderGenerator {
25
[System.Management.Automation.HiddenAttribute()]
36
[ParameterExtractor]$AdditionalParameterCache
47

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
class MergeBlocksAspect : ModuleBuilderAspect {
1+
using namespace System.Management.Automation.Language
2+
using namespace System.Collections.Generic
3+
4+
class BlockGenerator : ModuleBuilderGenerator {
25
[System.Management.Automation.HiddenAttribute()]
36
[NamedBlockAst]$BeginBlockTemplate
47

@@ -40,6 +43,7 @@ class MergeBlocksAspect : ModuleBuilderAspect {
4043
$BeginExtent = $ast.Body.BeginBlock.Extent
4144
$BeginBlockText = ($BeginExtent.Text -replace "^begin[\s\r\n]*{|}[\s\r\n]*$", "`n").Trim("`r`n").TrimEnd("`r`n ")
4245

46+
4347
$Replacement = [TextReplace]@{
4448
StartOffset = $BeginExtent.StartOffset
4549
EndOffset = $BeginExtent.EndOffset

Source/ModuleBuilder.psd1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
# Release Notes have to be here, so we can update them
1313
ReleaseNotes = '
14-
Fix case sensitivity of defaults for SourceDirectories and PublicFilter
14+
Add support for Aspect Oriented Programming (AOP) with the new `Aspects` parameter.
1515
'
1616

1717
# Tags applied to this module. These help with module discovery in online galleries.
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
function MergeAspect {
1+
function InvokeGenerator {
22
<#
33
.SYNOPSIS
4-
Merge features of a script into commands from a module, using a ModuleBuilderAspect
4+
Generate code using a ModuleBuilderGenerator
55
.DESCRIPTION
66
This is an aspect-oriented programming approach for adding cross-cutting features to functions in a module.
77
8-
The [ModuleBuilderAspect] implementations are [AstVisitors] that return [TextReplace] object representing modifications to be performed on the source.
8+
The [ModuleBuilderGenerator] implementations are [AstVisitors] that return [TextReplace] object representing modifications to be performed on the source.
99
#>
1010
[CmdletBinding()]
1111
param(
@@ -18,7 +18,7 @@ function MergeAspect {
1818
# - MergeBlocks. Supports Before/After/Around blocks for aspects like error handling or authentication.
1919
# - AddParameter. Supports adding common parameters to functions (usually in conjunction with MergeBlock that use those parameters)
2020
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
21-
[ValidateScript({ (($_ -As [Type]), ("${_}Aspect" -As [Type])).BaseType -eq [ModuleBuilderAspect] })]
21+
[ValidateScript({ (($_ -As [Type]), ("${_}Aspect" -As [Type])).BaseType -eq [ModuleBuilderGenerator] })]
2222
[string]$Action,
2323

2424
# The name(s) of functions in the module to run the generator against. Supports wildcards.
@@ -40,7 +40,7 @@ function MergeAspect {
4040
} elseif ("${Action}Aspect" -As [Type]) {
4141
"${Action}Aspect"
4242
} else {
43-
throw "Can't find $Action ModuleBuilderAspect"
43+
throw "Can't find $Action ModuleBuilderGenerator"
4444
}
4545

4646
$Aspect = New-Object $Action -Property @{

Source/Public/Build-Module.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ function Build-Module {
299299
if ($ModuleInfo.Aspects) {
300300
$AspectDirectory = Join-Path -Path $ModuleInfo.ModuleBase -ChildPath $ModuleInfo.AspectDirectory | Convert-Path -ErrorAction SilentlyContinue
301301
Write-Verbose "Apply $($ModuleInfo.Aspects.Count) Aspects from $AspectDirectory"
302-
$ModuleInfo.Aspects | MergeAspect $RootModule
302+
$ModuleInfo.Aspects | InvokeGenerator $RootModule
303303
}
304304

305305
# This is mostly for testing ...

0 commit comments

Comments
 (0)