Skip to content

Commit 9c53092

Browse files
committed
Add a couple of refactoring Aspect-Oriented Programming generators
1 parent 528b694 commit 9c53092

File tree

4 files changed

+366
-17
lines changed

4 files changed

+366
-17
lines changed

Source/ModuleBuilder.psd1

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
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 Script Generators and convert our Move-UsingStatment and Update-AliasesToExport to generators
15+
Add Merge-ScriptBlock and Add-Parameter generators to support extracting more common boilerplate code
1516
'
1617

1718
# Tags applied to this module. These help with module discovery in online galleries.

Source/Public/Add-Parameter.ps1

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
using namespace System.Management.Automation.Language
2+
3+
function Add-Parameter {
4+
<#
5+
.SYNOPSIS
6+
Adds parameters from one script or function to another.
7+
.DESCRIPTION
8+
Add-Parameter will copy parameters from the boilerplate to the function(s) in the InputObject without overwriting existing parameters.
9+
10+
It is usually used in conjunction with Merge-Block to merge a common implementation from the boilerplate.
11+
12+
Note that THIS generator does not add parameters to script files directly, but only to functions defined in the InputObject.
13+
.EXAMPLE
14+
# Or use a file path instead
15+
$Boilerplate = @'
16+
param(
17+
# The Foreground Color (name, #rrggbb, etc)
18+
[Alias('Fg')]
19+
[PoshCode.Pansies.RgbColor]$ForegroundColor,
20+
21+
# The Background Color (name, #rrggbb, etc)
22+
[Alias('Bg')]
23+
[PoshCode.Pansies.RgbColor]$BackgroundColor
24+
)
25+
$ForegroundColor.ToVt() + $BackgroundColor.ToVt($true) + (
26+
Use-OriginalBlock
27+
) +"`e[0m" # Reset colors
28+
'@
29+
30+
# Or use a file path instead
31+
$Source = @'
32+
function Show-Date {
33+
param(
34+
# The text to display
35+
[string]$Format
36+
)
37+
Get-Date -Format $Format
38+
}
39+
'@
40+
41+
Invoke-ScriptGenerator $Source -Generator Add-Parameter -Parameters @{ FunctionName = "*"; Boilerplate = $Boilerplate } -OutVariable Source
42+
43+
function Show-Date {
44+
param(
45+
# The text to display
46+
[string]$Format,
47+
48+
# The Foreground Color (name, #rrggbb, etc)
49+
[Alias('Fg')]
50+
[PoshCode.Pansies.RgbColor]$ForegroundColor,
51+
52+
# The Background Color (name, #rrggbb, etc)
53+
[Alias('Bg')]
54+
[PoshCode.Pansies.RgbColor]$BackgroundColor
55+
)
56+
Get-Date -Format $Format
57+
}
58+
#>
59+
[CmdletBinding()]
60+
[OutputType([TextReplacement])]
61+
param(
62+
#
63+
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
64+
[Alias("Ast")]
65+
[Ast]$InputObject,
66+
67+
[Parameter()]
68+
[string[]]$FunctionName = "*",
69+
70+
[Parameter(Mandatory)]
71+
[string]$Boilerplate
72+
)
73+
begin {
74+
75+
class ParameterPosition {
76+
[string]$Name
77+
[int]$StartOffset
78+
[string]$Text
79+
}
80+
81+
class ParameterExtractor : AstVisitor {
82+
[ParameterPosition[]]$Parameters = @()
83+
[int]$InsertLineNumber = -1
84+
[int]$InsertColumnNumber = -1
85+
[int]$InsertOffset = -1
86+
87+
ParameterExtractor([Ast]$Ast) {
88+
$ast.Visit($this)
89+
}
90+
91+
[AstVisitAction] VisitParamBlock([ParamBlockAst]$ast) {
92+
if ($Ast.Parameters) {
93+
$Text = $ast.Extent.Text -split "\r?\n"
94+
95+
$FirstLine = $ast.Extent.StartLineNumber
96+
$NextLine = 1
97+
$this.Parameters = @(
98+
foreach ($parameter in $ast.Parameters | Select-Object Name -Expand Extent) {
99+
[ParameterPosition]@{
100+
Name = $parameter.Name
101+
StartOffset = $parameter.StartOffset
102+
Text = if (($parameter.StartLineNumber - $FirstLine) -ge $NextLine) {
103+
Write-Debug "Extracted parameter $($Parameter.Name) with surrounding lines"
104+
# Take lines after the last parameter
105+
$Lines = @($Text[$NextLine..($parameter.EndLineNumber - $FirstLine)].Where{ ![string]::IsNullOrWhiteSpace($_) })
106+
# If the last line extends past the end of the parameter, trim that line
107+
if ($Lines.Length -gt 0 -and $parameter.EndColumnNumber -lt $Lines[-1].Length) {
108+
$Lines[-1] = $Lines[-1].SubString($parameter.EndColumnNumber)
109+
}
110+
# Don't return the commas, we'll add them back later
111+
($Lines -join "`n").TrimEnd(",")
112+
} else {
113+
Write-Debug "Extracted parameter $($Parameter.Name) text exactly"
114+
$parameter.Text.TrimEnd(",")
115+
}
116+
}
117+
$NextLine = 1 + $parameter.EndLineNumber - $FirstLine
118+
}
119+
)
120+
121+
$this.InsertLineNumber = $ast.Parameters[-1].Extent.EndLineNumber
122+
$this.InsertColumnNumber = $ast.Parameters[-1].Extent.EndColumnNumber
123+
$this.InsertOffset = $ast.Parameters[-1].Extent.EndOffset
124+
} else {
125+
$this.InsertLineNumber = $ast.Extent.EndLineNumber
126+
$this.InsertColumnNumber = $ast.Extent.EndColumnNumber - 1
127+
$this.InsertOffset = $ast.Extent.EndOffset - 1
128+
}
129+
return [AstVisitAction]::StopVisit
130+
}
131+
}
132+
133+
# By far the fastest way to parse things out is with an AstVisitor
134+
class ParameterGenerator : AstVisitor {
135+
[List[TextReplacement]]$Replacements = @()
136+
[ScriptBlock]$FunctionFilter = { $true }
137+
138+
[ParameterExtractor]$ParameterSource
139+
140+
[AstVisitAction] VisitFunctionDefinition([FunctionDefinitionAst]$ast) {
141+
if (!$ast.Where($this.FunctionFilter)) {
142+
return [AstVisitAction]::SkipChildren
143+
}
144+
145+
[ParameterExtractor]$ExistingParameters = $ast
146+
147+
Write-Debug "Existing parameters in $($ast.Name): $($ExistingParameters.Parameters.Name -join ', ')"
148+
$Additional = $this.ParameterSource.Parameters.Where{ $_.Name -notin $ExistingParameters.Parameters.Name }
149+
if (($Text = $Additional.Text -join ",`n`n")) {
150+
Write-Debug "Adding parameters to $($ast.Name): $($Additional.Name -join ', ')"
151+
$this.Replacements.Add(@{
152+
StartOffset = $ExistingParameters.InsertOffset
153+
EndOffset = $ExistingParameters.InsertOffset
154+
Text = if ($ExistingParameters.Parameters.Count -gt 0) {
155+
",`n`n" + $Text
156+
} else {
157+
"`n" + $Text
158+
}
159+
})
160+
}
161+
return [AstVisitAction]::SkipChildren
162+
}
163+
}
164+
}
165+
process {
166+
Write-Debug "Add-Parameter $InputObject $FunctionName $Boilerplate"
167+
168+
$Generator = [ParameterGenerator]@{
169+
FunctionFilter = { $Func = $_; $FunctionName.ForEach({ $Func.Name -like $_ }) -contains $true }.GetNewClosure()
170+
ParameterSource = (ConvertToAst $Boilerplate).AST
171+
}
172+
173+
$InputObject.Visit($Generator)
174+
175+
Write-Debug "Total Replacements: $($Generator.Replacements.Count)"
176+
177+
$Generator.Replacements
178+
}
179+
}

