From 16b3714ff37bcaf7f765a300a51970f2c5491496 Mon Sep 17 00:00:00 2001 From: "adrian.andersson" Date: Wed, 3 Jun 2026 19:12:50 +1000 Subject: [PATCH 1/3] chore: small linting errors --- source/functions/Add-MFProjectScripts.ps1 | 6 +++--- source/functions/Write-MFModuleDocs.ps1 | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/source/functions/Add-MFProjectScripts.ps1 b/source/functions/Add-MFProjectScripts.ps1 index 184d539..ef830d6 100644 --- a/source/functions/Add-MFProjectScripts.ps1 +++ b/source/functions/Add-MFProjectScripts.ps1 @@ -9,7 +9,7 @@ function Add-MFProjectScripts Copies script templates from ModuleForge's resource\scripts folder into a scripts subfolder at the project root. Skips existing files by default. - Includes Invoke-MFPester.ps1 — a standalone Pester runner that does not depend on + Includes Invoke-MFPester.ps1 - a standalone Pester runner that does not depend on the ModuleForge module, safe for use in clean CI environments. This function is also called automatically by add-mfFilesAndFolders during project @@ -65,7 +65,7 @@ function Add-MFProjectScripts if(!(Test-Path $resourceFolder)) { - Write-Warning 'Add-MFProjectScripts: resource folder not found in ModuleForge module — skipping script scaffold' + Write-Warning 'Add-MFProjectScripts: resource folder not found in ModuleForge module - skipping script scaffold' $skipProcess = $true } @@ -74,7 +74,7 @@ function Add-MFProjectScripts $resourceFolderScripts = Join-Path $resourceFolder 'scripts' if(!(Test-Path $resourceFolderScripts)) { - Write-Warning 'Add-MFProjectScripts: scripts folder not found in ModuleForge resource — skipping script scaffold' + Write-Warning 'Add-MFProjectScripts: scripts folder not found in ModuleForge resource - skipping script scaffold' $skipProcess = $true } } diff --git a/source/functions/Write-MFModuleDocs.ps1 b/source/functions/Write-MFModuleDocs.ps1 index e68c276..697e4ed 100644 --- a/source/functions/Write-MFModuleDocs.ps1 +++ b/source/functions/Write-MFModuleDocs.ps1 @@ -44,7 +44,7 @@ function Write-MFModuleDocs #> [CmdletBinding()] - [Diagnostics.CodeAnalysis.SuppressMessage("PSUseSingularNouns", "", Justification = "Plural 'Docs' reflects a short form of documentation")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification='Plural Docs reflects a short form of documentation')] PARAM( #The root path where documentation should be stored. Defaults to the current directory. [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)] From 92e9d72d094d7c0224f1ddec9176373b3b006f09 Mon Sep 17 00:00:00 2001 From: "adrian.andersson" Date: Wed, 3 Jun 2026 19:32:42 +1000 Subject: [PATCH 2/3] feat: move the scriptblock from Get-MFFolderItemDetails into an external file to improve readability and portabilty --- source/functions/Build-MFProject.Tests.ps1 | 2 + .../functions/Get-MFDependencyTree.Tests.ps1 | 1 + .../Get-MFFolderItemDetails.Tests.ps1 | 91 +++++- source/functions/Get-MFFolderItemDetails.ps1 | 266 ++---------------- .../Get-MFFolderItemDetails.scriptblock.ps1 | 216 ++++++++++++++ 5 files changed, 333 insertions(+), 243 deletions(-) create mode 100644 source/resource/scriptBlocks/Get-MFFolderItemDetails.scriptblock.ps1 diff --git a/source/functions/Build-MFProject.Tests.ps1 b/source/functions/Build-MFProject.Tests.ps1 index 1202108..c78dcd4 100644 --- a/source/functions/Build-MFProject.Tests.ps1 +++ b/source/functions/Build-MFProject.Tests.ps1 @@ -7,6 +7,8 @@ BeforeAll{ #Reference Current Path $currentPath = $(get-location).path $sourcePath = join-path -path $currentPath -childPath 'source' + # Captured before $sourcePath is reassigned to the temp build folder below + $mockPsScriptRoot = $sourcePath $dependencies = [ordered]@{ functions = @('Get-MFFolderItems.ps1','Get-MFDependencyTree.ps1','Get-MFFolderItemDetails.ps1','New-MFProject.ps1','Register-MFLocalPsResourceRepository.ps1','Remove-MFLocalPsResourceRepository.ps1','Add-MFRepositoryXmlData.ps1','Add-MFProjectScripts.ps1') diff --git a/source/functions/Get-MFDependencyTree.Tests.ps1 b/source/functions/Get-MFDependencyTree.Tests.ps1 index bdd4c2b..49b94f7 100644 --- a/source/functions/Get-MFDependencyTree.Tests.ps1 +++ b/source/functions/Get-MFDependencyTree.Tests.ps1 @@ -7,6 +7,7 @@ BeforeAll{ #Reference Current Path $currentPath = $(get-location).path $sourcePath = join-path -path $currentPath -childPath 'source' + $mockPsScriptRoot = $sourcePath $dependencies = [ordered]@{ functions = @('Get-MFFolderItems.ps1','Get-MFFolderItemDetails.ps1') diff --git a/source/functions/Get-MFFolderItemDetails.Tests.ps1 b/source/functions/Get-MFFolderItemDetails.Tests.ps1 index 1d76e7a..cfc7858 100644 --- a/source/functions/Get-MFFolderItemDetails.Tests.ps1 +++ b/source/functions/Get-MFFolderItemDetails.Tests.ps1 @@ -8,6 +8,8 @@ BeforeAll{ $currentPath = $(get-location).path $sourcePath = join-path -path $currentPath -childPath 'source' + $mockPsScriptRoot = $sourcePath + $dependencies = [ordered]@{ functions = @('Get-MFFolderItems.ps1') } @@ -26,7 +28,6 @@ BeforeAll{ } } - #Load This File . $PSCommandPath.Replace('.Tests.ps1','.ps1') @@ -62,5 +63,91 @@ Describe 'Get-MFFolderItemDetails' { $folderItemDetails.where{$_.name -eq 'Get-MFFolderItemDetails.ps1'}.Dependencies.Reference | Should -be 'Get-MFFolderItems' } +} + +Describe 'Get-MFFolderItemDetails scriptblock - function analysis' { + BeforeAll { + $sbPath = Join-Path $sourcePath 'resource' 'scriptBlocks' 'Get-MFFolderItemDetails.scriptblock.ps1' + [scriptblock]$sb = [scriptblock]::Create((Get-Content $sbPath -Raw)) + + $testRoot = Join-Path ([System.IO.Path]::GetTempPath()) "MFSbTest_$([System.IO.Path]::GetRandomFileName())" + $testFunctionsDir = Join-Path $testRoot 'source' 'functions' + New-Item -ItemType Directory -Path $testFunctionsDir -Force | Out-Null + + @' +function Do-Something { + param([string]$InputValue) + process { return $InputValue } +} +'@ | Set-Content (Join-Path $testFunctionsDir 'Do-Something.ps1') + + @' +function Do-Other { + param([string]$InputValue) + process { return Do-Something $InputValue } +} +'@ | Set-Content (Join-Path $testFunctionsDir 'Do-Other.ps1') -} \ No newline at end of file + $testFolderItems = Get-MFFolderItems -Path $testFunctionsDir -PSScriptsOnly + $sbResult = & $sb $testFunctionsDir $testFolderItems + } + + AfterAll { + Remove-Item $testRoot -Recurse -Force -ErrorAction SilentlyContinue -ProgressAction SilentlyContinue + } + + It 'Should return one result per file' { + $sbResult.Count | Should -Be 2 + } + It 'Should identify function names correctly' { + $sbResult.FunctionDetails.functionName | Should -Contain 'Do-Something' + $sbResult.FunctionDetails.functionName | Should -Contain 'Do-Other' + } + It 'Should detect the cross-file dependency' { + $sbResult.Where{$_.Name -eq 'Do-Other.ps1'}.Dependencies.Reference | Should -Contain 'Do-Something' + } + It 'Should populate relativePath on each item' { + $sbResult | ForEach-Object { $_.relativePath | Should -Not -BeNullOrEmpty } + } +} + +Describe 'Get-MFFolderItemDetails scriptblock - class analysis' { + BeforeAll { + $sbPath = Join-Path $sourcePath 'resource' 'scriptBlocks' 'Get-MFFolderItemDetails.scriptblock.ps1' + [scriptblock]$sb = [scriptblock]::Create((Get-Content $sbPath -Raw)) + + $testRoot = Join-Path ([System.IO.Path]::GetTempPath()) "MFSbClassTest_$([System.IO.Path]::GetRandomFileName())" + $testClassDir = Join-Path $testRoot 'source' 'classes' + New-Item -ItemType Directory -Path $testClassDir -Force | Out-Null + + @' +class MyTestClass { + [string]$Name + [int]$Count + MyTestClass([string]$n) { $this.Name = $n } + [string] GetName() { return $this.Name } +} +'@ | Set-Content (Join-Path $testClassDir 'MyTestClass.ps1') + + $testFolderItems = Get-MFFolderItems -Path $testClassDir -PSScriptsOnly + $sbResult = & $sb $testClassDir $testFolderItems + } + + AfterAll { + Remove-Item $testRoot -Recurse -Force -ErrorAction SilentlyContinue -ProgressAction SilentlyContinue + } + + It 'Should return a result for the class file' { + $sbResult | Should -Not -BeNullOrEmpty + } + It 'Should identify the class name' { + $sbResult.ClassDetails.className | Should -Contain 'MyTestClass' + } + It 'Should identify class methods' { + $sbResult.ClassDetails.Where{$_.className -eq 'MyTestClass'}.methods.Name | Should -Contain 'GetName' + } + It 'Should identify class properties' { + $sbResult.ClassDetails.Where{$_.className -eq 'MyTestClass'}.properties.Name | Should -Contain 'Name' + $sbResult.ClassDetails.Where{$_.className -eq 'MyTestClass'}.properties.Name | Should -Contain 'Count' + } +} diff --git a/source/functions/Get-MFFolderItemDetails.ps1 b/source/functions/Get-MFFolderItemDetails.ps1 index 2eb31ad..9a84aa1 100644 --- a/source/functions/Get-MFFolderItemDetails.ps1 +++ b/source/functions/Get-MFFolderItemDetails.ps1 @@ -4,10 +4,10 @@ function Get-MFFolderItemDetails <# .SYNOPSIS This function analyses a PS1 file, returning its content, any functions, classes and dependencies, as well as a relative location - + .DESCRIPTION The `Get-MFFolderItemDetails` function takes a path to source folder - + It creates a job that generates a details about all found PS1 files, including: The content of PS1 files, the names of any functions, the names of any classes, and any inter-related dependencies @@ -15,11 +15,11 @@ function Get-MFFolderItemDetails So it works without having to build the manifest etc This function is primary used to build dependency trees and during build to get file contents - + ------------ .EXAMPLE Get-MFFolderItemDetails .\source - + .INPUTS [STRING] - Path to Source Folder is accepted as Pipeline Input or direct assignment @@ -48,13 +48,13 @@ function Get-MFFolderItemDetails - **Methods** (`[String]`) - Methods defined in the class. - **Properties** (`[String[]]`) - Properties within the class. - + .NOTES Author: Adrian Andersson #> [CmdletBinding()] - [Diagnostics.CodeAnalysis.SuppressMessage("PSUseSingularNouns", "", Justification = "Plural 'Details' reflects multiple attributes returned and improves clarity.")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification='Plural Details reflects multiple attributes returned and improves clarity.')] PARAM( #Path to source folder. [Parameter(ValueFromPipelineByPropertyName,ValueFromPipeline)] @@ -65,243 +65,27 @@ function Get-MFFolderItemDetails write-verbose "===========Executing $($MyInvocation.InvocationName)===========" #Return the sent variables when running debug Write-Debug "BoundParams: $($MyInvocation.BoundParameters|Out-String)" - } - - process{ - - write-verbose 'Creating Scriptblock' - [scriptblock]$sblock = { - param($Path,$folderItems) - - function Get-MFScriptDetails { - param( - [Parameter(Mandatory)] - [string]$Path, - [Parameter()] - [string]$RelativePath, - [ValidateSet('Class','Function','All')] - [Parameter()] - [string]$Type = 'All', - [Parameter()] - [string]$FolderGroup - ) - begin{ - write-verbose 'Checking Item' - if($Path[-1] -eq '\' -or $Path[-1] -eq '/') - { - write-verbose 'Removing extra \ or / from path' - $Path = $Path.Substring(0,$($Path.length-1)) - write-verbose "New Path $Path" - } - $file = get-item $Path - if(!$file) - { - throw "File not found at: $Path" - } - } - process{ - $AST = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$null, [ref]$null) - - - #$Functions = $AST.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true) - #The above was the original way to do this - #However it was so efficient it also returned subfunctions AND functions in scriptblocks - #Since we don't want to do that, we cycle through and look at the start and end line numbers and only return top-level functions - #Leaving it here as a reminder - - $AllFunctions = $AST.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true) - $TopLevelFunctions = New-Object System.Collections.Generic.List[Object] - foreach($func in $allFunctions){ - $isNested = $false - foreach($parentFunc in $allFunctions) - { - if($func -ne $parentFunc -and $func.Extent.StartLineNumber -ge $parentFunc.Extent.StartLineNumber -and $func.Extent.EndLineNumber -le $parentFunc.Extent.EndLineNumber) - { - $isNested = $true - break - } - } - if(-not $isNested) { - $TopLevelFunctions.add($func) - } - } - - $Classes = $AST.FindAll({ $args[0] -is [System.Management.Automation.Language.TypeDefinitionAst] }, $true) - if($Type -eq 'All' -or $Type -eq 'Function') - { - $functionDetails = foreach ($Function in $TopLevelFunctions) { - $cmdletDependenciesList = New-Object System.Collections.Generic.List[string] - $TypeDependenciesList = New-Object System.Collections.Generic.List[string] - $paramTypeDependenciesList = New-Object System.Collections.Generic.List[string] - $validatorTypeDependenciesList = New-Object System.Collections.Generic.List[string] - $FunctionName = $Function.Name - $Cmdlets = $Function.FindAll({ $args[0] -is [System.Management.Automation.Language.CommandAst] }, $true) - foreach($c in $Cmdlets) - { - $cmdletDependenciesList.add($c.GetCommandName()) - } - $TypeExpressions = $Function.FindAll({ $args[0] -is [System.Management.Automation.Language.TypeExpressionAst] }, $true) - $TypeExpressions.TypeName.FullName.foreach{ - $tname = $_ - [string]$tnameReplace = $($tname.Replace('[','')).replace(']','') - $TypeDependenciesList.add($tnameReplace) - } - $Parameters = $Function.Body.ParamBlock.Parameters - $Parameters.StaticType.Name.foreach{$paramTypeDependenciesList.add($_)} - $attributes = $Parameters.Attributes - foreach($att in $attributes) - { - $refType = $att.TypeName.GetReflectionType() - if($refType -and ($refType.IsSubclassOf([System.Management.Automation.ValidateArgumentsAttribute]) -or [System.Management.Automation.ValidateArgumentsAttribute].IsAssignableFrom($refType))) { - [string]$tname = $Att.TypeName.FullName - [string]$tname = $($tname.Replace('[','')).replace(']','') - $validatorTypeDependenciesList.Add($tname) - } - } - - [psCustomObject]@{ - functionName = $FunctionName - cmdLets = $cmdletDependenciesList|group-object|Select-Object Name,Count - types = $TypeDependenciesList|group-object|Select-Object Name,Count - parameterTypes = $paramTypeDependenciesList|group-object|Select-Object name,count - Validators = $validatorTypeDependenciesList|Group-Object|Select-Object name,count - } - } - } - if($Type -eq 'all' -or $Type -eq 'Class') - { - $classDetails = foreach ($Class in $Classes) { - $className = $Class.Name - $classMethodsList = New-Object System.Collections.Generic.List[string] - $classPropertiesList = New-Object System.Collections.Generic.List[string] - $Methods = $Class.Members | Where-Object { $_ -is [System.Management.Automation.Language.FunctionMemberAst] } - foreach($m in $Methods) - { - $classMethodsList.add($m.Name) - } - $Properties = $Class.Members | Where-Object { $_ -is [System.Management.Automation.Language.PropertyMemberAst] } - foreach($p in $Properties) - { - $classPropertiesList.add($p.Name) - } - [psCustomObject]@{ - className = $className - methods = $classMethodsList|group-object|Select-Object Name,Count - properties = $classPropertiesList|group-object|Select-Object Name,Count - } - } - } - $objectHash = @{ - Name = $file.Name - Path = $file.FullName - FileSize = "$([math]::round($file.length / 1kb,2)) kb" - FunctionDetails = $functionDetails - ClassDetails = $classDetails - Content = $AST.ToString() - } - if($RelativePath) - { - $objectHash.relativePath = $RelativePath - } - if($FolderGroup) - { - $objectHash.group = $FolderGroup - } - [psCustomObject]$objectHash - } - } - - $privateMatch = "*$([IO.Path]::DirectorySeparatorChar)private$([IO.Path]::DirectorySeparatorChar)*" - $functionMatch = "*$([IO.Path]::DirectorySeparatorChar)functions$([IO.Path]::DirectorySeparatorChar)*" - - $folderItems.ForEach{ - - if($_.path -notlike $privateMatch -and $_.path -notlike $functionMatch) - { - #Need to dot source the files to make sure all the types are loaded - #Only needs to happen for non-function files - . $_.Path - } - } - - $thisPath = (Get-Item $Path) - $relPathBase = ".$([IO.Path]::DirectorySeparatorChar)$($thisPath.name)" - - $itemDetails = $folderItems.ForEach{ - $folderPath = join-path -path $_.folder -childpath $_.RelativePath.Substring(1) - $relPath = join-path -path $relPathBase -childpath $folderPath - if($_.path -like $privateMatch -or $_.path -like $functionMatch -or $_.folder -eq $functionMatch -or $_.folder -eq $privateMatch) - { - write-verbose "$($_.Path) matched on type: Function" - Get-mfScriptDetails -Path $_.Path -RelativePath $relPath -type Function -folderGroup $_.folder - }else{ - write-verbose "$($_.Path) matched on type: Class" - Get-mfScriptDetails -Path $_.Path -RelativePath $relPath -type Class -folderGroup $_.folder - } - - } - - write-verbose 'Return items in Context' - $inContextList =New-Object System.Collections.Generic.List[string] - $filenameReference = @{} - $filenameRelativeReference = @{} - - $itemDetails.foreach{ - $fullPath = $_.path - $relPath = $_.relativePath - write-verbose "Getting details for $($_.name)" - $_.FunctionDetails.Foreach{ - $inContextList.add($_.functionName) - $filenameReference.add($_.functionName,$fullPath) - $filenameRelativeReference.Add($_.functionName,$relPath) - } - $_.ClassDetails.Foreach{ - $inContextList.add($_.className) - $filenameReference.add($_.className,$fullPath) - $filenameRelativeReference.Add($_.className,$relPath) - } - } - $checklist = $filenameReference.GetEnumerator().name - - foreach($item in $itemDetails) - { - write-verbose "Checking dependencies for file: $($item.name)" - $compareList =New-Object System.Collections.Generic.List[string] - $item.ClassDetails.methods.name.foreach{$compareList.add($_)} - $item.FunctionDetails.cmdlets.name.foreach{$compareList.add($_)} - $item.FunctionDetails.types.Name.foreach{$compareList.add($_)} - $item.FunctionDetails.validators.name.foreach{$compareList.add($_)} - $item.FunctionDetails.parameterTypes.name.foreach{$compareList.add($_)} - - $dependenciesList =New-Object System.Collections.Generic.List[object] - - foreach($c in $compareList) - { - write-verbose "Checking dependency of $c" - if($c -in $checklist) - { - write-verbose "$c found in checklist" - if($item.path -ne $filenameReference["$c"]) - { - write-verbose "$c found in checklist" + if($mockPsScriptRoot) + { + write-warning 'Assuming PSScriptRoot from $mockPsScriptRoot. This should only be done for testing' + $resourceFolder = Join-Path $mockPsScriptRoot 'resource' + }else{ + $resourceFolder = Join-Path $PSScriptRoot 'resource' + } - $dependenciesList.add([psCustomObject]@{Reference=$c;ReferenceFile=$filenameRelativeReference["$c"]}) - - }else{ - write-verbose "$c found in checklist - but in same file, ignoring" - } - } - } - - #Add dependencies as an item - $item|add-member -MemberType NoteProperty -Name 'Dependencies' -Value $dependenciesList - $item - } + $scriptBlockPath = Join-Path $resourceFolder 'scriptBlocks' 'Get-MFFolderItemDetails.scriptblock.ps1' + if(!(Test-Path $scriptBlockPath)) + { + throw "Unable to find scriptblock resource at: $scriptBlockPath" } + } - write-verbose 'Getting Folder Items' + process{ + write-verbose 'Loading Scriptblock' + [scriptblock]$sblock = [scriptblock]::Create((Get-Content $scriptBlockPath -Raw)) + write-verbose 'Getting Folder Items' [array]$folders = @('enums','validationClasses','classes','dscClasses','functions','private') $folderItems = $folders.ForEach{ $folderPath = Join-Path $Path -ChildPath $_ @@ -321,6 +105,6 @@ function Get-MFFolderItemDetails $output = Receive-Job -Job $job remove-job -job $job - return $output - } -} \ No newline at end of file + return $output + } +} diff --git a/source/resource/scriptBlocks/Get-MFFolderItemDetails.scriptblock.ps1 b/source/resource/scriptBlocks/Get-MFFolderItemDetails.scriptblock.ps1 new file mode 100644 index 0000000..ceece18 --- /dev/null +++ b/source/resource/scriptBlocks/Get-MFFolderItemDetails.scriptblock.ps1 @@ -0,0 +1,216 @@ +param($Path,$folderItems) + +function Get-MFScriptDetails { + param( + [Parameter(Mandatory)] + [string]$Path, + [Parameter()] + [string]$RelativePath, + [ValidateSet('Class','Function','All')] + [Parameter()] + [string]$Type = 'All', + [Parameter()] + [string]$FolderGroup + ) + begin{ + write-verbose 'Checking Item' + if($Path[-1] -eq '\' -or $Path[-1] -eq '/') + { + write-verbose 'Removing extra \ or / from path' + $Path = $Path.Substring(0,$($Path.length-1)) + write-verbose "New Path $Path" + } + $file = get-item $Path + if(!$file) + { + throw "File not found at: $Path" + } + } + process{ + $AST = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$null, [ref]$null) + + # FindAll returns every function at every nesting depth. + # We only want top-level functions, so we filter out any function whose + # line range is entirely contained within another function's line range. + $AllFunctions = $AST.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true) + $TopLevelFunctions = New-Object System.Collections.Generic.List[Object] + foreach($func in $allFunctions){ + $isNested = $false + foreach($parentFunc in $allFunctions) + { + if($func -ne $parentFunc -and $func.Extent.StartLineNumber -ge $parentFunc.Extent.StartLineNumber -and $func.Extent.EndLineNumber -le $parentFunc.Extent.EndLineNumber) + { + $isNested = $true + break + } + } + if(-not $isNested) { + $TopLevelFunctions.add($func) + } + } + + $Classes = $AST.FindAll({ $args[0] -is [System.Management.Automation.Language.TypeDefinitionAst] }, $true) + if($Type -eq 'All' -or $Type -eq 'Function') + { + $functionDetails = foreach ($Function in $TopLevelFunctions) { + $cmdletDependenciesList = New-Object System.Collections.Generic.List[string] + $TypeDependenciesList = New-Object System.Collections.Generic.List[string] + $paramTypeDependenciesList = New-Object System.Collections.Generic.List[string] + $validatorTypeDependenciesList = New-Object System.Collections.Generic.List[string] + $FunctionName = $Function.Name + $Cmdlets = $Function.FindAll({ $args[0] -is [System.Management.Automation.Language.CommandAst] }, $true) + foreach($c in $Cmdlets) + { + $cmdletDependenciesList.add($c.GetCommandName()) + } + $TypeExpressions = $Function.FindAll({ $args[0] -is [System.Management.Automation.Language.TypeExpressionAst] }, $true) + $TypeExpressions.TypeName.FullName.foreach{ + $tname = $_ + [string]$tnameReplace = $($tname.Replace('[','')).replace(']','') + $TypeDependenciesList.add($tnameReplace) + } + $Parameters = $Function.Body.ParamBlock.Parameters + $Parameters.StaticType.Name.foreach{$paramTypeDependenciesList.add($_)} + $attributes = $Parameters.Attributes + foreach($att in $attributes) + { + $refType = $att.TypeName.GetReflectionType() + if($refType -and ($refType.IsSubclassOf([System.Management.Automation.ValidateArgumentsAttribute]) -or [System.Management.Automation.ValidateArgumentsAttribute].IsAssignableFrom($refType))) { + [string]$tname = $Att.TypeName.FullName + [string]$tname = $($tname.Replace('[','')).replace(']','') + $validatorTypeDependenciesList.Add($tname) + } + } + + [psCustomObject]@{ + functionName = $FunctionName + cmdLets = $cmdletDependenciesList|group-object|Select-Object Name,Count + types = $TypeDependenciesList|group-object|Select-Object Name,Count + parameterTypes = $paramTypeDependenciesList|group-object|Select-Object name,count + Validators = $validatorTypeDependenciesList|Group-Object|Select-Object name,count + } + } + } + if($Type -eq 'all' -or $Type -eq 'Class') + { + $classDetails = foreach ($Class in $Classes) { + $className = $Class.Name + $classMethodsList = New-Object System.Collections.Generic.List[string] + $classPropertiesList = New-Object System.Collections.Generic.List[string] + $Methods = $Class.Members | Where-Object { $_ -is [System.Management.Automation.Language.FunctionMemberAst] } + foreach($m in $Methods) + { + $classMethodsList.add($m.Name) + } + $Properties = $Class.Members | Where-Object { $_ -is [System.Management.Automation.Language.PropertyMemberAst] } + foreach($p in $Properties) + { + $classPropertiesList.add($p.Name) + } + [psCustomObject]@{ + className = $className + methods = $classMethodsList|group-object|Select-Object Name,Count + properties = $classPropertiesList|group-object|Select-Object Name,Count + } + } + } + $objectHash = @{ + Name = $file.Name + Path = $file.FullName + FileSize = "$([math]::round($file.length / 1kb,2)) kb" + FunctionDetails = $functionDetails + ClassDetails = $classDetails + Content = $AST.ToString() + } + if($RelativePath) + { + $objectHash.relativePath = $RelativePath + } + if($FolderGroup) + { + $objectHash.group = $FolderGroup + } + [psCustomObject]$objectHash + } +} + +$privateMatch = "*$([IO.Path]::DirectorySeparatorChar)private$([IO.Path]::DirectorySeparatorChar)*" +$functionMatch = "*$([IO.Path]::DirectorySeparatorChar)functions$([IO.Path]::DirectorySeparatorChar)*" + +$folderItems.ForEach{ + if($_.path -notlike $privateMatch -and $_.path -notlike $functionMatch) + { + # Dot-source non-function files so custom types are reflected correctly + . $_.Path + } +} + +$thisPath = (Get-Item $Path) +$relPathBase = ".$([IO.Path]::DirectorySeparatorChar)$($thisPath.name)" + +$itemDetails = $folderItems.ForEach{ + $folderPath = join-path -path $_.folder -childpath $_.RelativePath.Substring(1) + $relPath = join-path -path $relPathBase -childpath $folderPath + if($_.path -like $privateMatch -or $_.path -like $functionMatch -or $_.folder -eq $functionMatch -or $_.folder -eq $privateMatch) + { + write-verbose "$($_.Path) matched on type: Function" + Get-mfScriptDetails -Path $_.Path -RelativePath $relPath -type Function -folderGroup $_.folder + }else{ + write-verbose "$($_.Path) matched on type: Class" + Get-mfScriptDetails -Path $_.Path -RelativePath $relPath -type Class -folderGroup $_.folder + } +} + +write-verbose 'Return items in Context' +$inContextList = New-Object System.Collections.Generic.List[string] +$filenameReference = @{} +$filenameRelativeReference = @{} + +$itemDetails.foreach{ + $fullPath = $_.path + $relPath = $_.relativePath + write-verbose "Getting details for $($_.name)" + $_.FunctionDetails.Foreach{ + $inContextList.add($_.functionName) + $filenameReference.add($_.functionName,$fullPath) + $filenameRelativeReference.Add($_.functionName,$relPath) + } + $_.ClassDetails.Foreach{ + $inContextList.add($_.className) + $filenameReference.add($_.className,$fullPath) + $filenameRelativeReference.Add($_.className,$relPath) + } +} + +$checklist = $filenameReference.GetEnumerator().name + +foreach($item in $itemDetails) +{ + write-verbose "Checking dependencies for file: $($item.name)" + $compareList = New-Object System.Collections.Generic.List[string] + $item.ClassDetails.methods.name.foreach{$compareList.add($_)} + $item.FunctionDetails.cmdlets.name.foreach{$compareList.add($_)} + $item.FunctionDetails.types.Name.foreach{$compareList.add($_)} + $item.FunctionDetails.validators.name.foreach{$compareList.add($_)} + $item.FunctionDetails.parameterTypes.name.foreach{$compareList.add($_)} + + $dependenciesList = New-Object System.Collections.Generic.List[object] + + foreach($c in $compareList) + { + write-verbose "Checking dependency of $c" + if($c -in $checklist) + { + write-verbose "$c found in checklist" + if($item.path -ne $filenameReference["$c"]) + { + $dependenciesList.add([psCustomObject]@{Reference=$c;ReferenceFile=$filenameRelativeReference["$c"]}) + }else{ + write-verbose "$c found in checklist - but in same file, ignoring" + } + } + } + + $item|add-member -MemberType NoteProperty -Name 'Dependencies' -Value $dependenciesList + $item +} From 86888bf629e3408c9bb95facfa6339ed9c3687b1 Mon Sep 17 00:00:00 2001 From: "adrian.andersson" Date: Wed, 3 Jun 2026 19:46:24 +1000 Subject: [PATCH 3/3] chore: more linting fixes --- source/functions/Get-MFFolderItemDetails.ps1 | 2 +- source/functions/Write-MFModuleDocs.ps1 | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/source/functions/Get-MFFolderItemDetails.ps1 b/source/functions/Get-MFFolderItemDetails.ps1 index 9a84aa1..bbdef41 100644 --- a/source/functions/Get-MFFolderItemDetails.ps1 +++ b/source/functions/Get-MFFolderItemDetails.ps1 @@ -74,7 +74,7 @@ function Get-MFFolderItemDetails $resourceFolder = Join-Path $PSScriptRoot 'resource' } - $scriptBlockPath = Join-Path $resourceFolder 'scriptBlocks' 'Get-MFFolderItemDetails.scriptblock.ps1' + $scriptBlockPath = Join-Path -Path $resourceFolder -ChildPath 'scriptBlocks' -AdditionalChildPath 'Get-MFFolderItemDetails.scriptblock.ps1' if(!(Test-Path $scriptBlockPath)) { throw "Unable to find scriptblock resource at: $scriptBlockPath" diff --git a/source/functions/Write-MFModuleDocs.ps1 b/source/functions/Write-MFModuleDocs.ps1 index 697e4ed..8b11560 100644 --- a/source/functions/Write-MFModuleDocs.ps1 +++ b/source/functions/Write-MFModuleDocs.ps1 @@ -108,8 +108,8 @@ function Write-MFModuleDocs New-Item -ItemType Directory -Path $DocsFullPath } - # Phase 0 — snapshot all existing front matter before any writes. - # Stores a flat key→value dictionary per page so that hand-crafted fields + # Phase 0 - snapshot all existing front matter before any writes. + # Stores a flat key->value dictionary per page so that hand-crafted fields # (nav_order, custom titles, etc.) survive regeneration. Keys are # forward-slash relative paths from DocsFullPath (e.g. 'functions/index.md'). $frontMatterCache = @{} @@ -229,7 +229,7 @@ function Write-MFModuleDocs foreach($link in $funcChildLinks){ $funcIndexLines.Add($link) } $funcIndexLines -join "`n" | Out-File (join-path $functionsFullPath 'index.md') -Force - # Changelog — prepend JTD front matter, restoring nav_order from the snapshot if present + # Changelog - prepend JTD front matter, restoring nav_order from the snapshot if present if($IncludeChangeLog) { write-verbose 'Creating changelog file' @@ -426,14 +426,14 @@ function Write-MFModuleDocs $indexPath = join-path $DocsFullPath 'index.md' if(Test-Path $indexPath) { - # Homepage already exists — only update the version line to preserve the + # Homepage already exists - only update the version line to preserve the # hand-crafted sections table, root-level page links, and section names. $existingIndexContent = Get-Content $indexPath -Raw $updatedIndexContent = $existingIndexContent -replace '(?m)^> Module version:.*$', "> Module version: $moduleVersion" $updatedIndexContent | Out-File $indexPath -Force -NoNewline - write-verbose 'Homepage already exists — updated version line only' + write-verbose 'Homepage already exists - updated version line only' }else{ - # First run — generate the homepage from scratch + # First run - generate the homepage from scratch $indexLines -join "`n" | Out-File $indexPath -Force write-verbose 'Generated new homepage' }