Source/Public/Build-Module.ps1

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ function Build-Module {
3333
3434
This example shows how to use a semantic version from gitversion to version your build.
3535
#>
36-
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Build is approved now")]
36+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification = "Build is approved now")]
3737
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseCmdletCorrectly", "")]
38-
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "", Justification="Parameter handling is in InitializeBuild")]
38+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "", Justification = "Parameter handling is in InitializeBuild")]
3939
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidDefaultValueSwitchParameter", "", Justification = "VersionedOutputDirectory is Deprecated")]
40-
[CmdletBinding(DefaultParameterSetName="SemanticVersion")]
40+
[CmdletBinding(DefaultParameterSetName = "SemanticVersion")]
4141
[Alias("build")]
4242
param(
4343
# The path to the module folder, manifest or build.psd1
@@ -66,24 +66,24 @@ function Build-Module {
6666

6767
# Semantic version, like 1.0.3-beta01+sha.22c35ffff166f34addc49a3b80e622b543199cc5
6868
# If the SemVer has metadata (after a +), then the full Semver will be added to the ReleaseNotes
69-
[Parameter(ParameterSetName="SemanticVersion")]
69+
[Parameter(ParameterSetName = "SemanticVersion")]
7070
[string]$SemVer,
7171

7272
# The module version (must be a valid System.Version such as PowerShell supports for modules)
7373
[Alias("ModuleVersion")]
74-
[Parameter(ParameterSetName="ModuleVersion", Mandatory)]
75-
[version]$Version = $(if(($V = $SemVer.Split("+")[0].Split("-",2)[0])){$V}),
74+
[Parameter(ParameterSetName = "ModuleVersion", Mandatory)]
75+
[version]$Version = $(if (($V = $SemVer.Split("+")[0].Split("-", 2)[0])) { $V }),
7676

7777
# Setting pre-release forces the release to be a pre-release.
7878
# Must be valid pre-release tag like PowerShellGet supports
79-
[Parameter(ParameterSetName="ModuleVersion")]
80-
[string]$Prerelease = $($SemVer.Split("+")[0].Split("-",2)[1]),
79+
[Parameter(ParameterSetName = "ModuleVersion")]
80+
[string]$Prerelease = $($SemVer.Split("+")[0].Split("-", 2)[1]),
8181

8282
# Build metadata (like the commit sha or the date).
8383
# If a value is provided here, then the full Semantic version will be inserted to the release notes:
8484
# Like: ModuleName v(Version(-Prerelease?)+BuildMetadata)
85-
[Parameter(ParameterSetName="ModuleVersion")]
86-
[string]$BuildMetadata = $($SemVer.Split("+",2)[1]),
85+
[Parameter(ParameterSetName = "ModuleVersion")]
86+
[string]$BuildMetadata = $($SemVer.Split("+", 2)[1]),
8787

8888
# Folders which should be copied intact to the module output
8989
# Can be relative to the module folder
@@ -114,7 +114,7 @@ function Build-Module {
114114
# File encoding for output RootModule (defaults to UTF8)
115115
# Converted to System.Text.Encoding for PowerShell 6 (and something else for PowerShell 5)
116116
[ValidateSet("UTF8", "UTF8Bom", "UTF8NoBom", "UTF7", "ASCII", "Unicode", "UTF32")]
117-
[string]$Encoding = $(if($IsCoreCLR) { "UTF8Bom" } else { "UTF8" }),
117+
[string]$Encoding = $(if ($IsCoreCLR) { "UTF8Bom" } else { "UTF8" }),
118118

119119
# The prefix is either the path to a file (relative to the module folder) or text to put at the top of the file.
120120
# If the value of prefix resolves to a file, that file will be read in, otherwise, the value will be used.
@@ -124,7 +124,7 @@ function Build-Module {
124124
# The Suffix is either the path to a file (relative to the module folder) or text to put at the bottom of the file.
125125
# If the value of Suffix resolves to a file, that file will be read in, otherwise, the value will be used.
126126
# The default is nothing. See examples for more details.
127-
[Alias("ExportModuleMember","Postfix")]
127+
[Alias("ExportModuleMember", "Postfix")]
128128
[string]$Suffix,
129129

130130
# Controls whether we delete the output folder and whether we build the output
@@ -137,6 +137,17 @@ function Build-Module {
137137
[ValidateSet("Clean", "Build", "CleanBuild")]
138138
[string]$Target = "CleanBuild",
139139

140+
# A list of Generators to apply to the module. You can specify an array of hashtables where each hashtable has the Generator name, and any additional parameters that the generator requires.
141+
#
142+
# There are two built-in Generators so far:
143+
# - Add-Parameter. Adds parameters to functions in the module from a boilerplate file, which must be a script with a param block.
144+
# - Merge-ScriptBlock. Merges boilerplate templates into functions in your module. The command "Use-OriginalBlock" in the boilerplate indicates where the code from the original function would fit into the template. The added blocks come from a boilerplate tempalte file, which must be a script, and can have named begin, process, and end blocks.
145+
[PSCustomObject[]]$Generators = @(),
146+
147+
# The folder (relative to the module folder) which contains the scripts which serve as boilerplate templates for Script Generators
148+
# Defaults to "Generators"
149+
[string[]]$BoilerplateDirectory = @("Boilerplate", "Boilerplates", "Templates"),
150+
140151
# Output the ModuleInfo of the "built" module
141152
[switch]$Passthru
142153
)
@@ -202,7 +213,7 @@ function Build-Module {
202213
}
203214

204215
# We have to force the Encoding to string because PowerShell Core made up encodings
205-
SetModuleContent -Source (@($ModuleInfo.Prefix) + $AllScripts.FullName + @($ModuleInfo.Suffix)).Where{$_} -Output $RootModule -Encoding "$($ModuleInfo.Encoding)"
216+
SetModuleContent -Source (@($ModuleInfo.Prefix) + $AllScripts.FullName + @($ModuleInfo.Suffix)).Where{ $_ } -Output $RootModule -Encoding "$($ModuleInfo.Encoding)"
206217

207218
# Make sure Generators has (only one copy of) our built-in mandatory ones
208219
# Move-UsingStatement always comes first, in hopes there will not be any parse errors afterward
@@ -239,7 +250,7 @@ function Build-Module {
239250
if ($ModuleInfo.PublicFilter) {
240251
# SilentlyContinue because there don't *HAVE* to be public functions
241252
if (($PublicFunctions = Get-ChildItem $ModuleInfo.PublicFilter -Recurse -ErrorAction SilentlyContinue |
242-
Where-Object BaseName -in $AllScripts.BaseName |
253+
Where-Object BaseName -In $AllScripts.BaseName |
243254
Select-Object -ExpandProperty BaseName)) {
244255

245256
Update-Metadata -Path $OutputManifest -PropertyName FunctionsToExport -Value $PublicFunctions
@@ -278,12 +289,12 @@ function Build-Module {
278289
} elseif ($RelNote -match "^\s*\n") {
279290
# Leading whitespace includes newlines
280291
Write-Verbose "Existing ReleaseNotes:$RelNote"
281-
$RelNote = $RelNote -replace "^(?s)(\s*)\S.*$|^$","`${1}$($Line)`$_"
292+
$RelNote = $RelNote -replace "^(?s)(\s*)\S.*$|^$", "`${1}$($Line)`$_"
282293
Write-Verbose "New ReleaseNotes:$RelNote"
283294
Update-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.ReleaseNotes -Value $RelNote
284295
} else {
285296
Write-Verbose "Existing ReleaseNotes:`n$RelNote"
286-
$RelNote = $RelNote -replace "^(?s)(\s*)\S.*$|^$","`${1}$($Line)`n`$_"
297+
$RelNote = $RelNote -replace "^(?s)(\s*)\S.*$|^$", "`${1}$($Line)`n`$_"
287298
Write-Verbose "New ReleaseNotes:`n$RelNote"
288299
Update-Metadata -Path $OutputManifest -PropertyName PrivateData.PSData.ReleaseNotes -Value $RelNote
289300
}

0 commit comments

Comments
 (0)