├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ └── validate.yml ├── .gitignore ├── LICENSE ├── README.md ├── Refactor ├── Refactor.psd1 ├── Refactor.psm1 ├── bin │ ├── Refactor.dll │ ├── Refactor.pdb │ ├── Refactor.xml │ └── readme.md ├── changelog.md ├── en-us │ ├── about_Refactor.help.txt │ └── strings.psd1 ├── functions │ ├── BreakingChange │ │ ├── Clear-ReBreakingChange.ps1 │ │ ├── Get-ReBreakingChange.ps1 │ │ ├── Import-ReBreakingChange.ps1 │ │ ├── Register-ReBreakingChange.ps1 │ │ └── Search-ReBreakingChange.ps1 │ ├── Clear-ReTokenTransformationSet.ps1 │ ├── Convert-ReScriptFile.ps1 │ ├── Convert-ReScriptToken.ps1 │ ├── Get-ReScriptFile.ps1 │ ├── Get-ReSplat.ps1 │ ├── Get-ReToken.ps1 │ ├── Get-ReTokenProvider.ps1 │ ├── Get-ReTokenTransformationSet.ps1 │ ├── Import-ReTokenTransformationSet.ps1 │ ├── New-ReToken.ps1 │ ├── Read-ReAst.ps1 │ ├── Read-ReAstComponent.ps1 │ ├── Read-ReScriptCommand.ps1 │ ├── Register-ReTokenProvider.ps1 │ ├── Register-ReTokenTransformation.ps1 │ ├── Search-ReAst.ps1 │ ├── Test-ReSyntax.ps1 │ ├── Write-ReAstComponent.ps1 │ └── readme.md ├── internal │ ├── configurations │ │ ├── configuration.ps1 │ │ └── readme.md │ ├── functions │ │ ├── Find-BreakingChange.ps1 │ │ ├── Get-AstCommand.ps1 │ │ └── readme.md │ ├── scriptblocks │ │ └── scriptblocks.ps1 │ ├── scripts │ │ ├── license.ps1 │ │ ├── postimport.ps1 │ │ ├── preimport.ps1 │ │ ├── strings.ps1 │ │ └── variables.ps1 │ ├── tepp │ │ ├── assignment.ps1 │ │ ├── astTypes.tepp.ps1 │ │ ├── provider.tepp.ps1 │ │ └── readme.md │ └── tokenProvider │ │ ├── ast.token.ps1 │ │ ├── command.token.ps1 │ │ └── function.token.ps1 ├── readme.md ├── tests │ ├── functions │ │ └── readme.md │ ├── general │ │ ├── FileIntegrity.Exceptions.ps1 │ │ ├── FileIntegrity.Tests.ps1 │ │ ├── Help.Exceptions.ps1 │ │ ├── Help.Tests.ps1 │ │ ├── Manifest.Tests.ps1 │ │ ├── PSScriptAnalyzer.Tests.ps1 │ │ ├── strings.Exceptions.ps1 │ │ └── strings.Tests.ps1 │ ├── pester.ps1 │ └── readme.md └── xml │ ├── Refactor.Format.ps1xml │ ├── Refactor.Types.ps1xml │ └── readme.md ├── azFunctionResources ├── clientModule │ ├── function.ps1 │ ├── functions │ │ └── Connect-Refactor.ps1 │ ├── internal │ │ ├── configurations │ │ │ └── connection.ps1 │ │ └── functions │ │ │ └── Get-InternalConnectionData.ps1 │ └── moduleroot.psm1 ├── functionOverride │ ├── Get-Example.json │ ├── Get-Example.ps1 │ └── Get-Example.psd1 ├── host-az.json ├── host.json ├── local.settings.json ├── profile.ps1 ├── profileFunctions │ ├── Convert-AzureFunctionParameter.ps1 │ └── Write-AzureFunctionOutput.ps1 ├── readme.md ├── requirements.psd1 └── run.ps1 ├── azure-pipelines.yml ├── build ├── AzureFunction.readme.md ├── vsts-build.ps1 ├── vsts-createFunctionClientModule.ps1 ├── vsts-packageFunction.ps1 ├── vsts-prerequisites.ps1 └── vsts-validate.ps1 ├── docs ├── breakingChange │ └── BeerFactory.break.psd1 └── transforms │ ├── command.transform.psd1 │ └── readme.md ├── install.ps1 ├── library └── Refactor │ ├── Refactor.sln │ └── Refactor │ ├── AstToken.cs │ ├── BreakingChange.cs │ ├── Change.cs │ ├── CommandToken.cs │ ├── Component │ ├── AstResult.cs │ ├── ScriptFileConverted.cs │ └── ScriptResult.cs │ ├── FunctionToken.cs │ ├── GenericToken.cs │ ├── Host.cs │ ├── Message.cs │ ├── MessageType.cs │ ├── PSHelp.cs │ ├── PSHelpExample.cs │ ├── Refactor.csproj │ ├── ScriptFile.cs │ ├── ScriptToken.cs │ ├── SearchResult.cs │ ├── Splat.cs │ ├── TokenProvider.cs │ ├── TransformationResult.cs │ └── TransformationResultEntry.cs └── testfiles ├── breakingChange └── beer.ps1 ├── commands.ps1 └── help.ps1 /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | 6 | jobs: 7 | build: 8 | 9 | runs-on: windows-2019 10 | 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: Install Prerequisites 14 | run: .\build\vsts-prerequisites.ps1 15 | shell: powershell 16 | - name: Validate 17 | run: .\build\vsts-validate.ps1 18 | shell: powershell 19 | - name: Build 20 | run: .\build\vsts-build.ps1 -ApiKey $env:APIKEY 21 | shell: powershell 22 | env: 23 | APIKEY: ${{ secrets.ApiKey }} 24 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | on: [pull_request] 2 | 3 | jobs: 4 | validate: 5 | 6 | runs-on: windows-2019 7 | 8 | steps: 9 | - uses: actions/checkout@v1 10 | - name: Install Prerequisites 11 | run: .\build\vsts-prerequisites.ps1 12 | shell: powershell 13 | - name: Validate 14 | run: .\build\vsts-validate.ps1 15 | shell: powershell 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 |  2 | # ignore the settings folder and files for VSCode and PSS 3 | .vscode/* 4 | *.psproj 5 | *TempPoint* 6 | 7 | # Ignore staging info from Visual Studio 8 | library/Refactor/.vs/* 9 | library/Refactor/Refactor/bin/* 10 | library/Refactor/Refactor/obj/* 11 | 12 | # ignore PowerShell Studio MetaData 13 | Refactor/Refactor.psproj 14 | Refactor/Refactor.psproj.bak 15 | Refactor/Refactor.psprojs 16 | Refactor/Refactor.psproj 17 | 18 | # ignore the TestResults 19 | TestResults/* 20 | 21 | # ignore the publishing Directory 22 | publish/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Friedrich Weinmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Refactor 2 | 3 | Welcome to the Refactor project. 4 | This module is designed to provide an infrastructure for code analysis and refactoring your code. 5 | 6 | Intended use-cases: 7 | 8 | + Bulk Code Scanning 9 | + Code Migration advisor 10 | + Code Migration automation 11 | + Code Maintenance automation 12 | 13 | ## Installing 14 | 15 | To install the module from the PSGallery, run this command: 16 | 17 | ```powershell 18 | Install-Module Refactor 19 | ``` 20 | 21 | ## Getting Started 22 | 23 | > Processing command usage 24 | 25 | To rename a command, or the parameters used, or maybe just inform on them, you first need to provide a "Transform File". 26 | That is, a `.psd1` or `.json` document describing what you want to change or report upon. 27 | 28 | [Here is an example file to use as reference](https://github.com/FriedrichWeinmann/Refactor/blob/master/docs/transforms/command.transform.psd1) 29 | 30 | You can then apply the example file against your scripts in bulk. 31 | To simulate this - and demonstrate the example Transform File in action - [there is also an example script to run this against.](https://github.com/FriedrichWeinmann/Refactor/blob/master/testfiles/commands.ps1) 32 | 33 | To apply this, run the following: 34 | 35 | ```powershell 36 | Import-ReTokenTransformationSet -Path .\command.transform.psd1 37 | Get-ChildItem C:\scripts | Convert-ReScriptFile -Backup # -Backup creates a backup file before modifying the existing one 38 | ``` 39 | 40 | ## Concepts 41 | 42 | This toolkit is freely extensible and intended for both analysis and transformation. 43 | In order to process this in a structured manner, there are several key concepts: 44 | 45 | + Token 46 | + Token Provider 47 | + Transform 48 | 49 | > Token 50 | 51 | A token represents a code element. 52 | It may be a single piece of text in the code scanned, or several associated ones: 53 | 54 | + A single command invocation 55 | + A single command invocation, the declaration of a hashtable used to splat 56 | + All instances of a specific variable 57 | 58 | A token is associated with a single Token Provider, multiple tokens may describe the same piece of code, or partially overlap. 59 | 60 | Tokens are implemented as a class in C#, extending the abstract [ScriptToken](https://github.com/FriedrichWeinmann/Refactor/blob/master/library/Refactor/Refactor/ScriptToken.cs) class. 61 | There is however a [Generic Token Class](https://github.com/FriedrichWeinmann/Refactor/blob/master/library/Refactor/Refactor/GenericToken.cs) that can be reused, avoiding the need to implement your own class in return for less flexibility. 62 | Use the `New-ReToken` command to generate a new generic token. 63 | 64 | > Token Provider 65 | 66 | A Token Provider is a set of tools used to process tokens. 67 | At its core, it contains two pieces of code: 68 | 69 | + Logic to parse an Abstract Syntax Tree to generate tokens 70 | + Logic to react to those tokens, reporting on them or converting/transforming them 71 | 72 | For example, the [Command Provider](https://github.com/FriedrichWeinmann/Refactor/blob/master/Refactor/internal/tokenProvider/command.token.ps1) was implemented to scan and change how a command is called, renaming the command and its parameters. 73 | It will also rename properties on splats, if possible. 74 | 75 | > Transform 76 | 77 | A transform, is a set of instructions to a Token Provider. 78 | They are then matched against the relevant Token, which is implemented in the Token Provider. 79 | 80 | Here is an [example file for reference](https://github.com/FriedrichWeinmann/Refactor/blob/master/docs/transforms/command.transform.psd1). 81 | 82 | The format for Transform Files is predefined: 83 | 84 | ```powershell 85 | @{ 86 | # Must be included in all files. The version notation allows avoiding breaking changes in future updates 87 | Version = 1 88 | 89 | # The token provider to use. 90 | Type = 'Command' 91 | 92 | # The actual entries to process. This is where we place the individual transformation rules 93 | Content = @{ 94 | # Add individual entries here 95 | } 96 | } 97 | ``` 98 | 99 | The structure of the individual entries under Content - which properties are supported and which not - is also defined in the Token provider. 100 | -------------------------------------------------------------------------------- /Refactor/Refactor.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | # Script module or binary module file associated with this manifest 3 | RootModule = 'Refactor.psm1' 4 | 5 | # Version number of this module. 6 | ModuleVersion = '1.2.25' 7 | 8 | # ID used to uniquely identify this module 9 | GUID = '505d7400-5106-4643-9c64-c8e430fa0032' 10 | 11 | # Author of this module 12 | Author = 'Friedrich Weinmann' 13 | 14 | # Company or vendor of this module 15 | CompanyName = ' ' 16 | 17 | # Copyright statement for this module 18 | Copyright = 'Copyright (c) 2022 Friedrich Weinmann' 19 | 20 | # Description of the functionality provided by this module 21 | Description = 'PowerShell Code refactoring framework' 22 | 23 | # Minimum version of the Windows PowerShell engine required by this module 24 | PowerShellVersion = '5.1' 25 | 26 | # Modules that must be imported into the global environment prior to importing 27 | # this module 28 | RequiredModules = @( 29 | @{ ModuleName = 'PSFramework'; ModuleVersion = '1.6.214' } 30 | ) 31 | 32 | # Assemblies that must be loaded prior to importing this module 33 | RequiredAssemblies = @('bin\Refactor.dll') 34 | 35 | # Type files (.ps1xml) to be loaded when importing this module 36 | # TypesToProcess = @('xml\Refactor.Types.ps1xml') 37 | 38 | # Format files (.ps1xml) to be loaded when importing this module 39 | FormatsToProcess = @('xml\Refactor.Format.ps1xml') 40 | 41 | # Functions to export from this module 42 | FunctionsToExport = @( 43 | 'Clear-ReBreakingChange' 44 | 'Clear-ReTokenTransformationSet' 45 | 'Convert-ReScriptFile' 46 | 'Convert-ReScriptToken' 47 | 'Get-ReBreakingChange' 48 | 'Get-ReScriptFile' 49 | 'Get-ReSplat' 50 | 'Get-ReToken' 51 | 'Get-ReTokenProvider' 52 | 'Get-ReTokenTransformationSet' 53 | 'Import-ReBreakingChange' 54 | 'Import-ReTokenTransformationSet' 55 | 'New-ReToken' 56 | 'Read-ReAst' 57 | 'Read-ReAstComponent' 58 | 'Read-ReScriptCommand' 59 | 'Register-ReBreakingChange' 60 | 'Register-ReTokenProvider' 61 | 'Register-ReTokenTransformation' 62 | 'Search-ReAst' 63 | 'Search-ReBreakingChange' 64 | 'Test-ReSyntax' 65 | 'Write-ReAstComponent' 66 | ) 67 | 68 | # Cmdlets to export from this module 69 | # CmdletsToExport = '' 70 | 71 | # Variables to export from this module 72 | # VariablesToExport = '' 73 | 74 | # Aliases to export from this module 75 | # AliasesToExport = '' 76 | 77 | # List of all modules packaged with this module 78 | ModuleList = @() 79 | 80 | # List of all files packaged with this module 81 | FileList = @() 82 | 83 | # Private data to pass to the module specified in ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 84 | PrivateData = @{ 85 | 86 | #Support for PowerShellGet galleries. 87 | PSData = @{ 88 | 89 | # Tags applied to this module. These help with module discovery in online galleries. 90 | Tags = @('code', 'refactor') 91 | 92 | # A URL to the license for this module. 93 | LicenseUri = 'https://github.com/FriedrichWeinmann/Refactor/blob/master/LICENSE' 94 | 95 | # A URL to the main website for this project. 96 | ProjectUri = 'https://github.com/FriedrichWeinmann/Refactor' 97 | 98 | # A URL to an icon representing this module. 99 | # IconUri = '' 100 | 101 | # ReleaseNotes of this module 102 | ReleaseNotes = 'https://github.com/FriedrichWeinmann/Refactor/blob/master/Refactor/changelog.md' 103 | 104 | } # End of PSData hashtable 105 | 106 | } # End of PrivateData hashtable 107 | } -------------------------------------------------------------------------------- /Refactor/Refactor.psm1: -------------------------------------------------------------------------------- 1 | $script:ModuleRoot = $PSScriptRoot 2 | $script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\Refactor.psd1").ModuleVersion 3 | 4 | # Detect whether at some level dotsourcing was enforced 5 | $script:doDotSource = Get-PSFConfigValue -FullName Refactor.Import.DoDotSource -Fallback $false 6 | if ($Refactor_dotsourcemodule) { $script:doDotSource = $true } 7 | 8 | <# 9 | Note on Resolve-Path: 10 | All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator. 11 | This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS. 12 | Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist. 13 | This is important when testing for paths. 14 | #> 15 | 16 | # Detect whether at some level loading individual module files, rather than the compiled module was enforced 17 | $importIndividualFiles = Get-PSFConfigValue -FullName Refactor.Import.IndividualFiles -Fallback $false 18 | if ($Refactor_importIndividualFiles) { $importIndividualFiles = $true } 19 | if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true } 20 | if ("" -eq '') { $importIndividualFiles = $true } 21 | 22 | function Import-ModuleFile 23 | { 24 | <# 25 | .SYNOPSIS 26 | Loads files into the module on module import. 27 | 28 | .DESCRIPTION 29 | This helper function is used during module initialization. 30 | It should always be dotsourced itself, in order to proper function. 31 | 32 | This provides a central location to react to files being imported, if later desired 33 | 34 | .PARAMETER Path 35 | The path to the file to load 36 | 37 | .EXAMPLE 38 | PS C:\> . Import-ModuleFile -File $function.FullName 39 | 40 | Imports the file stored in $function according to import policy 41 | #> 42 | [CmdletBinding()] 43 | Param ( 44 | [string] 45 | $Path 46 | ) 47 | 48 | $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath 49 | if ($doDotSource) { . $resolvedPath } 50 | else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) } 51 | } 52 | 53 | #region Load individual files 54 | if ($importIndividualFiles) 55 | { 56 | # Execute Preimport actions 57 | foreach ($path in (& "$ModuleRoot\internal\scripts\preimport.ps1")) { 58 | . Import-ModuleFile -Path $path 59 | } 60 | 61 | # Import all internal functions 62 | foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) 63 | { 64 | . Import-ModuleFile -Path $function.FullName 65 | } 66 | 67 | # Import all public functions 68 | foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) 69 | { 70 | . Import-ModuleFile -Path $function.FullName 71 | } 72 | 73 | # Execute Postimport actions 74 | foreach ($path in (& "$ModuleRoot\internal\scripts\postimport.ps1")) { 75 | . Import-ModuleFile -Path $path 76 | } 77 | 78 | # End it here, do not load compiled code below 79 | return 80 | } 81 | #endregion Load individual files 82 | 83 | #region Load compiled code 84 | "" 85 | #endregion Load compiled code -------------------------------------------------------------------------------- /Refactor/bin/Refactor.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriedrichWeinmann/Refactor/d58810100896757d92c1f1ddf12824c14ed3e901/Refactor/bin/Refactor.dll -------------------------------------------------------------------------------- /Refactor/bin/Refactor.pdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriedrichWeinmann/Refactor/d58810100896757d92c1f1ddf12824c14ed3e901/Refactor/bin/Refactor.pdb -------------------------------------------------------------------------------- /Refactor/bin/readme.md: -------------------------------------------------------------------------------- 1 | # bin folder 2 | 3 | The bin folder exists to store binary data. And scripts related to the type system. 4 | 5 | This may include your own C#-based library, third party libraries you want to include (watch the license!), or a script declaring type accelerators (effectively aliases for .NET types) 6 | 7 | For more information on Type Accelerators, see the help on Set-PSFTypeAlias -------------------------------------------------------------------------------- /Refactor/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.2.25 (2023-06-12) 4 | 5 | + Fix: Test-ReSyntax - returns always true when providing direct code as string. 6 | 7 | ## 1.2.24 (2023-06-05) 8 | 9 | + Fix: Write-ReAstComponent - fixed change detection 10 | 11 | ## 1.2.23 (2023-06-05) 12 | 13 | + Fix: Test-ReSyntax - returns wrong result 14 | 15 | ## 1.2.22 (2023-05-31) 16 | 17 | + New: Read-ReAstComponent - search for ast elements of a given type. Intended for simplified code updates. 18 | + New: Test-ReSyntax - tests the syntax of the specified script or code 19 | + New: Write-ReAstComponent - Updates a scriptfile that was read from using Read-ReAstComponent. 20 | + New: Token Provider: Ast - generic Ast object token scanner, designed for manual conversion logic. 21 | 22 | ## 1.1.18 (2023-04-12) 23 | 24 | + Fix: Import-ReTokenTransformationSet - silently fails to load json files 25 | 26 | ## 1.1.17 (2022-11-18) 27 | 28 | + Fix: Function Provider - "StartIndex cannot be less than zero" 29 | 30 | ## 1.1.16 (2022-11-17) 31 | 32 | + New: Function Provider - Detects function definitions and allows renaming them, including examples in the help. 33 | 34 | ## 1.1.15 (2022-06-15) 35 | 36 | + Upd: Class: ScriptFile - added WriteTo method to support exporting to a path other than the source file 37 | + Upd: Convert-ReScriptFile - added `-OutPath` parameter to support writing the converted files to another folder 38 | + Fix: Get-ReScriptFile - fails to create over an empty file-content 39 | 40 | ## 1.1.12 (2022-04-15) 41 | 42 | + Fix: Get-ReScriptFile - generates error when not providing a path 43 | 44 | ## 1.1.11 (2022-04-14) 45 | 46 | + Upd: Get-ReScriptFile - added option to specify name and scriptcode, rather than being limited to reading from file. 47 | + Upd: CommandToken - will display line rather than offset by default 48 | + Upd: ScriptFile.cs - added ability to define script files from their text content, rather than just their path, in order to support working with non-file based services (scanning code retrieved from an API, ...) 49 | 50 | ## 1.1.8 (2022-04-11) 51 | 52 | + New: Component: Breaking Change - Scan script files for breaking changes 53 | + New: Command Get-ReToken - Scans a script file for all tokens contained within. 54 | + Upd: Added "Line" property to all tokens 55 | + Upd: Get-ReTokenProvider - `-Name` parameter now accepts an array of values 56 | + Upd: ScriptFile - GetTokens() now supports specifying a list of providers to process 57 | + Upd: Convert-ReScriptFile - implements ShouldProcess 58 | + Upd: Convert-ReScriptFile - added `-ProviderName` parameter to support filtering by provider name 59 | + Upd: Read-ReAst - changed `-ScriptCode` parameter to expect a string as input 60 | 61 | ## 1.0.0 (2022-03-06) 62 | 63 | + Initial Release 64 | -------------------------------------------------------------------------------- /Refactor/en-us/about_Refactor.help.txt: -------------------------------------------------------------------------------- 1 | TOPIC 2 | about_Refactor 3 | 4 | SHORT DESCRIPTION 5 | Explains how to use the Refactor powershell module 6 | 7 | LONG DESCRIPTION 8 | 9 | 10 | KEYWORDS 11 | Refactor -------------------------------------------------------------------------------- /Refactor/en-us/strings.psd1: -------------------------------------------------------------------------------- 1 | # This is where the strings go, that are written by 2 | # Write-PSFMessage, Stop-PSFFunction or the PSFramework validation scriptblocks 3 | @{ 4 | 'key' = 'Value' 5 | } -------------------------------------------------------------------------------- /Refactor/functions/BreakingChange/Clear-ReBreakingChange.ps1: -------------------------------------------------------------------------------- 1 | function Clear-ReBreakingChange { 2 | <# 3 | .SYNOPSIS 4 | Removes entire datasets of entries from the list of registered breaking changes. 5 | 6 | .DESCRIPTION 7 | Removes entire datasets of entries from the list of registered breaking changes. 8 | 9 | .PARAMETER Module 10 | The module to unregister. 11 | 12 | .PARAMETER Version 13 | The version of the module to unregister. 14 | If not specified, ALL versions are unregistered. 15 | 16 | .EXAMPLE 17 | PS C:\> Clear-ReBreakingChange -Module MyModule 18 | 19 | Removes all breaking changes of all versions of "MyModule" from the in-memory configuration set. 20 | #> 21 | [CmdletBinding()] 22 | param ( 23 | [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] 24 | [string] 25 | $Module, 26 | 27 | [Parameter(ValueFromPipelineByPropertyName = $true)] 28 | [Version] 29 | $Version 30 | ) 31 | 32 | process { 33 | if (-not $script:breakingChanges[$Module]) { return } 34 | if (-not $Version) { 35 | $script:breakingChanges.Remove($Module) 36 | return 37 | } 38 | $script:breakingChanges[$Module].Remove($Version) 39 | if ($script:breakingChanges[$Module].Count -lt 1) { 40 | $script:breakingChanges.Remove($Module) 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Refactor/functions/BreakingChange/Get-ReBreakingChange.ps1: -------------------------------------------------------------------------------- 1 | function Get-ReBreakingChange { 2 | <# 3 | .SYNOPSIS 4 | Searches for a breaking change configuration entry that has previously registered. 5 | 6 | .DESCRIPTION 7 | Searches for a breaking change configuration entry that has previously registered. 8 | 9 | .PARAMETER Module 10 | The module to search by. 11 | Defaults to '*' 12 | 13 | .PARAMETER Version 14 | The version of the module to search for. 15 | By default, changes for all versions are returned. 16 | 17 | .PARAMETER Command 18 | The affected command to search for. 19 | Defaults to '*' 20 | 21 | .PARAMETER Tags 22 | Only include changes that contain at least one of the listed tags. 23 | 24 | .EXAMPLE 25 | PS C:\> Get-ReBreakingChange 26 | 27 | Returns all registered breaking change configuration entries. 28 | #> 29 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] 30 | [CmdletBinding()] 31 | param ( 32 | [Parameter(ValueFromPipelineByPropertyName = $true)] 33 | [string] 34 | $Module = '*', 35 | 36 | [Parameter(ValueFromPipelineByPropertyName = $true)] 37 | [Version] 38 | $Version, 39 | 40 | [Parameter(ValueFromPipelineByPropertyName = $true)] 41 | [string] 42 | $Command = '*', 43 | 44 | [Parameter(ValueFromPipelineByPropertyName = $true)] 45 | [AllowEmptyCollection()] 46 | [string[]] 47 | $Tags 48 | ) 49 | 50 | process { 51 | $script:breakingChanges.Values.Values | Write-Output | Where-Object { 52 | if ($_.Module -notlike $Module) { return } 53 | if ($_.Command -notlike $Command) { return } 54 | if ($Version -and $_.Version -ne $Version) { return } 55 | if ($Tags -and -not ($_.Tags | Where-Object { $_ -in $Tags })) { return } 56 | $true 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /Refactor/functions/BreakingChange/Import-ReBreakingChange.ps1: -------------------------------------------------------------------------------- 1 | function Import-ReBreakingChange { 2 | <# 3 | .SYNOPSIS 4 | Imports a set of Breaking Change configurations from file. 5 | 6 | .DESCRIPTION 7 | Imports a set of Breaking Change configurations from file. 8 | Expects a PowerShell Document File (.psd1) 9 | 10 | Example layout of import file: 11 | 12 | @{ 13 | MyModule = @{ 14 | '2.0.0' = @{ 15 | 'Get-Something' = @{ 16 | Description = 'Command was fully redesigned' 17 | } 18 | 'Get-SomethingElse' = @{ 19 | Parameters @{ 20 | Param1 = 'Parameter was dropped' 21 | Param2 = 'Accepts string only now and will not try to parse custom objects anymore' 22 | Param3 = 'Was renamed to Param4' 23 | } 24 | Labels = @('primary') 25 | } 26 | } 27 | } 28 | } 29 | 30 | .PARAMETER Path 31 | Path to the file(s) to import. 32 | 33 | .PARAMETER EnableException 34 | Replaces user friendly yellow warnings with bloody red exceptions of doom! 35 | Use this if you want the function to throw terminating errors you want to catch. 36 | 37 | .EXAMPLE 38 | PS C:\> Import-ReBreakingChange -Path .\mymodule.break.psd1 39 | 40 | Imports the mymodule.break.psd1 41 | #> 42 | [CmdletBinding()] 43 | param ( 44 | [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'File')] 45 | [PsfValidateScript('PSFramework.Validate.FSPath.File', ErrorString = 'PSFramework.Validate.FSPath.File')] 46 | [Alias('FullName')] 47 | [string[]] 48 | $Path, 49 | 50 | [switch] 51 | $EnableException 52 | ) 53 | 54 | process { 55 | foreach ($file in $Path | Resolve-PSFPath) { 56 | $dataSet = Import-PSFPowerShellDataFile -Path $file 57 | 58 | foreach ($module in $dataSet.Keys) { 59 | foreach ($version in $dataSet.$module.Keys) { 60 | if (-not ($version -as [version])) { 61 | Stop-PSFFunction -Message "Invalid Version node $($version) for module $($module). Ensure it is a valid version number, prerelease version notations are not supported!" -EnableException $EnableException -Continue -Cmdlet $PSCmdlet 62 | } 63 | foreach ($command in $dataSet.$module.$version.Keys) { 64 | $commandData = $dataSet.$module.$version.$command 65 | 66 | $param = @{ 67 | Module = $module 68 | Version = $version 69 | Command = $command 70 | } 71 | if ($commandData.Description) { $param.Description = $commandData.Description } 72 | if ($commandData.Parameters) { $param.Parameters = $commandData.Parameters } 73 | if ($commandData.Tags) { $param.Tags = $commandData.Tags } 74 | 75 | Register-ReBreakingChange @param 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /Refactor/functions/BreakingChange/Register-ReBreakingChange.ps1: -------------------------------------------------------------------------------- 1 | function Register-ReBreakingChange { 2 | <# 3 | .SYNOPSIS 4 | Register a breaking change. 5 | 6 | .DESCRIPTION 7 | Register a breaking change. 8 | A breaking change is a definition of a command or its parameters that were broken at a given version of the module. 9 | This can include tags to classify the breaking change. 10 | 11 | .PARAMETER Module 12 | The name of the module the breaking change occured in. 13 | 14 | .PARAMETER Version 15 | The version of the module in which the breaking change was applied. 16 | 17 | .PARAMETER Command 18 | The command that was changed in a breaking manner. 19 | 20 | .PARAMETER Description 21 | A description to show when reporting the command itself as being broken. 22 | This is the message shown in the report when finding this breaking change, so make sure it contains actionable information for the user. 23 | 24 | .PARAMETER Parameters 25 | A hashtable containing parameters that were broken, maping parametername to a description of what was changed. 26 | That description will be shown to the user, so make sure it contains actionable information. 27 | Defining parameters will cause the command to only generate scan results when the parameter is being used or the total parameters cannot be determined. 28 | It is possible to assign multiple breaking changes to the same command - one for the command and one for parameters. 29 | 30 | .PARAMETER Tags 31 | Any tags to assign to the breaking change. 32 | Breaking Change scans can be filtered by tags. 33 | 34 | .EXAMPLE 35 | PS C:\> Register-ReBreakingChange -Module MyModule -Version 2.0.0 -Command Get-Something -Description 'Redesigned command' 36 | 37 | Adds a breaking change for the Get-Something command in the module MyModule at version 2.0.0 38 | #> 39 | [CmdletBinding()] 40 | param ( 41 | [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] 42 | [string] 43 | $Module, 44 | 45 | [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] 46 | [Version] 47 | $Version, 48 | 49 | [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] 50 | [string] 51 | $Command, 52 | 53 | [Parameter(ValueFromPipelineByPropertyName = $true)] 54 | [string] 55 | $Description, 56 | 57 | [Parameter(ValueFromPipelineByPropertyName = $true)] 58 | [Hashtable] 59 | $Parameters = @{ }, 60 | 61 | [Parameter(ValueFromPipelineByPropertyName = $true)] 62 | [string[]] 63 | $Tags = @() 64 | ) 65 | 66 | process { 67 | if (-not $script:breakingChanges[$Module]) { 68 | $script:breakingChanges[$Module] = @{ } 69 | } 70 | 71 | if (-not $script:breakingChanges[$Module][$Version]) { 72 | $script:breakingChanges[$Module][$Version] = [System.Collections.Generic.List[object]]::new() 73 | } 74 | 75 | $object = [PSCustomObject]@{ 76 | Module = $Module 77 | Version = $Version 78 | Command = $Command 79 | Description = $Description 80 | Parameters = $Parameters 81 | Tags = $Tags 82 | } 83 | 84 | $script:breakingChanges[$Module][$Version].Add($object) 85 | } 86 | } -------------------------------------------------------------------------------- /Refactor/functions/BreakingChange/Search-ReBreakingChange.ps1: -------------------------------------------------------------------------------- 1 | function Search-ReBreakingChange { 2 | <# 3 | .SYNOPSIS 4 | Search script files for breaking changes. 5 | 6 | .DESCRIPTION 7 | Search script files for breaking changes. 8 | Use Import-ReBreakingChange or Register-ReBreakingChange to define which command was broken in what module and version. 9 | 10 | .PARAMETER Path 11 | Path to the file(s) to scan. 12 | 13 | .PARAMETER Content 14 | Script Content to scan. 15 | 16 | .PARAMETER Name 17 | Name of the scanned content 18 | 19 | .PARAMETER Module 20 | The module(s) to scan for. 21 | This can be either a name (and then use the version definitions from -FromVersion and -ToVersion parameters), 22 | or a Hashtable with three keys: Name, FromVersion and ToVersion. 23 | Example inputs: 24 | 25 | MyModule 26 | @{ Name = 'MyModule'; FromVersion = '1.0.0'; ToVersion = '2.0.0' } 27 | 28 | .PARAMETER FromVersion 29 | The version of the module for which the script was written. 30 | 31 | .PARAMETER ToVersion 32 | The version of the module to which the script is being migrated 33 | 34 | .PARAMETER Tags 35 | Only include breaking changes that include one of these tags. 36 | This allows targeting a specific subset of breaking changes. 37 | 38 | .EXAMPLE 39 | PS C:\> Get-ChildItem -Path C:\scripts -Recurse -Filter *.ps1 | Search-ReBreakingChange -Module Az -FromVersion 5.0 -ToVersion 7.0 40 | 41 | Return all breaking changes in all scripts between Az v5.0 and v7.0. 42 | Requires a breaking change definition file for the Az Modules to be registered, in order to work. 43 | #> 44 | [CmdletBinding(DefaultParameterSetName = 'File')] 45 | param ( 46 | [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'File')] 47 | [PsfValidateScript('PSFramework.Validate.FSPath.File', ErrorString = 'PSFramework.Validate.FSPath.File')] 48 | [Alias('FullName')] 49 | [string[]] 50 | $Path, 51 | 52 | [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Content')] 53 | [string] 54 | $Content, 55 | 56 | [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Content')] 57 | [string] 58 | $Name, 59 | 60 | [Parameter(Mandatory = $true)] 61 | [object[]] 62 | $Module, 63 | 64 | [Version] 65 | $FromVersion, 66 | 67 | [Version] 68 | $ToVersion, 69 | 70 | [string[]] 71 | $Tags = @() 72 | ) 73 | 74 | begin { 75 | #region Collect Changes to apply 76 | $changeObjects = foreach ($moduleItem in $Module) { 77 | $fromV = $FromVersion 78 | $toV = $ToVersion 79 | if ($moduleItem.FromVersion) { $fromV = $moduleItem.FromVersion } 80 | if ($moduleItem.ToVersion) { $toV = $moduleItem.ToVersion } 81 | $moduleName = $moduleItem.Name 82 | if (-not $moduleName) { $moduleName = $moduleItem.ModuleName } 83 | if (-not $moduleName) { $moduleName = $moduleItem -as [string] } 84 | 85 | if (-not $fromV) { Write-PSFMessage -Level Warning -Message "Unable to identify the starting version from which the module $moduleItem is being migrated! be sure to specify the '-FromVersion' parameter." -Target $moduleItem } 86 | if (-not $toV) { Write-PSFMessage -Level Warning -Message "Unable to identify the destination version from which the module $moduleItem is being migrated! be sure to specify the '-ToVersion' parameter." -Target $moduleItem } 87 | if (-not $fromV) { Write-PSFMessage -Level Warning -Message "Unable to identify the name of the module being migrated! Be sure to specify a legitimate name to the '-Module' parameter." -Target $moduleItem } 88 | if (-not ($fromV -and $toV -and $moduleName)) { 89 | Stop-PSFFunction -Message "Failed to resolve the migration metadata - provide a module, the source and the destination version number!" -EnableException $true -Cmdlet $PSCmdlet 90 | } 91 | 92 | Get-ReBreakingChange -Module $moduleName -Tags $Tags | Where-Object { 93 | $fromV -lt $_.Version -and 94 | $toV -ge $_.Version 95 | } 96 | } 97 | $changes = @{ } 98 | foreach ($group in $changeObjects | Group-Object Command) { 99 | $changes[$group.Name] = $group.Group 100 | } 101 | #endregion Collect Changes to apply 102 | } 103 | process { 104 | switch ($PSCmdlet.ParameterSetName) { 105 | File { 106 | foreach ($filePath in $Path) { 107 | $ast = Read-ReAst -Path $filePath 108 | Find-BreakingChange -Ast $ast.Ast -Changes $changes 109 | } 110 | } 111 | Content { 112 | $ast = Read-ReAst -ScriptCode $Content 113 | Find-BreakingChange -Ast $ast.Ast -Name $Name -Changes $changes 114 | } 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /Refactor/functions/Clear-ReTokenTransformationSet.ps1: -------------------------------------------------------------------------------- 1 | function Clear-ReTokenTransformationSet { 2 | <# 3 | .SYNOPSIS 4 | Remove all registered transformation sets. 5 | 6 | .DESCRIPTION 7 | Remove all registered transformation sets. 8 | 9 | .EXAMPLE 10 | PS C:\> Clear-ReTokenTransformationSet 11 | 12 | Removes all registered transformation sets. 13 | #> 14 | [CmdletBinding()] 15 | Param ( 16 | 17 | ) 18 | 19 | process { 20 | $script:tokenTransformations = @{ } 21 | } 22 | } -------------------------------------------------------------------------------- /Refactor/functions/Convert-ReScriptFile.ps1: -------------------------------------------------------------------------------- 1 | function Convert-ReScriptFile 2 | { 3 | <# 4 | .SYNOPSIS 5 | Perform AST-based replacement / refactoring of scriptfiles 6 | 7 | .DESCRIPTION 8 | Perform AST-based replacement / refactoring of scriptfiles 9 | This process depends on two factors: 10 | + Token Provider 11 | + Token Transformation Sets 12 | 13 | The provider is a plugin that performs the actual AST analysis and replacement. 14 | For example, by default the "Command" provider allows renaming commands or their parameters. 15 | Use Register-ReTokenprovider to define your own plugin. 16 | 17 | Transformation Sets are rules that are applied to the tokens of a specific provider. 18 | For example, the "Command" provider could receive a rule that renames the command "Get-AzureADUser" to "Get-MgUser" 19 | Use Import-ReTokenTransformationSet to provide such rules. 20 | 21 | .PARAMETER Path 22 | Path to the scriptfile to modify. 23 | 24 | .PARAMETER ProviderName 25 | Name of the Token Provider to apply. 26 | Defaults to: '*' 27 | 28 | .PARAMETER Backup 29 | Whether to create a backup of the file before modifying it. 30 | 31 | .PARAMETER OutPath 32 | Folder to which to write the converted scriptfile. 33 | 34 | .PARAMETER Force 35 | Whether to update files that end in ".backup.ps1" 36 | By default these are skipped, as they would be the backup-files of previous conversions ... or even the current one, when providing input via pipeline! 37 | 38 | .PARAMETER WhatIf 39 | If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. 40 | 41 | .PARAMETER Confirm 42 | If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. 43 | 44 | .EXAMPLE 45 | PS C:\> Get-ChildItem C:\scripts -Recurse -Filter *.ps1 | Convert-ReScriptFile 46 | 47 | Converts all scripts under C:\scripts according to the provided transformation sets. 48 | #> 49 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] 50 | [OutputType([Refactor.TransformationResult])] 51 | [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'inplace')] 52 | Param ( 53 | [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] 54 | [PsfValidateScript('PSFramework.Validate.FSPath.File', ErrorString = 'PSFramework.Validate.FSPath.File')] 55 | [Alias('FullName')] 56 | [string[]] 57 | $Path, 58 | 59 | [PsfArgumentCompleter('Refactor.TokenProvider')] 60 | [string[]] 61 | $ProviderName = '*', 62 | 63 | [Parameter(ParameterSetName = 'inplace')] 64 | [switch] 65 | $Backup, 66 | 67 | [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'path')] 68 | [PsfValidateScript('PSFramework.Validate.FSPath.Folder', ErrorString = 'PSFramework.Validate.FSPath.Folder')] 69 | [string] 70 | $OutPath, 71 | 72 | [switch] 73 | $Force 74 | ) 75 | 76 | begin { 77 | $lastResolvedPath = "" 78 | } 79 | process 80 | { 81 | if ($OutPath -ne $lastResolvedPath) { 82 | $resolvedOutPath = Resolve-PSFPath -Path $OutPath 83 | $lastResolvedPath = $OutPath 84 | } 85 | foreach ($file in $Path | Resolve-PSFPath) { 86 | if (-not $Force -and -not $OutPath -and $file -match '\.backup\.ps1$|\.backup\.psm1$') { continue } 87 | Write-PSFMessage -Message 'Processing file: {0}' -StringValues $file 88 | $scriptfile = [Refactor.ScriptFile]::new($file) 89 | 90 | try { 91 | $result = $scriptfile.Transform($scriptfile.GetTokens($ProviderName)) 92 | } 93 | catch { 94 | Write-PSFMessage -Level Error -Message 'Failed to convert file: {0}' -StringValues $file -Target $scriptfile -ErrorRecord $_ -EnableException $true -PSCmdlet $PSCmdlet 95 | } 96 | 97 | if ($OutPath) { 98 | Invoke-PSFProtectedCommand -Action 'Replacing content of script' -Target $file -ScriptBlock { 99 | $scriptfile.WriteTo($resolvedOutPath, "") 100 | } -PSCmdlet $PSCmdlet -EnableException $EnableException -Continue 101 | } 102 | else { 103 | Invoke-PSFProtectedCommand -Action 'Replacing content of script' -Target $file -ScriptBlock { 104 | $scriptfile.Save($Backup.ToBool()) 105 | } -PSCmdlet $PSCmdlet -EnableException $EnableException -Continue 106 | } 107 | $result 108 | Write-PSFMessage -Message 'Finished processing file: {0} | Transform Count {1} | Success {2}' -StringValues $file, $result.Count, $result.Success 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /Refactor/functions/Convert-ReScriptToken.ps1: -------------------------------------------------------------------------------- 1 | function Convert-ReScriptToken { 2 | <# 3 | .SYNOPSIS 4 | Converts a token using the conversion logic defined per token type. 5 | 6 | .DESCRIPTION 7 | Converts a token using the conversion logic defined per token type. 8 | This could mean renaming a command, changing a parameter, etc. 9 | 10 | The actual logic happens in the converter scriptblock provided by the Token Provider. 11 | This should update the changes in the Token object, as well as returning a summary object as output. 12 | 13 | .PARAMETER Token 14 | The token to transform. 15 | 16 | .PARAMETER Preview 17 | Instead of returning the new text for the token, return a metadata object providing additional information. 18 | 19 | .EXAMPLE 20 | PS C:\> Convert-ReScriptToken -Token $token 21 | 22 | Returns an object, showing what would have been done, had this been applied. 23 | #> 24 | [OutputType([Refactor.Change])] 25 | [CmdletBinding()] 26 | Param ( 27 | [Parameter(Mandatory = $true, ValueFromPipeline = $true)] 28 | [Refactor.ScriptToken[]] 29 | $Token 30 | ) 31 | 32 | process { 33 | foreach ($tokenObject in $Token) { 34 | $provider = Get-ReTokenProvider -Name $tokenObject.Type 35 | if (-not $provider) { 36 | Stop-PSFFunction -Message "No provider found for type $($tokenObject.Type)" -Target $tokenObject -EnableException $true -Cmdlet $PSCmdlet 37 | } 38 | & $provider.Converter $tokenObject 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /Refactor/functions/Get-ReScriptFile.ps1: -------------------------------------------------------------------------------- 1 | function Get-ReScriptFile { 2 | <# 3 | .SYNOPSIS 4 | Reads a scriptfile and returns an object representing it. 5 | 6 | .DESCRIPTION 7 | Reads a scriptfile and returns an object representing it. 8 | Use this for custom transformation needs - for example to only process some select token kinds. 9 | 10 | .PARAMETER Path 11 | Path to the scriptfile to read. 12 | 13 | .PARAMETER Name 14 | The name of the script. 15 | Used for identifying scriptcode that is not backed by an actual file. 16 | 17 | .PARAMETER Content 18 | The code of the script. 19 | Used to provide scriptcode without requiring the backing of a file. 20 | 21 | .EXAMPLE 22 | PS C:\> Get-ReScriptFile -Path C:\scripts\script.ps1 23 | 24 | Reads in the specified scriptfile 25 | #> 26 | [OutputType([Refactor.ScriptFile])] 27 | [CmdletBinding(DefaultParameterSetName = 'Path')] 28 | Param ( 29 | [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Path')] 30 | [PsfValidateScript('PSFramework.Validate.FSPath.File', ErrorString = 'PSFramework.Validate.FSPath.File')] 31 | [Alias('FullName')] 32 | [string[]] 33 | $Path, 34 | 35 | [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Content')] 36 | [string] 37 | $Name, 38 | 39 | [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Content')] 40 | [AllowEmptyString()] 41 | [string] 42 | $Content 43 | ) 44 | process { 45 | if ($Path) { 46 | foreach ($file in $Path | Resolve-PSFPath) { 47 | [Refactor.ScriptFile]::new($file) 48 | } 49 | } 50 | 51 | if ($Name -and $PSBoundParameters.ContainsKey('Content')) { 52 | [Refactor.ScriptFile]::new($Name, $Content) 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /Refactor/functions/Get-ReSplat.ps1: -------------------------------------------------------------------------------- 1 | function Get-ReSplat { 2 | <# 3 | .SYNOPSIS 4 | Resolves all splats in the offered Ast. 5 | 6 | .DESCRIPTION 7 | Resolves all splats in the offered Ast. 8 | This will look up any hashtable definitions and property-assignments to that hashtable, 9 | whether through property notation, index assignment or add method. 10 | 11 | It will then attempt to define an authorative list of properties assigned to that hashtable. 12 | If the result is unclear, that will be indicated accordingly. 13 | 14 | Return Objects include properties: 15 | + Splat : The original Ast where the hashtable is used for splatting 16 | + Parameters : A hashtable containing all properties clearly identified 17 | + ParametersKnown : Whether we are confident of having identified all properties passed through as parameters 18 | 19 | .PARAMETER Ast 20 | The Ast object to search. 21 | Use "Read-ReAst" to parse a scriptfile into an AST object. 22 | 23 | .EXAMPLE 24 | PS C:\> Get-ReSplat -Ast $ast 25 | 26 | Returns all splats used in the Abstract Syntax Tree object specified 27 | #> 28 | [OutputType([Refactor.Splat])] 29 | [CmdletBinding()] 30 | param ( 31 | [Parameter(Mandatory = $true)] 32 | [System.Management.Automation.Language.Ast] 33 | $Ast 34 | ) 35 | 36 | $splats = Search-ReAst -Ast $Ast -Filter { 37 | if ($args[0] -isnot [System.Management.Automation.Language.VariableExpressionAst]) { return $false } 38 | $args[0].Splatted 39 | } 40 | if (-not $splats) { return } 41 | 42 | foreach ($splat in $splats) { 43 | # Select the last variable declaration _before_ the splat is being used 44 | $assignments = Search-ReAst -Ast $Ast -Filter { 45 | if ($args[0] -isnot [System.Management.Automation.Language.AssignmentStatementAst]) { return $false } 46 | if ($args[0].Left -isnot [System.Management.Automation.Language.VariableExpressionAst]) { return $false } 47 | $args[0].Left.VariablePath.UserPath -eq $splat.Data.VariablePath.UserPath 48 | } 49 | $declaration = $assignments | Where-Object { $_.Start -lt $splat.Start } | Sort-Object { 50 | $_.Start 51 | } -Descending | Select-Object -First 1 52 | 53 | $result = [Refactor.Splat]@{ 54 | Ast = $splat.Data 55 | } 56 | 57 | if (-not $declaration) { 58 | $result.ParametersKnown = $false 59 | $result 60 | continue 61 | } 62 | 63 | $propertyAssignments = Search-ReAst -Ast $Ast -Filter { 64 | if ($args[0].Extent.StartLineNumber -le $declaration.Start) { return $false } 65 | if ($args[0].Extent.StartLineNumber -ge $splat.Start) { return $false } 66 | 67 | $isAssignment = $( 68 | ($args[0] -is [System.Management.Automation.Language.AssignmentStatementAst]) -and ( 69 | ($args[0].Left.VariablePath.UserPath -eq $splat.Data.VariablePath.UserPath) -or 70 | ($args[0].Left.Expression.VariablePath.UserPath -eq $splat.Data.VariablePath.UserPath) -or 71 | ($args[0].Left.Target.VariablePath.UserPath -eq $splat.Data.VariablePath.UserPath) 72 | ) 73 | ) 74 | $isAddition = $( 75 | ($args[0] -is [System.Management.Automation.Language.InvokeMemberExpressionAst]) -and 76 | ($args[0].Expression.VariablePath.UserPath -eq $splat.Data.VariablePath.UserPath) -and 77 | ($args[0].Member.Value -eq 'Add') 78 | ) 79 | $isAddition -or $isAssignment 80 | } 81 | 82 | if ($declaration.Data.Right.Expression -isnot [System.Management.Automation.Language.HashtableAst]) { 83 | $result.ParametersKnown = $false 84 | } 85 | 86 | foreach ($pair in $declaration.Data.Right.Expression.KeyValuePairs) { 87 | if ($pair.Item1 -is [System.Management.Automation.Language.StringConstantExpressionAst]) { 88 | $result.Parameters[$pair.Item1.Value] = $pair.Item1.Value 89 | } 90 | else { 91 | $result.ParametersKnown = $false 92 | } 93 | } 94 | 95 | foreach ($assignment in $propertyAssignments) { 96 | switch ($assignment.Type) { 97 | 'AssignmentStatementAst' { 98 | if ($assignment.Data.Left.Member -is [System.Management.Automation.Language.StringConstantExpressionAst]) { 99 | $result.Parameters[$assignment.Data.Left.Member.Value] = $assignment.Data.Left.Member.Value 100 | continue 101 | } 102 | if ($assignment.Data.Left.Index -is [System.Management.Automation.Language.StringConstantExpressionAst]) { 103 | $result.Parameters[$assignment.Data.Left.Index.Value] = $assignment.Data.Left.Index.Value 104 | continue 105 | } 106 | 107 | $result.ParametersKnown = $false 108 | } 109 | 'InvokeMemberExpressionAst' { 110 | if ($assignment.Data.Arguments[0] -is [System.Management.Automation.Language.StringConstantExpressionAst]) { 111 | $result.Parameters[$assignment.Data.Arguments[0].Value] = $assignment.Data.Arguments[0].Value 112 | continue 113 | } 114 | 115 | $result.ParametersKnown = $false 116 | } 117 | } 118 | } 119 | # Include all relevant Ast objects 120 | $result.Assignments = @($declaration.Data) + @($propertyAssignments.Data) | Remove-PSFNull -Enumerate 121 | $result 122 | } 123 | } -------------------------------------------------------------------------------- /Refactor/functions/Get-ReToken.ps1: -------------------------------------------------------------------------------- 1 | function Get-ReToken { 2 | <# 3 | .SYNOPSIS 4 | Scans a scriptfile for all tokens contained within. 5 | 6 | .DESCRIPTION 7 | Scans a scriptfile for all tokens contained within. 8 | 9 | .PARAMETER Path 10 | Path to the file to scan 11 | 12 | .PARAMETER ProviderName 13 | Names of the providers to use. 14 | Defaults to '*' 15 | 16 | .EXAMPLE 17 | PS C:\> Get-ChildItem C:\scripts | Get-ReToken 18 | 19 | Returns all tokens for all scripts under C:\scripts 20 | #> 21 | [CmdletBinding()] 22 | param ( 23 | [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] 24 | [PsfValidateScript('PSFramework.Validate.FSPath.File', ErrorString = 'PSFramework.Validate.FSPath.File')] 25 | [Alias('FullName')] 26 | [string[]] 27 | $Path, 28 | 29 | [PsfArgumentCompleter('Refactor.TokenProvider')] 30 | [string[]] 31 | $ProviderName = '*' 32 | ) 33 | 34 | process { 35 | foreach ($file in $Path | Resolve-PSFPath) { 36 | Write-PSFMessage -Message 'Processing file: {0}' -StringValues $file 37 | $scriptfile = [Refactor.ScriptFile]::new($file) 38 | $scriptfile.GetTokens($ProviderName) 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /Refactor/functions/Get-ReTokenProvider.ps1: -------------------------------------------------------------------------------- 1 | function Get-ReTokenProvider { 2 | <# 3 | .SYNOPSIS 4 | List registered token providers. 5 | 6 | .DESCRIPTION 7 | List registered token providers. 8 | 9 | Token providers are scriptblocks that will parse an Abstract Syntax Tree, searching for specific types of code content. 10 | These can then be used for code analysis or refactoring. 11 | 12 | .PARAMETER Name 13 | Name of the provider to filter by. 14 | Defaults to "*" 15 | 16 | .PARAMETER Component 17 | Return only the specified component: 18 | + All: Return the entire provider 19 | + Tokenizer: Return only the scriptblock, that parses out the Ast 20 | + Converter: Return only the scriptblock, that applies transforms to tokens 21 | Default: All 22 | 23 | .EXAMPLE 24 | PS C:\> Get-ReTokenProvider 25 | 26 | List all token providers 27 | #> 28 | [OutputType([Refactor.TokenProvider])] 29 | [CmdletBinding()] 30 | Param ( 31 | [PsfArgumentCompleter('Refactor.TokenProvider')] 32 | [string[]] 33 | $Name = '*', 34 | 35 | [ValidateSet('All','Tokenizer','Converter')] 36 | [string] 37 | $Component = 'All' 38 | ) 39 | 40 | process { 41 | foreach ($provider in $script:tokenProviders.GetEnumerator()) { 42 | $matched = $false 43 | foreach ($nameFilter in $Name) { 44 | if ($provider.Key -like $nameFilter) { $matched = $true } 45 | } 46 | if (-not $matched) { continue } 47 | 48 | if ($Component -eq 'Tokenizer') { 49 | $provider.Value.Tokenizer 50 | continue 51 | } 52 | if ($Component -eq 'Converter') { 53 | $provider.Value.Converter 54 | continue 55 | } 56 | 57 | $provider.Value 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /Refactor/functions/Get-ReTokenTransformationSet.ps1: -------------------------------------------------------------------------------- 1 | function Get-ReTokenTransformationSet { 2 | <# 3 | .SYNOPSIS 4 | List the registered transformation sets. 5 | 6 | .DESCRIPTION 7 | List the registered transformation sets. 8 | 9 | .PARAMETER Type 10 | The type of token to filter by. 11 | Defaults to '*' 12 | 13 | .EXAMPLE 14 | PS C:\> Get-ReTokenTransformationSet 15 | 16 | Return all registerd transformation sets. 17 | #> 18 | [CmdletBinding()] 19 | param ( 20 | [string] 21 | $Type = '*' 22 | ) 23 | 24 | process { 25 | foreach ($pair in $script:tokenTransformations.GetEnumerator()) { 26 | if ($pair.Key -notlike $Type) { continue } 27 | $pair.Value.Values 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Refactor/functions/Import-ReTokenTransformationSet.ps1: -------------------------------------------------------------------------------- 1 | function Import-ReTokenTransformationSet { 2 | <# 3 | .SYNOPSIS 4 | Imports a token transformation file. 5 | 6 | .DESCRIPTION 7 | Imports a token transformation file. 8 | Can be either json or psd1 format 9 | 10 | Root level must contain at least three nodes: 11 | + Version: The schema version of this file. Should be 1 12 | + Type: The type of token being transformed. E.g.: "Command" 13 | + Content: A hashtable containing the actual sets of transformation. The properties required depend on the Token Provider. 14 | 15 | Example: 16 | @{ 17 | Version = 1 18 | Type = 'Command' 19 | Content = @{ 20 | "Get-AzureADUser" = @{ 21 | Name = "Get-AzureADUser" 22 | NewName = "Get-MgUser" 23 | Comment = "Filter and search parameters cannot be mapped straight, may require manual attention" 24 | Parameters = @{ 25 | Search = "Filter" # Rename Search on "Get-AzureADUser" to "Filter" on "Get-MgUser" 26 | } 27 | } 28 | } 29 | } 30 | 31 | .PARAMETER Path 32 | Path to the file to import. 33 | Must be json or psd1 format 34 | 35 | .EXAMPLE 36 | PS C:\> Import-ReTokenTransformationSet -Path .\azureAD-to-graph.psd1 37 | 38 | Imports all the transformationsets stored in "azureAD-to-graph.psd1" in the current folder. 39 | #> 40 | [CmdletBinding()] 41 | Param ( 42 | [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] 43 | [Alias('FullName')] 44 | [string[]] 45 | $Path 46 | ) 47 | 48 | begin { 49 | function Import-TransformV1 { 50 | [CmdletBinding()] 51 | param ( 52 | $Data, 53 | 54 | $Path 55 | ) 56 | 57 | $msgDefault = @{ 58 | Level = "Warning" 59 | FunctionName = 'Import-ReTokenTransformationSet' 60 | PSCmdlet = $PSCmdlet 61 | StringValues = $Path 62 | } 63 | 64 | $defaultType = $Data.Type 65 | $contentHash = $Data.Content | ConvertTo-PSFHashtable 66 | foreach ($entry in $contentHash.Values) { 67 | $entryHash = $entry | ConvertTo-PSFHashtable 68 | if ($defaultType -and -not $entryHash.Type) { 69 | $entryHash.Type = $defaultType 70 | } 71 | if (-not $entryHash.Type) { 72 | Write-PSFMessage @msgDefault -Message "Invalid entry within file - No Type defined: {0}" -Target $entryHash 73 | continue 74 | } 75 | 76 | try { Register-ReTokenTransformation @entryHash -ErrorAction Stop } 77 | catch { 78 | Write-PSFMessage @msgDefault -Message "Error processing entry within file: {0}" -ErrorRecord $_ -Target $entryHash 79 | continue 80 | } 81 | } 82 | } 83 | } 84 | process { 85 | :main foreach ($filePath in $Path | Resolve-PSFPath -Provider FileSystem) { 86 | if (Test-Path -LiteralPath $filePath -PathType Container) { continue } 87 | 88 | $fileInfo = Get-Item -LiteralPath $filePath 89 | $data = switch ($fileInfo.Extension) { 90 | '.json' { 91 | Get-Content -LiteralPath $fileInfo.FullName | ConvertFrom-Json 92 | } 93 | '.psd1' { 94 | Import-PSFPowerShellDataFile -LiteralPath $fileInfo.FullName 95 | } 96 | default { 97 | $exception = [System.ArgumentException]::new("Unknown file extension: $($fileInfo.Extension)") 98 | Write-PSFMessage -Message "Error importing $($fileInfo.FullName): Unknown file extension: $($fileInfo.Extension)" -Level Error -Exception $exception -EnableException $true -Target $fileInfo -OverrideExceptionMessage 99 | continue main 100 | } 101 | } 102 | 103 | switch ("$($data.Version)") { 104 | "1" { Import-TransformV1 -Data $data -Path $fileInfo.FullName } 105 | default { 106 | $exception = [System.ArgumentException]::new("Unknown schema version: $($data.Version)") 107 | Write-PSFMessage -Message "Error importing $($fileInfo.FullName): Unknown schema version: $($data.Version)" -Level Error -Exception $exception -EnableException $true -Target $fileInfo -OverrideExceptionMessage 108 | continue main 109 | } 110 | } 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /Refactor/functions/New-ReToken.ps1: -------------------------------------------------------------------------------- 1 | function New-ReToken { 2 | <# 3 | .SYNOPSIS 4 | Creates a new, generic token object. 5 | 6 | .DESCRIPTION 7 | Creates a new, generic token object. 8 | Use this in script-only Token Providers, trading the flexibility of a custom Token type 9 | for the simplicity of not having to deal with C# or classes. 10 | 11 | .PARAMETER Type 12 | The type of the token. 13 | Must match the name of the provider using it. 14 | 15 | .PARAMETER Name 16 | The name of the token. 17 | Used to match the token against transforms. 18 | 19 | .PARAMETER Ast 20 | An Ast object representing the location in the script the token deals with. 21 | Purely optional, so long as your provider knows how to deal with the token. 22 | 23 | .PARAMETER Data 24 | Any additional data to store with the token. 25 | 26 | .EXAMPLE 27 | PS C:\> New-ReToken -Type variable -Name ComputerName 28 | 29 | Creates a new token of type variable with name ComputerName. 30 | Assumes you have registered a Token Provider of name variable. 31 | #> 32 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] 33 | [OutputType([Refactor.GenericToken])] 34 | [CmdletBinding()] 35 | param ( 36 | [parameter(Mandatory = $true)] 37 | [string] 38 | $Type, 39 | 40 | [parameter(Mandatory = $true)] 41 | [string] 42 | $Name, 43 | 44 | [System.Management.Automation.Language.Ast] 45 | $Ast, 46 | 47 | [object] 48 | $Data 49 | ) 50 | 51 | process { 52 | $token = [Refactor.GenericToken]::new($Type, $Name) 53 | $token.Ast = $Ast 54 | $token.Data = $Data 55 | $token 56 | } 57 | } -------------------------------------------------------------------------------- /Refactor/functions/Read-ReAst.ps1: -------------------------------------------------------------------------------- 1 | function Read-ReAst 2 | { 3 | <# 4 | .SYNOPSIS 5 | Parse the content of a script 6 | 7 | .DESCRIPTION 8 | Uses the powershell parser to parse the content of a script or scriptfile. 9 | 10 | .PARAMETER ScriptCode 11 | The scriptblock to parse. 12 | 13 | .PARAMETER Path 14 | Path to the scriptfile to parse. 15 | Silently ignores folder objects. 16 | 17 | .EXAMPLE 18 | PS C:\> Read-Ast -ScriptCode $ScriptCode 19 | 20 | Parses the code in $ScriptCode 21 | 22 | .EXAMPLE 23 | PS C:\> Get-ChildItem | Read-ReAst 24 | 25 | Parses all script files in the current directory 26 | #> 27 | [CmdletBinding()] 28 | param ( 29 | [Parameter(Position = 0, ParameterSetName = 'Script', Mandatory = $true)] 30 | [string] 31 | $ScriptCode, 32 | 33 | [Parameter(Mandatory = $true, ParameterSetName = 'File', ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] 34 | [Alias('FullName')] 35 | [string[]] 36 | $Path 37 | ) 38 | 39 | process 40 | { 41 | foreach ($file in $Path) 42 | { 43 | Write-PSFMessage -Level Verbose -Message "Processing $file" -Target $file 44 | $item = Get-Item $file 45 | if ($item.PSIsContainer) 46 | { 47 | Write-PSFMessage -Level Verbose -Message "is folder, skipping $file" -Target $file 48 | continue 49 | } 50 | 51 | $tokens = $null 52 | $errors = $null 53 | $ast = [System.Management.Automation.Language.Parser]::ParseFile($item.FullName, [ref]$tokens, [ref]$errors) 54 | [pscustomobject]@{ 55 | Ast = $ast 56 | Tokens = $tokens 57 | Errors = $errors 58 | File = $item.FullName 59 | } 60 | } 61 | 62 | if ($ScriptCode) 63 | { 64 | if (-not $content) { $content = $ScriptCode } 65 | $tokens = $null 66 | $errors = $null 67 | $ast = [System.Management.Automation.Language.Parser]::ParseInput($content, [ref]$tokens, [ref]$errors) 68 | [pscustomobject]@{ 69 | Ast = $ast 70 | Tokens = $tokens 71 | Errors = $errors 72 | Source = $ScriptCode 73 | } 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /Refactor/functions/Read-ReAstComponent.ps1: -------------------------------------------------------------------------------- 1 | function Read-ReAstComponent { 2 | <# 3 | .SYNOPSIS 4 | Search for instances of a given AST type. 5 | 6 | .DESCRIPTION 7 | Search for instances of a given AST type. 8 | This command - together with its sibling command "Write-ReAstComponent" - is designed to simplify code updates. 9 | 10 | Use the data on the object, update its "NewText" property and use the "Write"-command to apply it back to the original document. 11 | 12 | .PARAMETER Name 13 | Name of the "file" to search. 14 | Use this together with the 'ScriptCode' parameter when you do not actually have a file object and just the code itself. 15 | Usually happens when scanning a git repository or otherwise getting the data from some API/service. 16 | 17 | .PARAMETER ScriptCode 18 | Code of the "file" to search. 19 | Use this together with the 'Name' parameter when you do not actually have a file object and just the code itself. 20 | Usually happens when scanning a git repository or otherwise getting the data from some API/service. 21 | 22 | .PARAMETER Path 23 | Path to the file to scan. 24 | Uses wildcards to interpret results. 25 | 26 | .PARAMETER LiteralPath 27 | Literal path to the file to scan. 28 | Does not interpret the path and instead use it as it is written. 29 | Useful when there are brackets in the filename. 30 | 31 | .PARAMETER Select 32 | The AST types to select for. 33 | 34 | .PARAMETER EnableException 35 | This parameters disables user-friendly warnings and enables the throwing of exceptions. 36 | This is less user friendly, but allows catching exceptions in calling scripts. 37 | 38 | .EXAMPLE 39 | PS C:\> Get-ChildItem -Recurse -Filter *.ps1 | Read-ReAstComponent -Select FunctionDefinitionAst, ForEachStatementAst 40 | 41 | Reads all ps1 files in the current folder and subfolders and scans for all function definitions and foreach statements. 42 | #> 43 | [OutputType([Refactor.Component.AstResult])] 44 | [CmdletBinding(DefaultParameterSetName = 'File')] 45 | param ( 46 | [Parameter(Position = 0, ParameterSetName = 'Script', Mandatory = $true, ValueFromPipelineByPropertyName = $true)] 47 | [string] 48 | $Name, 49 | 50 | [Parameter(Position = 1, ParameterSetName = 'Script', Mandatory = $true, ValueFromPipelineByPropertyName = $true)] 51 | [Alias('Content')] 52 | [string] 53 | $ScriptCode, 54 | 55 | [Parameter(Mandatory = $true, ParameterSetName = 'File', ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] 56 | [Alias('FullName')] 57 | [string[]] 58 | $Path, 59 | 60 | [Parameter(Mandatory = $true, ParameterSetName = 'Literal')] 61 | [string[]] 62 | $LiteralPath, 63 | 64 | [Parameter(Mandatory = $true)] 65 | [PsfArgumentCompleter('Refactor.AstTypes')] 66 | [PsfValidateSet(TabCompletion = 'Refactor.AstTypes')] 67 | [string[]] 68 | $Select, 69 | 70 | [switch] 71 | $EnableException 72 | ) 73 | 74 | process { 75 | #region Resolve Targets 76 | $targets = [System.Collections.ArrayList]@() 77 | if ($Name) { 78 | $null = $targets.Add( 79 | [PSCustomObject]@{ 80 | Name = $Name 81 | Content = $ScriptCode 82 | Path = '' 83 | } 84 | ) 85 | } 86 | foreach ($pathEntry in $Path) { 87 | try { $resolvedPaths = Resolve-PSFPath -Path $pathEntry -Provider FileSystem } 88 | catch { 89 | Write-PSFMessage -Level Warning -Message 'Failed to resolve path: {0}' -StringValues $pathEntry -ErrorRecord $_ -EnableException $EnableException 90 | continue 91 | } 92 | 93 | foreach ($resolvedPath in $resolvedPaths) { 94 | $null = $targets.Add( 95 | [PSCustomObject]@{ 96 | Name = Split-Path -Path $resolvedPath -Leaf 97 | Path = $resolvedPath 98 | } 99 | ) 100 | } 101 | } 102 | foreach ($pathEntry in $LiteralPath) { 103 | try { $resolvedPath = (Get-Item -LiteralPath $pathEntry -ErrorAction Stop).FullName } 104 | catch { 105 | Write-PSFMessage -Level Warning -Message 'Failed to resolve path: {0}' -StringValues $pathEntry -ErrorRecord $_ -EnableException $EnableException 106 | continue 107 | } 108 | 109 | $null = $targets.Add( 110 | [PSCustomObject]@{ 111 | Name = Split-Path -Path $resolvedPath -Leaf 112 | Path = $resolvedPath 113 | } 114 | ) 115 | } 116 | #endregion Resolve Targets 117 | 118 | Clear-ReTokenTransformationSet 119 | Register-ReTokenTransformation -Type ast -TypeName $Select 120 | 121 | foreach ($target in $targets) { 122 | # Create ScriptFile object 123 | if ($target.Path) { 124 | $scriptFile = [Refactor.ScriptFile]::new($target.Path) 125 | } 126 | else { 127 | $scriptFile = [Refactor.ScriptFile]::new($target.Name, $target.Content) 128 | } 129 | 130 | # Generate Tokens 131 | $tokens = $scriptFile.GetTokens('Ast') 132 | 133 | # Profit! 134 | $result = [Refactor.Component.ScriptResult]::new() 135 | $result.File = $scriptFile 136 | $result.Types = $Select 137 | foreach ($token in $tokens) { $result.Tokens.Add($token) } 138 | 139 | foreach ($token in $tokens) { 140 | [Refactor.Component.AstResult]::new($token, $scriptFile, $result) 141 | } 142 | } 143 | 144 | Clear-ReTokenTransformationSet 145 | } 146 | } -------------------------------------------------------------------------------- /Refactor/functions/Read-ReScriptCommand.ps1: -------------------------------------------------------------------------------- 1 | function Read-ReScriptCommand 2 | { 3 | <# 4 | .SYNOPSIS 5 | Reads a scriptfile and returns all commands contained within. 6 | 7 | .DESCRIPTION 8 | Reads a scriptfile and returns all commands contained within. 9 | Includes parameters used and whether all parameters could be resolved. 10 | 11 | .PARAMETER Path 12 | Path to the file to scan 13 | 14 | .PARAMETER Ast 15 | An already provided Abstract Syntax Tree object to process 16 | 17 | .EXAMPLE 18 | Get-ChildItem C:\scripts -Recurse -Filter *.ps1 | Read-ReScriptCommand 19 | 20 | Returns all commands in all files under C:\scripts 21 | #> 22 | [OutputType([Refactor.CommandToken])] 23 | [CmdletBinding()] 24 | Param ( 25 | [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Path')] 26 | [PsfValidateScript('PSFramework.Validate.FSPath.File', ErrorString = 'PSFramework.Validate.FSPath.File')] 27 | [Alias('FullName')] 28 | [string[]] 29 | $Path, 30 | 31 | [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Ast')] 32 | [System.Management.Automation.Language.Ast] 33 | $Ast 34 | ) 35 | 36 | process 37 | { 38 | if ($Path) { 39 | foreach ($file in $Path | Resolve-PSFPath) { 40 | $parsed = Read-ReAst -Path $file 41 | 42 | $splats = Get-ReSplat -Ast $parsed.Ast 43 | Get-AstCommand -Ast $parsed.Ast -Splat $splats 44 | } 45 | } 46 | 47 | foreach ($astObject in $Ast) { 48 | $splats = Get-ReSplat -Ast $astObject 49 | Get-AstCommand -Ast $astObject -Splat $splats 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /Refactor/functions/Register-ReTokenProvider.ps1: -------------------------------------------------------------------------------- 1 | function Register-ReTokenProvider { 2 | <# 3 | .SYNOPSIS 4 | Register a Token Provider, that implements scanning and refactor logic. 5 | 6 | .DESCRIPTION 7 | Register a Token Provider, that implements scanning and refactor logic. 8 | 9 | For example, the "Command" Token Provider supports: 10 | - Finding all commands called in a script, resolving all parameters used as possible. 11 | - Renaming commands and their parameters. 12 | 13 | For examples on how to implement this, see: 14 | Provider: https://github.com/FriedrichWeinmann/Refactor/blob/development/Refactor/internal/tokenProvider/command.token.ps1 15 | Token Class: https://github.com/FriedrichWeinmann/Refactor/blob/development/library/Refactor/Refactor/CommandToken.cs 16 | 17 | Note: Rather than implementing your on Token Class, you can use New-ReToken and the GenericToken class. 18 | This allows you to avoid the need for coding your own class, but offers no extra functionality. 19 | 20 | .PARAMETER Name 21 | Name of the token provider. 22 | 23 | .PARAMETER TransformIndex 24 | The property name used to map a transformation rule to a token. 25 | 26 | .PARAMETER ParametersMandatory 27 | The parameters a transformation rule MUST have to be valid. 28 | 29 | .PARAMETER Parameters 30 | The parameters a transformation rule accepts / supports. 31 | 32 | .PARAMETER Tokenizer 33 | Code that provides the required tokens when executed. 34 | Accepts one argument: An Ast object. 35 | 36 | .PARAMETER Converter 37 | Code that applies the registered transformation rule to a given token. 38 | Accepts two arguments: A Token and a boolean. 39 | The boolean argument representing, whether a preview object, representing the expected changes should be returned. 40 | 41 | .EXAMPLE 42 | PS C:\> Register-ReTokenProvider @param 43 | 44 | Registers a token provider. 45 | A useful example for what to provide is a bit more than can be fit in an example block, 46 | See an example provider here: 47 | Provider: https://github.com/FriedrichWeinmann/Refactor/blob/development/Refactor/internal/tokenProvider/command.token.ps1 48 | Token Class: https://github.com/FriedrichWeinmann/Refactor/blob/development/library/Refactor/Refactor/CommandToken.cs 49 | #> 50 | [CmdletBinding()] 51 | Param ( 52 | [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] 53 | [string] 54 | $Name, 55 | 56 | [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] 57 | [string] 58 | $TransformIndex, 59 | 60 | [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] 61 | [string[]] 62 | $ParametersMandatory, 63 | 64 | [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] 65 | [string[]] 66 | $Parameters, 67 | 68 | [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] 69 | [ScriptBlock] 70 | $Tokenizer, 71 | 72 | [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] 73 | [ScriptBlock] 74 | $Converter 75 | ) 76 | 77 | process { 78 | $script:tokenProviders[$Name] = [Refactor.TokenProvider]@{ 79 | Name = $Name 80 | TransformIndex = $TransformIndex 81 | TransformParametersMandatory = $ParametersMandatory 82 | TransformParameters = $Parameters 83 | Tokenizer = $Tokenizer 84 | Converter = $Converter 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /Refactor/functions/Register-ReTokenTransformation.ps1: -------------------------------------------------------------------------------- 1 | function Register-ReTokenTransformation { 2 | <# 3 | .SYNOPSIS 4 | Register a transformation rule used when refactoring scripts. 5 | 6 | .DESCRIPTION 7 | Register a transformation rule used when refactoring scripts. 8 | Rules are specific to their token type. 9 | Different types require different parameters, which are added via dynamic parameters. 10 | For more details, look up the documentation for the specific token type you want to register a transformation for. 11 | 12 | .PARAMETER Type 13 | The type of token to register a transformation over. 14 | 15 | .EXAMPLE 16 | PS C:\> Register-ReTokenTransformation -Type Command -Name Get-AzureADUser -NewName Get-MGUser -Comment "The filter parameter requires manual adjustments if used" 17 | 18 | Registers a transformation rule, that will convert the Get-AzureADUser command to Get-MGUser 19 | #> 20 | [CmdletBinding()] 21 | Param ( 22 | [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] 23 | [string] 24 | $Type 25 | ) 26 | 27 | DynamicParam { 28 | $parameters = (Get-ReTokenProvider -Name $Type).TransformParameters 29 | if (-not $parameters) { return } 30 | 31 | $results = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary 32 | foreach ($parameter in $parameters) { 33 | $parameterAttribute = New-Object System.Management.Automation.ParameterAttribute 34 | $parameterAttribute.ParameterSetName = '__AllParameterSets' 35 | $attributesCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] 36 | $attributesCollection.Add($parameterAttribute) 37 | $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter($parameter, [object], $attributesCollection) 38 | 39 | $results.Add($parameter, $RuntimeParam) 40 | } 41 | 42 | $results 43 | } 44 | 45 | begin { 46 | $commonParam = 'Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'InformationAction', 'ErrorVariable', 'WarningVariable', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable' 47 | } 48 | process { 49 | $provider = Get-ReTokenProvider -Name $Type 50 | if (-not $provider) { 51 | Stop-PSFFunction -Message "No provider found for type $Type" -Target $PSBoundParameters -EnableException $true -Cmdlet $PSCmdlet 52 | } 53 | 54 | $hash = $PSBoundParameters | ConvertTo-PSFHashtable -Exclude $commonParam 55 | $missingMandatory = $provider.TransformParametersMandatory | Where-Object { $_ -notin $hash.Keys } 56 | if ($missingMandatory) { 57 | Stop-PSFFunction -Message "Error defining a $($Type) transformation: $($provider.TransformParametersMandatory -join ",") must be specified! Missing: $($missingMandatory -join ",")" -Target $PSBoundParameters -EnableException $true -Cmdlet $PSCmdlet 58 | } 59 | if (-not $script:tokenTransformations[$Type]) { 60 | $script:tokenTransformations[$Type] = @{ } 61 | } 62 | 63 | $script:tokenTransformations[$Type][$hash.$($provider.TransformIndex)] = [PSCustomObject]$hash 64 | } 65 | } -------------------------------------------------------------------------------- /Refactor/functions/Search-ReAst.ps1: -------------------------------------------------------------------------------- 1 | function Search-ReAst { 2 | <# 3 | .SYNOPSIS 4 | Tool to search the Abstract Syntax Tree 5 | 6 | .DESCRIPTION 7 | Tool to search the Abstract Syntax Tree 8 | 9 | .PARAMETER Ast 10 | The Ast to search 11 | 12 | .PARAMETER Filter 13 | The filter condition to apply 14 | 15 | .EXAMPLE 16 | PS C:\> Search-ReAst -Ast $ast -Filter { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] } 17 | 18 | Searches for all function definitions 19 | #> 20 | [OutputType([Refactor.SearchResult])] 21 | [CmdletBinding()] 22 | param ( 23 | [Parameter(Mandatory = $true)] 24 | [System.Management.Automation.Language.Ast] 25 | $Ast, 26 | 27 | [Parameter(Mandatory = $true)] 28 | [ScriptBlock] 29 | $Filter 30 | ) 31 | 32 | process { 33 | $results = $Ast.FindAll($Filter, $true) 34 | 35 | foreach ($result in $results) { 36 | [Refactor.SearchResult]::new($result) 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Refactor/functions/Test-ReSyntax.ps1: -------------------------------------------------------------------------------- 1 | function Test-ReSyntax { 2 | <# 3 | .SYNOPSIS 4 | Tests whether the syntax of a given scriptfile or scriptcode is valid. 5 | 6 | .DESCRIPTION 7 | Tests whether the syntax of a given scriptfile or scriptcode is valid. 8 | This uses the PowerShell syntax validation. 9 | Some cases - especially around PowerShell classes - may evaluate as syntax error when missing dependencies. 10 | 11 | .PARAMETER Path 12 | Path to the file to test. 13 | 14 | .PARAMETER LiteralPath 15 | Non-interpreted path to the file to test. 16 | 17 | .PARAMETER Code 18 | Actual code to test. 19 | 20 | .PARAMETER Not 21 | Reverses the returned logic: A syntax error found returns as $true, an error-free script returns $false. 22 | 23 | .EXAMPLE 24 | PS C:\> Test-ReSyntax .\script.ps1 25 | 26 | Verifies the syntax of the file 'script.ps1' in the current path. 27 | #> 28 | [OutputType([bool])] 29 | [CmdletBinding(DefaultParameterSetName = 'path')] 30 | param ( 31 | [Parameter(Mandatory = $true, ParameterSetName = 'path', Position = 0)] 32 | [string] 33 | $Path, 34 | 35 | [Parameter(Mandatory = $true, ParameterSetName = 'literal')] 36 | [string] 37 | $LiteralPath, 38 | 39 | [Parameter(Mandatory = $true, ParameterSetName = 'code')] 40 | [string] 41 | $Code, 42 | 43 | [switch] 44 | $Not 45 | ) 46 | 47 | process { 48 | if ($Code) { 49 | $result = Read-ReAst -ScriptCode $Code 50 | return ($result.Errors -as [bool]) -eq $Not 51 | } 52 | if ($Path) { 53 | try { $resolvedPath = Resolve-PSFPath -Path $Path -Provider FileSystem -SingleItem } 54 | catch { return $Not -as [bool] } # as bool to satisfy output type PSSA warnings and unify result type 55 | 56 | $fileItem = Get-Item -LiteralPath $resolvedPath 57 | } 58 | if ($LiteralPath) { 59 | try { $fileItem = Get-Item -LiteralPath $LiteralPath -ErrorAction Stop } 60 | catch { return $Not -as [bool] } 61 | } 62 | 63 | $tokens = $null 64 | $errors = $null 65 | $null = [System.Management.Automation.Language.Parser]::ParseFile($fileItem.FullName, [ref]$tokens, [ref]$errors) 66 | ($errors -as [bool]) -eq $Not 67 | } 68 | } -------------------------------------------------------------------------------- /Refactor/functions/Write-ReAstComponent.ps1: -------------------------------------------------------------------------------- 1 | function Write-ReAstComponent { 2 | <# 3 | .SYNOPSIS 4 | Updates a scriptfile that was read from using Read-ReAstComponent. 5 | 6 | .DESCRIPTION 7 | Updates a scriptfile that was read from using Read-ReAstComponent. 8 | Automatically picks up the file to update from the scan results. 9 | Expects the caller to first apply changes on the test results outside of the Refactor module. 10 | 11 | This command processes all output in end, to support sane pipeline processing of multiple findings from a single file. 12 | 13 | .PARAMETER Components 14 | Component objects scanned from the file to update. 15 | Use Read-ReAstComponent. 16 | Pass all objects from the search in one go (or pipe them into the command) 17 | 18 | .PARAMETER PassThru 19 | Return result objects from the conversion. 20 | By default, this command updates the files in situ or in the target location (OutPath). 21 | Whether you use this parameter or not, scan results that were provided input from memory - and are thus not backed by a file - will always be returned as output. 22 | 23 | .PARAMETER Backup 24 | Whether to create a backup of the file before modifying it. 25 | 26 | .PARAMETER OutPath 27 | Folder to which to write the converted scriptfile. 28 | 29 | .PARAMETER WhatIf 30 | If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. 31 | 32 | .PARAMETER Confirm 33 | If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. 34 | 35 | .PARAMETER EnableException 36 | This parameters disables user-friendly warnings and enables the throwing of exceptions. 37 | This is less user friendly, but allows catching exceptions in calling scripts. 38 | 39 | .EXAMPLE 40 | PS C:\> Write-ReAstComponent -Components $scriptParts 41 | 42 | Writes back the components in $scriptParts, which had previously been generated using Read-ReAstComponent, then had their content modified. 43 | #> 44 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] 45 | [OutputType([Refactor.Component.ScriptFileConverted])] 46 | [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'default')] 47 | param ( 48 | [Parameter(Mandatory = $true, ValueFromPipeline = $true)] 49 | [Refactor.Component.AstResult[]] 50 | $Components, 51 | 52 | [switch] 53 | $PassThru, 54 | 55 | [Parameter(ParameterSetName = 'inplace')] 56 | [switch] 57 | $Backup, 58 | 59 | [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'path')] 60 | [PsfValidateScript('PSFramework.Validate.FSPath.Folder', ErrorString = 'PSFramework.Validate.FSPath.Folder')] 61 | [string] 62 | $OutPath, 63 | 64 | [switch] 65 | $EnableException 66 | ) 67 | begin { 68 | $componentObjects = [System.Collections.ArrayList]@() 69 | if ($OutPath) { 70 | $resolvedOutPath = Resolve-PSFPath -Path $OutPath 71 | } 72 | } 73 | process { 74 | $null = $componentObjects.AddRange($Components) 75 | } 76 | end { 77 | $grouped = $componentObjects | Group-Object { $_.Result.Id } 78 | foreach ($tokenGroup in $grouped) { 79 | $scriptFile = $tokenGroup.Group[0].File 80 | $before = $scriptFile.Content 81 | $null = $scriptFile.Transform($tokenGroup.Group.Token) 82 | if (-not $OutPath -and $before -eq $scriptFile.Content) { continue } 83 | 84 | if ($PassThru) { 85 | [Refactor.Component.ScriptFileConverted]::new($tokenGroup.Group[0].Result) 86 | } 87 | 88 | #region From File 89 | if ($scriptFile.FromFile) { 90 | if ($OutPath) { 91 | Invoke-PSFProtectedCommand -Action "Writing updated script to $resolvedOutPath" -Target $scriptFile.Path -ScriptBlock { 92 | $scriptfile.WriteTo($resolvedOutPath, "") 93 | } -PSCmdlet $PSCmdlet -EnableException $EnableException -Continue 94 | continue 95 | } 96 | 97 | Invoke-PSFProtectedCommand -Action 'Replacing content of script' -Target $scriptFile.Path -ScriptBlock { 98 | $scriptfile.Save($Backup.ToBool()) 99 | } -PSCmdlet $PSCmdlet -EnableException $EnableException -Continue 100 | } 101 | #endregion From File 102 | 103 | #region From Content 104 | else { 105 | if ($OutPath) { 106 | Invoke-PSFProtectedCommand -Action "Writing updated script to $resolvedOutPath" -Target $scriptFile.Path -ScriptBlock { 107 | $scriptfile.WriteTo($resolvedOutPath, $scriptFile.Path) 108 | } -PSCmdlet $PSCmdlet -EnableException $EnableException -Continue 109 | continue 110 | } 111 | 112 | # Since it's already returned once for $PassThru, let's not double up here 113 | if (-not $PassThru) { 114 | [Refactor.Component.ScriptFileConverted]::new($tokenGroup[0].Result) 115 | } 116 | } 117 | #endregion From Content 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /Refactor/functions/readme.md: -------------------------------------------------------------------------------- 1 | # Functions 2 | 3 | This is the folder where the functions go. 4 | 5 | Depending on the complexity of the module, it is recommended to subdivide them into subfolders. 6 | 7 | The module will pick up all .ps1 files recursively -------------------------------------------------------------------------------- /Refactor/internal/configurations/configuration.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | This is an example configuration file 3 | 4 | By default, it is enough to have a single one of them, 5 | however if you have enough configuration settings to justify having multiple copies of it, 6 | feel totally free to split them into multiple files. 7 | #> 8 | 9 | <# 10 | # Example Configuration 11 | Set-PSFConfig -Module 'Refactor' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'" 12 | #> 13 | 14 | Set-PSFConfig -Module 'Refactor' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging." 15 | Set-PSFConfig -Module 'Refactor' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments." -------------------------------------------------------------------------------- /Refactor/internal/configurations/readme.md: -------------------------------------------------------------------------------- 1 | # Configurations 2 | 3 | Through the `PSFramework` you have a simple method that allows you to ... 4 | 5 | - Publish settings 6 | - With onboard documentation 7 | - Input validation 8 | - Scripts that run on change of settings 9 | - That can be discovered and updated by the user 10 | - That can be administrated by policy & DSC 11 | 12 | The configuration system is a bit too complex to describe in a help file, you can however visit us at http://psframework.org for detailed guidance. 13 | 14 | An example can be seen in the attached ps1 file -------------------------------------------------------------------------------- /Refactor/internal/functions/Find-BreakingChange.ps1: -------------------------------------------------------------------------------- 1 | function Find-BreakingChange { 2 | <# 3 | .SYNOPSIS 4 | Search a given AST for any breaking change contained. 5 | 6 | .DESCRIPTION 7 | Search a given AST for any breaking change contained. 8 | 9 | Use Import-ReBreakingChange to load definitions of breaking changes to look for. 10 | 11 | .PARAMETER Ast 12 | The AST to search 13 | 14 | .PARAMETER Name 15 | The name of the file being searched. 16 | Use this to identify non-filesystem code. 17 | 18 | .PARAMETER Changes 19 | The breaking changes to look out for. 20 | 21 | .EXAMPLE 22 | PS C:\> Find-BreakingChange -Ast $ast -Changes $changes 23 | 24 | Find all instances of breaking changes found within $ast. 25 | #> 26 | [OutputType([Refactor.BreakingChange])] 27 | [CmdletBinding()] 28 | param ( 29 | [Parameter(Mandatory = $true)] 30 | [System.Management.Automation.Language.Ast] 31 | $Ast, 32 | 33 | [string] 34 | $Name, 35 | 36 | [Parameter(Mandatory = $true)] 37 | [hashtable] 38 | $Changes 39 | ) 40 | 41 | if (-not $Name) { $Name = $Ast.Extent.File } 42 | $filePath = $Name 43 | $fileName = ($Name -split "\\|/")[-1] 44 | 45 | $commands = Read-ReScriptCommand -Ast $Ast 46 | foreach ($commandToken in $commands) { 47 | foreach ($change in $Changes[$commandToken.Name]) { 48 | if ($change.Parameters.Count -lt 1) { 49 | [Refactor.BreakingChange]@{ 50 | Path = $filePath 51 | Name = $fileName 52 | Line = $commandToken.Line 53 | Command = $commandToken.Name 54 | Type = 'Error' 55 | Description = $change.Description 56 | Module = $change.Module 57 | Version = $change.Version 58 | Tags = $change.Tags 59 | } 60 | continue 61 | } 62 | 63 | foreach ($parameter in $change.Parameters.Keys) { 64 | if ($commandToken.Parameters.Keys -contains $parameter) { 65 | [Refactor.BreakingChange]@{ 66 | Path = $filePath 67 | Name = $fileName 68 | Line = $commandToken.Line 69 | Command = $commandToken.Name 70 | Parameter = $parameter 71 | Type = 'Error' 72 | Description = $change.Parameters.$parameter 73 | Module = $change.Module 74 | Version = $change.Version 75 | Tags = $change.Tags 76 | } 77 | continue 78 | } 79 | 80 | if ($commandToken.ParametersKnown) { continue } 81 | 82 | [Refactor.BreakingChange]@{ 83 | Path = $filePath 84 | Name = $fileName 85 | Line = $commandToken.Line 86 | Command = $commandToken.Name 87 | Parameter = $parameter 88 | Type = 'Warning' 89 | Description = "Not all parameters on command resolveable - might be in use. $($change.Parameters.$parameter)" 90 | Module = $change.Module 91 | Version = $change.Version 92 | Tags = $change.Tags 93 | } 94 | } 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /Refactor/internal/functions/Get-AstCommand.ps1: -------------------------------------------------------------------------------- 1 | function Get-AstCommand { 2 | <# 3 | .SYNOPSIS 4 | Parses out all commands contained in an AST. 5 | 6 | .DESCRIPTION 7 | Parses out all commands contained in an Abstract Syntax Tree. 8 | Will also resolve all parameters used as able and indicate, whether all could be identified. 9 | 10 | .PARAMETER Ast 11 | The Ast object to scan. 12 | 13 | .PARAMETER Splat 14 | Splat Data to use for parameter mapping 15 | 16 | .EXAMPLE 17 | PS C:\> Get-AstCommand -Ast $parsed.Ast -Splat $splats 18 | 19 | Returns all commands in the specified AST, mapping to the splats contained in $splats 20 | #> 21 | [OutputType([Refactor.CommandToken])] 22 | [CmdletBinding()] 23 | Param ( 24 | [Parameter(Mandatory = $true)] 25 | [System.Management.Automation.Language.Ast] 26 | $Ast, 27 | 28 | [AllowNull()] 29 | $Splat 30 | ) 31 | 32 | process { 33 | $splatHash = @{ } 34 | foreach ($splatItem in $Splat) { $splatHash[$splatItem.Ast] = $splatItem } 35 | 36 | $allCommands = Search-ReAst -Ast $Ast -Filter { 37 | $args[0] -is [System.Management.Automation.Language.CommandAst] 38 | } 39 | 40 | foreach ($command in $allCommands) { 41 | $result = [Refactor.CommandToken]::new($command.Data) 42 | 43 | # Splats 44 | foreach ($splatted in $command.Data.CommandElements | Where-Object Splatted) { 45 | $result.HasSplat = $true 46 | $splatItem = $splatHash[$splatted] 47 | if (-not $splatItem.ParametersKnown) { 48 | $result.ParametersKnown = $false 49 | } 50 | foreach ($parameterName in $splatItem.Parameters.Keys) { 51 | $result.parameters[$parameterName] = $parameterName 52 | } 53 | $result.Splats[$splatted] = $splatItem 54 | } 55 | 56 | $result 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /Refactor/internal/functions/readme.md: -------------------------------------------------------------------------------- 1 | # Functions 2 | 3 | This is the folder where the internal functions go. 4 | 5 | Depending on the complexity of the module, it is recommended to subdivide them into subfolders. 6 | 7 | The module will pick up all .ps1 files recursively -------------------------------------------------------------------------------- /Refactor/internal/scriptblocks/scriptblocks.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Stored scriptblocks are available in [PsfValidateScript()] attributes. 3 | This makes it easier to centrally provide the same scriptblock multiple times, 4 | without having to maintain it in separate locations. 5 | 6 | It also prevents lengthy validation scriptblocks from making your parameter block 7 | hard to read. 8 | 9 | Set-PSFScriptblock -Name 'Refactor.ScriptBlockName' -Scriptblock { 10 | 11 | } 12 | #> -------------------------------------------------------------------------------- /Refactor/internal/scripts/license.ps1: -------------------------------------------------------------------------------- 1 | New-PSFLicense -Product 'Refactor' -Manufacturer 'Friedrich Weinmann' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2022-03-05") -Text @" 2 | Copyright (c) 2022 Friedrich Weinmann 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | "@ -------------------------------------------------------------------------------- /Refactor/internal/scripts/postimport.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Add all things you want to run after importing the main function code 3 | 4 | WARNING: ONLY provide paths to files! 5 | 6 | After building the module, this file will be completely ignored, adding anything but paths to files ... 7 | - Will not work after publishing 8 | - Could break the build process 9 | #> 10 | 11 | $moduleRoot = Split-Path (Split-Path $PSScriptRoot) 12 | 13 | # Load Configurations 14 | (Get-ChildItem "$moduleRoot\internal\configurations\*.ps1" -ErrorAction Ignore).FullName 15 | 16 | # Load Scriptblocks 17 | (Get-ChildItem "$moduleRoot\internal\scriptblocks\*.ps1" -ErrorAction Ignore).FullName 18 | 19 | # Load Tab Expansion 20 | (Get-ChildItem "$moduleRoot\internal\tepp\*.tepp.ps1" -ErrorAction Ignore).FullName 21 | 22 | # Load Tab Expansion Assignment 23 | "$moduleRoot\internal\tepp\assignment.ps1" 24 | 25 | # Load License 26 | "$moduleRoot\internal\scripts\license.ps1" 27 | 28 | # Load Variable definitions 29 | "$moduleRoot\internal\scripts\variables.ps1" 30 | 31 | # Load Token Providers 32 | (Get-ChildItem "$moduleRoot\internal\tokenprovider\*.ps1" -ErrorAction Ignore).FullName -------------------------------------------------------------------------------- /Refactor/internal/scripts/preimport.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Add all things you want to run before importing the main function code. 3 | 4 | WARNING: ONLY provide paths to files! 5 | 6 | After building the module, this file will be completely ignored, adding anything but paths to files ... 7 | - Will not work after publishing 8 | - Could break the build process 9 | #> 10 | 11 | $moduleRoot = Split-Path (Split-Path $PSScriptRoot) 12 | 13 | # Load the strings used in messages 14 | "$moduleRoot\internal\scripts\strings.ps1" -------------------------------------------------------------------------------- /Refactor/internal/scripts/strings.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | This file loads the strings documents from the respective language folders. 3 | This allows localizing messages and errors. 4 | Load psd1 language files for each language you wish to support. 5 | Partial translations are acceptable - when missing a current language message, 6 | it will fallback to English or another available language. 7 | #> 8 | Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'Refactor' -Language 'en-US' -------------------------------------------------------------------------------- /Refactor/internal/scripts/variables.ps1: -------------------------------------------------------------------------------- 1 | # Module-wide storage for token provider scriptblocks 2 | $script:tokenProviders = @{ } 3 | 4 | # Transformation rules for tokens 5 | $script:tokenTransformations = @{ } 6 | 7 | # Container for Breaking Change data 8 | $script:breakingChanges = @{ } -------------------------------------------------------------------------------- /Refactor/internal/tepp/assignment.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | # Example: 3 | Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name Refactor.alcohol 4 | #> -------------------------------------------------------------------------------- /Refactor/internal/tepp/astTypes.tepp.ps1: -------------------------------------------------------------------------------- 1 | Register-PSFTeppScriptblock -Name 'Refactor.AstTypes' -ScriptBlock { 2 | 'Ast' 3 | 'SequencePointAst' 4 | 'ScriptBlockAst' 5 | 'ParamBlockAst' 6 | 'NamedBlockAst' 7 | 'NamedAttributeArgumentAst' 8 | 'AttributeBaseAst' 9 | 'AttributeAst' 10 | 'TypeConstraintAst' 11 | 'ParameterAst' 12 | 'StatementBlockAst' 13 | 'StatementAst' 14 | 'TypeDefinitionAst' 15 | 'UsingStatementAst' 16 | 'FunctionDefinitionAst' 17 | 'IfStatementAst' 18 | 'DataStatementAst' 19 | 'LabeledStatementAst' 20 | 'LoopStatementAst' 21 | 'ForEachStatementAst' 22 | 'ForStatementAst' 23 | 'DoWhileStatementAst' 24 | 'DoUntilStatementAst' 25 | 'WhileStatementAst' 26 | 'SwitchStatementAst' 27 | 'TryStatementAst' 28 | 'TrapStatementAst' 29 | 'BreakStatementAst' 30 | 'ContinueStatementAst' 31 | 'ReturnStatementAst' 32 | 'ExitStatementAst' 33 | 'ThrowStatementAst' 34 | 'PipelineBaseAst' 35 | 'ErrorStatementAst' 36 | 'ChainableAst' 37 | 'PipelineChainAst' 38 | 'PipelineAst' 39 | 'AssignmentStatementAst' 40 | 'CommandBaseAst' 41 | 'CommandAst' 42 | 'CommandExpressionAst' 43 | 'ConfigurationDefinitionAst' 44 | 'DynamicKeywordStatementAst' 45 | 'BlockStatementAst' 46 | 'MemberAst' 47 | 'PropertyMemberAst' 48 | 'FunctionMemberAst' 49 | 'CompilerGeneratedMemberFunctionAst' 50 | 'CatchClauseAst' 51 | 'CommandElementAst' 52 | 'CommandParameterAst' 53 | 'ExpressionAst' 54 | 'ErrorExpressionAst' 55 | 'TernaryExpressionAst' 56 | 'BinaryExpressionAst' 57 | 'UnaryExpressionAst' 58 | 'AttributedExpressionAst' 59 | 'ConvertExpressionAst' 60 | 'MemberExpressionAst' 61 | 'InvokeMemberExpressionAst' 62 | 'BaseCtorInvokeMemberExpressionAst' 63 | 'TypeExpressionAst' 64 | 'VariableExpressionAst' 65 | 'ConstantExpressionAst' 66 | 'StringConstantExpressionAst' 67 | 'ExpandableStringExpressionAst' 68 | 'ScriptBlockExpressionAst' 69 | 'ArrayLiteralAst' 70 | 'HashtableAst' 71 | 'ArrayExpressionAst' 72 | 'ParenExpressionAst' 73 | 'SubExpressionAst' 74 | 'UsingExpressionAst' 75 | 'IndexExpressionAst' 76 | 'RedirectionAst' 77 | 'MergingRedirectionAst' 78 | 'FileRedirectionAst' 79 | 'AssignmentTarget' 80 | } -------------------------------------------------------------------------------- /Refactor/internal/tepp/provider.tepp.ps1: -------------------------------------------------------------------------------- 1 | Register-PSFTeppScriptblock -Name 'Refactor.TokenProvider' -ScriptBlock { 2 | (Get-ReTokenProvider).Name 3 | } -------------------------------------------------------------------------------- /Refactor/internal/tepp/readme.md: -------------------------------------------------------------------------------- 1 | # Tab Expansion 2 | 3 | ## Description 4 | 5 | Modern Tab Expansion was opened to users with the module `Tab Expansion Plus Plus` (TEPP). 6 | 7 | It allows you to define, what options a user is offered when tabbing through input options. This can save a lot of time for the user and is considered a key element in user experience. 8 | 9 | The `PSFramework` offers a simplified way of offering just this, as the two example files show. 10 | 11 | ## Concept 12 | 13 | Custom tab completion is defined in two steps: 14 | 15 | - Define a scriptblock that is run when the user hits `TAB` and provides the strings that are his options. 16 | - Assign that scriptblock to the parameter of a command. You can assign the same scriptblock multiple times. 17 | 18 | ## Structure 19 | 20 | Import order matters. In order to make things work with the default scaffold, follow those rules: 21 | 22 | - All scriptfiles _defining_ completion scriptblocks like this: `*.tepp.ps1` 23 | - Put all your completion assignments in `assignment.ps1` -------------------------------------------------------------------------------- /Refactor/internal/tokenProvider/ast.token.ps1: -------------------------------------------------------------------------------- 1 | $tokenizer = { 2 | param ( 3 | $Ast 4 | ) 5 | 6 | $astTypes = Get-ReTokenTransformationSet -Type Ast | ForEach-Object TypeName 7 | 8 | $astObjects = Search-ReAst -Ast $Ast -Filter { 9 | $args[0].GetType().Name -in $astTypes 10 | } 11 | 12 | foreach ($astObject in $astObjects.Data) { 13 | [Refactor.AstToken]::new($astObject) 14 | } 15 | } 16 | $converter = { 17 | param ( 18 | [Refactor.ScriptToken] 19 | $Token, 20 | 21 | $Preview 22 | ) 23 | 24 | <# 25 | The AST Token is special in that it expects the actual changes to be applied not by configuration but manually outside of the process. 26 | As such it is pointless to use in the full, config-only driven workflow of Convert-ReScriptFile. 27 | Instead, manually creating the scriptfile object and executing the workflows is the way to go here. 28 | #> 29 | 30 | # Return changes 31 | $Token.GetChanges() 32 | } 33 | 34 | $parameters = @( 35 | 'TypeName' 36 | ) 37 | $param = @{ 38 | Name = 'Ast' 39 | TransformIndex = 'TypeName' 40 | ParametersMandatory = 'TypeName' 41 | Parameters = $parameters 42 | Tokenizer = $tokenizer 43 | Converter = $converter 44 | } 45 | Register-ReTokenProvider @param -------------------------------------------------------------------------------- /Refactor/internal/tokenProvider/command.token.ps1: -------------------------------------------------------------------------------- 1 | $tokenizer = { 2 | Read-ReScriptCommand -Ast $args[0] 3 | } 4 | $converter = { 5 | param ( 6 | [Refactor.ScriptToken] 7 | $Token 8 | ) 9 | $transform = Get-ReTokenTransformationSet -Type Command | Where-Object Name -EQ $Token.Name 10 | 11 | if ($transform.MsgInfo) { 12 | $Token.WriteMessage('Information', $transform.MsgInfo, $transform) 13 | } 14 | if ($transform.MsgWarning) { 15 | $Token.WriteMessage('Warning', $transform.MsgWarning, $transform) 16 | } 17 | if ($transform.MsgError) { 18 | $Token.WriteMessage('Error', $transform.MsgError, $transform) 19 | } 20 | 21 | $changed = $false 22 | $items = foreach ($commandElement in $Token.Ast.CommandElements) { 23 | # Command itself 24 | if ($commandElement -eq $Token.Ast.CommandElements[0]) { 25 | if ($transform.NewName) { $transform.NewName; $changed = $true } 26 | else { $commandElement.Value } 27 | continue 28 | } 29 | 30 | if ($commandElement -isnot [System.Management.Automation.Language.CommandParameterAst]) { 31 | $commandElement.Extent.Text 32 | continue 33 | } 34 | if (-not $transform.Parameters) { 35 | $commandElement.Extent.Text 36 | continue 37 | } 38 | # Not guaranteed to be a hashtable 39 | $transform.Parameters = $transform.Parameters | ConvertTo-PSFHashtable 40 | if (-not $transform.Parameters[$commandElement.ParameterName]) { 41 | $commandElement.Extent.Text 42 | continue 43 | } 44 | 45 | "-$($transform.Parameters[$commandElement.ParameterName])" 46 | $changed = $true 47 | } 48 | 49 | #region Conditional Messages 50 | if ($transform.InfoParameters) { $transform.InfoParameters | ConvertTo-PSFHashtable } 51 | foreach ($parameter in $transform.InfoParameters.Keys) { 52 | if ($Token.Parameters[$parameter]) { 53 | $Token.WriteMessage('Information', $transform.InfoParameters[$parameter], $transform) 54 | } 55 | } 56 | if ($transform.WarningParameters) { $transform.WarningParameters | ConvertTo-PSFHashtable } 57 | foreach ($parameter in $transform.WarningParameters.Keys) { 58 | if ($Token.Parameters[$parameter]) { 59 | $Token.WriteMessage('Warning', $transform.WarningParameters[$parameter], $transform) 60 | } 61 | } 62 | if ($transform.ErrorParameters) { $transform.ErrorParameters | ConvertTo-PSFHashtable } 63 | foreach ($parameter in $transform.ErrorParameters.Keys) { 64 | if ($Token.Parameters[$parameter]) { 65 | $Token.WriteMessage('Error', $transform.ErrorParameters[$parameter], $transform) 66 | } 67 | } 68 | if (-not $Token.ParametersKnown) { 69 | if ($transform.UnknownInfo) { 70 | $Token.WriteMessage('Information', $transform.UnknownInfo, $transform) 71 | } 72 | if ($transform.UnknownWarning) { 73 | $Token.WriteMessage('Warning', $transform.UnknownInfo, $transform) 74 | } 75 | if ($transform.UnknownError) { 76 | $Token.WriteMessage('Error', $transform.UnknownInfo, $transform) 77 | } 78 | } 79 | #endregion Conditional Messages 80 | 81 | $Token.NewText = $items -join " " 82 | if (-not $changed) { $Token.NewText = $Token.Text } 83 | 84 | #region Add changes for splat properties 85 | foreach ($property in $Token.Splats.Values.Parameters.Keys) { 86 | if ($transform.Parameters.Keys -notcontains $property) { continue } 87 | 88 | foreach ($ast in $Token.Splats.Values.Assignments) { 89 | #region Case: Method Invocation 90 | if ($ast -is [System.Management.Automation.Language.InvokeMemberExpressionAst]) { 91 | if ($ast.Arguments[0].Value -ne $property) { continue } 92 | $Token.AddChange($ast.Arguments[0].Extent.Text, ("'{0}'" -f ($transform.Parameters[$property] -replace "^'|'$|^`"|`"$")), $ast.Arguments[0].Extent.StartOffset, $ast) 93 | continue 94 | } 95 | #endregion Case: Method Invocation 96 | 97 | #region Case: Original assignment 98 | if ($ast.Left -is [System.Management.Automation.Language.VariableExpressionAst]) { 99 | foreach ($hashKey in $ast.Right.Expression.KeyValuePairs.Item1) { 100 | if ($hashKey.Value -ne $property) { continue } 101 | $Token.AddChange($hashKey.Extent.Text, ("'{0}'" -f ($transform.Parameters[$property] -replace "^'|'$|^`"|`"$")), $hashKey.Extent.StartOffset, $hashKey) 102 | } 103 | continue 104 | } 105 | #endregion Case: Original assignment 106 | 107 | #region Case: Property assignment 108 | if ($ast.Left -is [System.Management.Automation.Language.MemberExpressionAst]) { 109 | if ($ast.Left.Member.Value -ne $property) { continue } 110 | $Token.AddChange($ast.Left.Member.Extent.Text, $transform.Parameters[$property], $ast.Left.Member.Extent.StartOffset, $ast) 111 | continue 112 | } 113 | #endregion Case: Property assignment 114 | 115 | #region Case: Index assignment 116 | if ($ast.Left -is [System.Management.Automation.Language.IndexExpressionAst]) { 117 | if ($ast.Left.Index.Value -ne $property) { continue } 118 | $Token.AddChange($ast.Left.Index.Extent.Text, ("'{0}'" -f ($transform.Parameters[$property] -replace "^'|'$|^`"|`"$")), $ast.Left.Index.Extent.StartOffset, $ast) 119 | continue 120 | } 121 | #endregion Case: Index assignment 122 | } 123 | } 124 | #endregion Add changes for splat properties 125 | 126 | # Return changes 127 | $Token.GetChanges() 128 | } 129 | $parameters = @( 130 | 'Name' 131 | 'NewName' 132 | 'Parameters' 133 | 134 | 'MsgInfo' 135 | 'MsgWarning' 136 | 'MsgError' 137 | 138 | 'InfoParameters' 139 | 'WarningParameters' 140 | 'ErrorParameters' 141 | 142 | 'UnknownInfo' 143 | 'UnknownWarning' 144 | 'UnknownError' 145 | ) 146 | $param = @{ 147 | Name = 'Command' 148 | TransformIndex = 'Name' 149 | ParametersMandatory = 'Name' 150 | Parameters = $parameters 151 | Tokenizer = $tokenizer 152 | Converter = $converter 153 | } 154 | Register-ReTokenProvider @param -------------------------------------------------------------------------------- /Refactor/internal/tokenProvider/function.token.ps1: -------------------------------------------------------------------------------- 1 | $tokenizer = { 2 | param ( 3 | $Ast 4 | ) 5 | 6 | $functionAsts = Search-ReAst -Ast $Ast -Filter { 7 | $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] 8 | } 9 | 10 | foreach ($functionAst in $functionAsts.Data) { 11 | [Refactor.FunctionToken]::new($functionAst) 12 | } 13 | } 14 | $converter = { 15 | param ( 16 | [Refactor.ScriptToken] 17 | $Token, 18 | 19 | $Preview 20 | ) 21 | 22 | $transform = Get-ReTokenTransformationSet -Type Function | Where-Object Name -EQ $Token.Name 23 | if (-not $transform) { return } 24 | 25 | #region Function Name 26 | if ($transform.NewName) { 27 | $startIndex = $Token.Ast.Extent.Text.IndexOf($Token.Ast.Name) + $Token.Ast.Extent.StartOffset 28 | $Token.AddChange($Token.Ast.Name, $transform.NewName, $startIndex, $null) 29 | 30 | $helpData = $Token.Ast.GetHelpContent() 31 | $endOffset = $Token.Ast.Body.ParamBlock.Extent.StartOffset 32 | if (-not $endOffset) { $Token.Ast.Body.DynamicParamBlock.Extent.StartOffset } 33 | if (-not $endOffset) { $Token.Ast.Body.BeginBlock.Extent.StartOffset } 34 | if (-not $endOffset) { $Token.Ast.Body.ProcessBlock.Extent.StartOffset } 35 | if (-not $endOffset) { $Token.Ast.Body.EndBlock.Extent.StartOffset } 36 | 37 | foreach ($example in $helpData.Examples) { 38 | foreach ($line in $example -split "`n") { 39 | if ($line -notmatch "\b$($Token.Ast.Name)\b") { continue } 40 | $lineIndex = $Token.Ast.Extent.Text.Indexof($line) 41 | $commandIndex = ($line -split "\b$($Token.Ast.Name)\b")[0].Length 42 | # Hard-Prevent editing in function body. 43 | # Renaming references, including recursive references, is responsibility of the Command token 44 | if (($lineIndex + $line.Length) -gt $endOffset) { continue } 45 | 46 | $Token.AddChange($Token.Ast.Name, $transform.NewName, ($lineIndex + $commandIndex), $null) 47 | } 48 | } 49 | } 50 | #endregion Function Name 51 | 52 | # Return changes 53 | $Token.GetChanges() 54 | } 55 | $parameters = @( 56 | 'Name' 57 | 'NewName' 58 | ) 59 | $param = @{ 60 | Name = 'Function' 61 | TransformIndex = 'Name' 62 | ParametersMandatory = 'Name' 63 | Parameters = $parameters 64 | Tokenizer = $tokenizer 65 | Converter = $converter 66 | } 67 | Register-ReTokenProvider @param -------------------------------------------------------------------------------- /Refactor/readme.md: -------------------------------------------------------------------------------- 1 | # PSFModule guidance 2 | 3 | This is a finished module layout optimized for implementing the PSFramework. 4 | 5 | If you don't care to deal with the details, this is what you need to do to get started seeing results: 6 | 7 | - Add the functions you want to publish to `/functions/` 8 | - Update the `FunctionsToExport` node in the module manifest (Refactor.psd1). All functions you want to publish should be in a list. 9 | - Add internal helper functions the user should not see to `/internal/functions/` 10 | 11 | ## Path Warning 12 | 13 | > If you want your module to be compatible with Linux and MacOS, keep in mind that those OS are case sensitive for paths and files. 14 | 15 | `Import-ModuleFile` is preconfigured to resolve the path of the files specified, so it will reliably convert weird path notations the system can't handle. 16 | Content imported through that command thus need not mind the path separator. 17 | If you want to make sure your code too will survive OS-specific path notations, get used to using `Resolve-path` or the more powerful `Resolve-PSFPath`. -------------------------------------------------------------------------------- /Refactor/tests/functions/readme.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | This is where the function tests go. 4 | 5 | Make sure to put them in folders reflecting the actual module structure. 6 | 7 | It is not necessary to differentiate between internal and public functions here. -------------------------------------------------------------------------------- /Refactor/tests/general/FileIntegrity.Exceptions.ps1: -------------------------------------------------------------------------------- 1 | # List of forbidden commands 2 | $global:BannedCommands = @( 3 | 'Write-Host' 4 | 'Write-Verbose' 5 | 'Write-Warning' 6 | 'Write-Error' 7 | 'Write-Output' 8 | 'Write-Information' 9 | 'Write-Debug' 10 | 11 | # Use CIM instead where possible 12 | 'Get-WmiObject' 13 | 'Invoke-WmiMethod' 14 | 'Register-WmiEvent' 15 | 'Remove-WmiObject' 16 | 'Set-WmiInstance' 17 | 18 | # Use Get-WinEvent instead 19 | 'Get-EventLog' 20 | ) 21 | 22 | <# 23 | Contains list of exceptions for banned cmdlets. 24 | Insert the file names of files that may contain them. 25 | 26 | Example: 27 | "Write-Host" = @('Write-PSFHostColor.ps1','Write-PSFMessage.ps1') 28 | #> 29 | $global:MayContainCommand = @{ 30 | "Write-Host" = @() 31 | "Write-Verbose" = @() 32 | "Write-Warning" = @() 33 | "Write-Error" = @() 34 | "Write-Output" = @('Get-ReBreakingChange.ps1') 35 | "Write-Information" = @() 36 | "Write-Debug" = @() 37 | } -------------------------------------------------------------------------------- /Refactor/tests/general/FileIntegrity.Tests.ps1: -------------------------------------------------------------------------------- 1 | $moduleRoot = (Resolve-Path "$global:testroot\..").Path 2 | 3 | . "$global:testroot\general\FileIntegrity.Exceptions.ps1" 4 | 5 | Describe "Verifying integrity of module files" { 6 | BeforeAll { 7 | function Get-FileEncoding 8 | { 9 | <# 10 | .SYNOPSIS 11 | Tests a file for encoding. 12 | 13 | .DESCRIPTION 14 | Tests a file for encoding. 15 | 16 | .PARAMETER Path 17 | The file to test 18 | #> 19 | [CmdletBinding()] 20 | Param ( 21 | [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)] 22 | [Alias('FullName')] 23 | [string] 24 | $Path 25 | ) 26 | 27 | if ($PSVersionTable.PSVersion.Major -lt 6) 28 | { 29 | [byte[]]$byte = get-content -Encoding byte -ReadCount 4 -TotalCount 4 -Path $Path 30 | } 31 | else 32 | { 33 | [byte[]]$byte = Get-Content -AsByteStream -ReadCount 4 -TotalCount 4 -Path $Path 34 | } 35 | 36 | if ($byte[0] -eq 0xef -and $byte[1] -eq 0xbb -and $byte[2] -eq 0xbf) { 'UTF8 BOM' } 37 | elseif ($byte[0] -eq 0xfe -and $byte[1] -eq 0xff) { 'Unicode' } 38 | elseif ($byte[0] -eq 0 -and $byte[1] -eq 0 -and $byte[2] -eq 0xfe -and $byte[3] -eq 0xff) { 'UTF32' } 39 | elseif ($byte[0] -eq 0x2b -and $byte[1] -eq 0x2f -and $byte[2] -eq 0x76) { 'UTF7' } 40 | else { 'Unknown' } 41 | } 42 | } 43 | 44 | Context "Validating PS1 Script files" { 45 | $allFiles = Get-ChildItem -Path $moduleRoot -Recurse | Where-Object Name -like "*.ps1" | Where-Object FullName -NotLike "$moduleRoot\tests\*" 46 | 47 | foreach ($file in $allFiles) 48 | { 49 | $name = $file.FullName.Replace("$moduleRoot\", '') 50 | 51 | It "[$name] Should have UTF8 encoding with Byte Order Mark" -TestCases @{ file = $file } { 52 | Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8 BOM' 53 | } 54 | 55 | It "[$name] Should have no trailing space" -TestCases @{ file = $file } { 56 | ($file | Select-String "\s$" | Where-Object { $_.Line.Trim().Length -gt 0}).LineNumber | Should -BeNullOrEmpty 57 | } 58 | 59 | $tokens = $null 60 | $parseErrors = $null 61 | $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$parseErrors) 62 | 63 | It "[$name] Should have no syntax errors" -TestCases @{ parseErrors = $parseErrors } { 64 | $parseErrors | Should -BeNullOrEmpty 65 | } 66 | 67 | foreach ($command in $global:BannedCommands) 68 | { 69 | if ($global:MayContainCommand["$command"] -notcontains $file.Name) 70 | { 71 | It "[$name] Should not use $command" -TestCases @{ tokens = $tokens; command = $command } { 72 | $tokens | Where-Object Text -EQ $command | Should -BeNullOrEmpty 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | Context "Validating help.txt help files" { 80 | $allFiles = Get-ChildItem -Path $moduleRoot -Recurse | Where-Object Name -like "*.help.txt" | Where-Object FullName -NotLike "$moduleRoot\tests\*" 81 | 82 | foreach ($file in $allFiles) 83 | { 84 | $name = $file.FullName.Replace("$moduleRoot\", '') 85 | 86 | It "[$name] Should have UTF8 encoding" -TestCases @{ file = $file } { 87 | Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8 BOM' 88 | } 89 | 90 | It "[$name] Should have no trailing space" -TestCases @{ file = $file } { 91 | ($file | Select-String "\s$" | Where-Object { $_.Line.Trim().Length -gt 0 } | Measure-Object).Count | Should -Be 0 92 | } 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /Refactor/tests/general/Help.Exceptions.ps1: -------------------------------------------------------------------------------- 1 | # List of functions that should be ignored 2 | $global:FunctionHelpTestExceptions = @( 3 | 4 | ) 5 | 6 | <# 7 | List of arrayed enumerations. These need to be treated differently. Add full name. 8 | Example: 9 | 10 | "Sqlcollaborative.Dbatools.Connection.ManagementConnectionType[]" 11 | #> 12 | $global:HelpTestEnumeratedArrays = @( 13 | 14 | ) 15 | 16 | <# 17 | Some types on parameters just fail their validation no matter what. 18 | For those it becomes possible to skip them, by adding them to this hashtable. 19 | Add by following this convention: = @() 20 | Example: 21 | 22 | "Get-DbaCmObject" = @("DoNotUse") 23 | #> 24 | $global:HelpTestSkipParameterType = @{ 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Refactor/tests/general/Help.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .NOTES 3 | The original test this is based upon was written by June Blender. 4 | After several rounds of modifications it stands now as it is, but the honor remains hers. 5 | 6 | Thank you June, for all you have done! 7 | 8 | .DESCRIPTION 9 | This test evaluates the help for all commands in a module. 10 | 11 | .PARAMETER SkipTest 12 | Disables this test. 13 | 14 | .PARAMETER CommandPath 15 | List of paths under which the script files are stored. 16 | This test assumes that all functions have their own file that is named after themselves. 17 | These paths are used to search for commands that should exist and be tested. 18 | Will search recursively and accepts wildcards, make sure only functions are found 19 | 20 | .PARAMETER ModuleName 21 | Name of the module to be tested. 22 | The module must already be imported 23 | 24 | .PARAMETER ExceptionsFile 25 | File in which exceptions and adjustments are configured. 26 | In it there should be two arrays and a hashtable defined: 27 | $global:FunctionHelpTestExceptions 28 | $global:HelpTestEnumeratedArrays 29 | $global:HelpTestSkipParameterType 30 | These can be used to tweak the tests slightly in cases of need. 31 | See the example file for explanations on each of these usage and effect. 32 | #> 33 | [CmdletBinding()] 34 | Param ( 35 | [switch] 36 | $SkipTest, 37 | 38 | [string[]] 39 | $CommandPath = @("$global:testroot\..\functions", "$global:testroot\..\internal\functions"), 40 | 41 | [string] 42 | $ModuleName = "Refactor", 43 | 44 | [string] 45 | $ExceptionsFile = "$global:testroot\general\Help.Exceptions.ps1" 46 | ) 47 | if ($SkipTest) { return } 48 | . $ExceptionsFile 49 | 50 | $includedNames = (Get-ChildItem $CommandPath -Recurse -File | Where-Object Name -like "*.ps1").BaseName 51 | $commandTypes = @('Cmdlet', 'Function') 52 | if ($PSVersionTable.PSEdition -eq 'Desktop' ) { $commandTypes += 'Workflow' } 53 | $commands = Get-Command -Module (Get-Module $ModuleName) -CommandType $commandTypes | Where-Object Name -In $includedNames 54 | 55 | ## When testing help, remember that help is cached at the beginning of each session. 56 | ## To test, restart session. 57 | 58 | 59 | foreach ($command in $commands) { 60 | $commandName = $command.Name 61 | 62 | # Skip all functions that are on the exclusions list 63 | if ($global:FunctionHelpTestExceptions -contains $commandName) { continue } 64 | 65 | # The module-qualified command fails on Microsoft.PowerShell.Archive cmdlets 66 | $Help = Get-Help $commandName -ErrorAction SilentlyContinue 67 | 68 | Describe "Test help for $commandName" { 69 | 70 | # If help is not found, synopsis in auto-generated help is the syntax diagram 71 | It "should not be auto-generated" -TestCases @{ Help = $Help } { 72 | $Help.Synopsis | Should -Not -BeLike '*`[``]*' 73 | } 74 | 75 | # Should be a description for every function 76 | It "gets description for $commandName" -TestCases @{ Help = $Help } { 77 | $Help.Description | Should -Not -BeNullOrEmpty 78 | } 79 | 80 | # Should be at least one example 81 | It "gets example code from $commandName" -TestCases @{ Help = $Help } { 82 | ($Help.Examples.Example | Select-Object -First 1).Code | Should -Not -BeNullOrEmpty 83 | } 84 | 85 | # Should be at least one example description 86 | It "gets example help from $commandName" -TestCases @{ Help = $Help } { 87 | ($Help.Examples.Example.Remarks | Select-Object -First 1).Text | Should -Not -BeNullOrEmpty 88 | } 89 | 90 | Context "Test parameter help for $commandName" { 91 | 92 | $common = 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutBuffer', 'OutVariable', 'PipelineVariable', 'Verbose', 'WarningAction', 'WarningVariable' 93 | 94 | $parameters = $command.ParameterSets.Parameters | Sort-Object -Property Name -Unique | Where-Object Name -notin $common 95 | $parameterNames = $parameters.Name 96 | $HelpParameterNames = $Help.Parameters.Parameter.Name | Sort-Object -Unique 97 | foreach ($parameter in $parameters) { 98 | $parameterName = $parameter.Name 99 | $parameterHelp = $Help.parameters.parameter | Where-Object Name -EQ $parameterName 100 | 101 | # Should be a description for every parameter 102 | It "gets help for parameter: $parameterName : in $commandName" -TestCases @{ parameterHelp = $parameterHelp } { 103 | $parameterHelp.Description.Text | Should -Not -BeNullOrEmpty 104 | } 105 | 106 | $codeMandatory = $parameter.IsMandatory.toString() 107 | It "help for $parameterName parameter in $commandName has correct Mandatory value" -TestCases @{ parameterHelp = $parameterHelp; codeMandatory = $codeMandatory } { 108 | $parameterHelp.Required | Should -Be $codeMandatory 109 | } 110 | 111 | if ($HelpTestSkipParameterType[$commandName] -contains $parameterName) { continue } 112 | 113 | $codeType = $parameter.ParameterType.Name 114 | 115 | if ($parameter.ParameterType.IsEnum) { 116 | # Enumerations often have issues with the typename not being reliably available 117 | $names = $parameter.ParameterType::GetNames($parameter.ParameterType) 118 | # Parameter type in Help should match code 119 | It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ parameterHelp = $parameterHelp; names = $names } { 120 | $parameterHelp.parameterValueGroup.parameterValue | Should -be $names 121 | } 122 | } 123 | elseif ($parameter.ParameterType.FullName -in $HelpTestEnumeratedArrays) { 124 | # Enumerations often have issues with the typename not being reliably available 125 | $names = [Enum]::GetNames($parameter.ParameterType.DeclaredMembers[0].ReturnType) 126 | It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ parameterHelp = $parameterHelp; names = $names } { 127 | $parameterHelp.parameterValueGroup.parameterValue | Should -be $names 128 | } 129 | } 130 | else { 131 | # To avoid calling Trim method on a null object. 132 | $helpType = if ($parameterHelp.parameterValue) { $parameterHelp.parameterValue.Trim() } 133 | # Parameter type in Help should match code 134 | It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ helpType = $helpType; codeType = $codeType } { 135 | $helpType | Should -be $codeType 136 | } 137 | } 138 | } 139 | foreach ($helpParm in $HelpParameterNames) { 140 | # Shouldn't find extra parameters in help. 141 | It "finds help parameter in code: $helpParm" -TestCases @{ helpParm = $helpParm; parameterNames = $parameterNames } { 142 | $helpParm -in $parameterNames | Should -Be $true 143 | } 144 | } 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /Refactor/tests/general/Manifest.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe "Validating the module manifest" { 2 | $moduleRoot = (Resolve-Path "$global:testroot\..").Path 3 | $manifest = ((Get-Content "$moduleRoot\Refactor.psd1") -join "`n") | Invoke-Expression 4 | Context "Basic resources validation" { 5 | $files = Get-ChildItem "$moduleRoot\functions" -Recurse -File | Where-Object Name -like "*.ps1" 6 | It "Exports all functions in the public folder" -TestCases @{ files = $files; manifest = $manifest } { 7 | 8 | $functions = (Compare-Object -ReferenceObject $files.BaseName -DifferenceObject $manifest.FunctionsToExport | Where-Object SideIndicator -Like '<=').InputObject 9 | $functions | Should -BeNullOrEmpty 10 | } 11 | It "Exports no function that isn't also present in the public folder" -TestCases @{ files = $files; manifest = $manifest } { 12 | $functions = (Compare-Object -ReferenceObject $files.BaseName -DifferenceObject $manifest.FunctionsToExport | Where-Object SideIndicator -Like '=>').InputObject 13 | $functions | Should -BeNullOrEmpty 14 | } 15 | 16 | It "Exports none of its internal functions" -TestCases @{ moduleRoot = $moduleRoot; manifest = $manifest } { 17 | $files = Get-ChildItem "$moduleRoot\internal\functions" -Recurse -File -Filter "*.ps1" 18 | $files | Where-Object BaseName -In $manifest.FunctionsToExport | Should -BeNullOrEmpty 19 | } 20 | } 21 | 22 | Context "Individual file validation" { 23 | It "The root module file exists" -TestCases @{ moduleRoot = $moduleRoot; manifest = $manifest } { 24 | Test-Path "$moduleRoot\$($manifest.RootModule)" | Should -Be $true 25 | } 26 | 27 | foreach ($format in $manifest.FormatsToProcess) 28 | { 29 | It "The file $format should exist" -TestCases @{ moduleRoot = $moduleRoot; format = $format } { 30 | Test-Path "$moduleRoot\$format" | Should -Be $true 31 | } 32 | } 33 | 34 | foreach ($type in $manifest.TypesToProcess) 35 | { 36 | It "The file $type should exist" -TestCases @{ moduleRoot = $moduleRoot; type = $type } { 37 | Test-Path "$moduleRoot\$type" | Should -Be $true 38 | } 39 | } 40 | 41 | foreach ($assembly in $manifest.RequiredAssemblies) 42 | { 43 | if ($assembly -like "*.dll") { 44 | It "The file $assembly should exist" -TestCases @{ moduleRoot = $moduleRoot; assembly = $assembly } { 45 | Test-Path "$moduleRoot\$assembly" | Should -Be $true 46 | } 47 | } 48 | else { 49 | It "The file $assembly should load from the GAC" -TestCases @{ moduleRoot = $moduleRoot; assembly = $assembly } { 50 | { Add-Type -AssemblyName $assembly } | Should -Not -Throw 51 | } 52 | } 53 | } 54 | 55 | foreach ($tag in $manifest.PrivateData.PSData.Tags) 56 | { 57 | It "Tags should have no spaces in name" -TestCases @{ tag = $tag } { 58 | $tag -match " " | Should -Be $false 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /Refactor/tests/general/PSScriptAnalyzer.Tests.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | Param ( 3 | [switch] 4 | $SkipTest, 5 | 6 | [string[]] 7 | $CommandPath = @("$global:testroot\..\functions", "$global:testroot\..\internal\functions") 8 | ) 9 | 10 | if ($SkipTest) { return } 11 | 12 | $global:__pester_data.ScriptAnalyzer = New-Object System.Collections.ArrayList 13 | 14 | Describe 'Invoking PSScriptAnalyzer against commandbase' { 15 | $commandFiles = foreach ($path in $CommandPath) { 16 | Get-ChildItem -Path $path -Recurse | Where-Object Name -like "*.ps1" 17 | } 18 | $scriptAnalyzerRules = Get-ScriptAnalyzerRule 19 | 20 | foreach ($file in $commandFiles) 21 | { 22 | Context "Analyzing $($file.BaseName)" { 23 | $analysis = Invoke-ScriptAnalyzer -Path $file.FullName -ExcludeRule PSAvoidTrailingWhitespace, PSShouldProcess 24 | 25 | forEach ($rule in $scriptAnalyzerRules) 26 | { 27 | It "Should pass $rule" -TestCases @{ analysis = $analysis; rule = $rule } { 28 | If ($analysis.RuleName -contains $rule) 29 | { 30 | $analysis | Where-Object RuleName -EQ $rule -outvariable failures | ForEach-Object { $null = $global:__pester_data.ScriptAnalyzer.Add($_) } 31 | 32 | 1 | Should -Be 0 33 | } 34 | else 35 | { 36 | 0 | Should -Be 0 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /Refactor/tests/general/strings.Exceptions.ps1: -------------------------------------------------------------------------------- 1 | $exceptions = @{ } 2 | 3 | <# 4 | A list of entries that MAY be in the language files, without causing the tests to fail. 5 | This is commonly used in modules that generate localized messages straight from C#. 6 | Specify the full key as it is written in the language files, do not prepend the modulename, 7 | as you would have to in C# code. 8 | 9 | Example: 10 | $exceptions['LegalSurplus'] = @( 11 | 'Exception.Streams.FailedCreate' 12 | 'Exception.Streams.FailedDispose' 13 | ) 14 | #> 15 | $exceptions['LegalSurplus'] = @( 16 | 17 | ) 18 | <# 19 | A list of entries that MAY be used without needing to have text defined. 20 | This is intended for modules (re-)using strings provided by another module 21 | #> 22 | $exceptions['NoTextNeeded'] = @( 23 | 'Validate.FSPath' 24 | 'Validate.FSPath.File' 25 | 'Validate.FSPath.FileOrParent' 26 | 'Validate.FSPath.Folder' 27 | 'Validate.Path' 28 | 'Validate.Path.Container' 29 | 'Validate.Path.Leaf' 30 | 'Validate.TimeSpan.Positive' 31 | 'Validate.Uri.Absolute' 32 | 'Validate.Uri.Absolute.File' 33 | 'Validate.Uri.Absolute.Https' 34 | ) 35 | 36 | $exceptions -------------------------------------------------------------------------------- /Refactor/tests/general/strings.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .DESCRIPTION 3 | This test verifies, that all strings that have been used, 4 | are listed in the language files and thus have a message being displayed. 5 | 6 | It also checks, whether the language files have orphaned entries that need cleaning up. 7 | #> 8 | 9 | 10 | 11 | Describe "Testing localization strings" { 12 | $moduleRoot = (Get-Module Refactor).ModuleBase 13 | $stringsResults = Export-PSMDString -ModuleRoot $moduleRoot 14 | $exceptions = & "$global:testroot\general\strings.Exceptions.ps1" 15 | 16 | foreach ($stringEntry in $stringsResults) { 17 | if ($stringEntry.String -eq "key") { continue } # Skipping the template default entry 18 | It "Should be used & have text: $($stringEntry.String)" -TestCases @{ stringEntry = $stringEntry; exceptions = $exceptions } { 19 | if ($exceptions.LegalSurplus -notcontains $stringEntry.String) { 20 | $stringEntry.Surplus | Should -BeFalse 21 | } 22 | if ($exceptions.NoTextNeeded -notcontains $stringEntry.String) { 23 | $stringEntry.Text | Should -Not -BeNullOrEmpty 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Refactor/tests/pester.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | $TestGeneral = $true, 3 | 4 | $TestFunctions = $true, 5 | 6 | [ValidateSet('None', 'Normal', 'Detailed', 'Diagnostic')] 7 | [Alias('Show')] 8 | $Output = "None", 9 | 10 | $Include = "*", 11 | 12 | $Exclude = "" 13 | ) 14 | 15 | Write-PSFMessage -Level Important -Message "Starting Tests" 16 | 17 | Write-PSFMessage -Level Important -Message "Importing Module" 18 | 19 | $global:testroot = $PSScriptRoot 20 | $global:__pester_data = @{ } 21 | 22 | Remove-Module Refactor -ErrorAction Ignore 23 | Import-Module "$PSScriptRoot\..\Refactor.psd1" 24 | Import-Module "$PSScriptRoot\..\Refactor.psm1" -Force 25 | 26 | # Need to import explicitly so we can use the configuration class 27 | Import-Module Pester 28 | 29 | Write-PSFMessage -Level Important -Message "Creating test result folder" 30 | $null = New-Item -Path "$PSScriptRoot\..\.." -Name TestResults -ItemType Directory -Force 31 | 32 | $totalFailed = 0 33 | $totalRun = 0 34 | 35 | $testresults = @() 36 | $config = [PesterConfiguration]::Default 37 | $config.TestResult.Enabled = $true 38 | 39 | #region Run General Tests 40 | if ($TestGeneral) 41 | { 42 | Write-PSFMessage -Level Important -Message "Modules imported, proceeding with general tests" 43 | foreach ($file in (Get-ChildItem "$PSScriptRoot\general" | Where-Object Name -like "*.Tests.ps1")) 44 | { 45 | if ($file.Name -notlike $Include) { continue } 46 | if ($file.Name -like $Exclude) { continue } 47 | 48 | Write-PSFMessage -Level Significant -Message " Executing $($file.Name)" 49 | $config.TestResult.OutputPath = Join-Path "$PSScriptRoot\..\..\TestResults" "TEST-$($file.BaseName).xml" 50 | $config.Run.Path = $file.FullName 51 | $config.Run.PassThru = $true 52 | $config.Output.Verbosity = $Output 53 | $results = Invoke-Pester -Configuration $config 54 | foreach ($result in $results) 55 | { 56 | $totalRun += $result.TotalCount 57 | $totalFailed += $result.FailedCount 58 | $result.Tests | Where-Object Result -ne 'Passed' | ForEach-Object { 59 | $testresults += [pscustomobject]@{ 60 | Block = $_.Block 61 | Name = "It $($_.Name)" 62 | Result = $_.Result 63 | Message = $_.ErrorRecord.DisplayErrorMessage 64 | } 65 | } 66 | } 67 | } 68 | } 69 | #endregion Run General Tests 70 | 71 | $global:__pester_data.ScriptAnalyzer | Out-Host 72 | 73 | #region Test Commands 74 | if ($TestFunctions) 75 | { 76 | Write-PSFMessage -Level Important -Message "Proceeding with individual tests" 77 | foreach ($file in (Get-ChildItem "$PSScriptRoot\functions" -Recurse -File | Where-Object Name -like "*Tests.ps1")) 78 | { 79 | if ($file.Name -notlike $Include) { continue } 80 | if ($file.Name -like $Exclude) { continue } 81 | 82 | Write-PSFMessage -Level Significant -Message " Executing $($file.Name)" 83 | $config.TestResult.OutputPath = Join-Path "$PSScriptRoot\..\..\TestResults" "TEST-$($file.BaseName).xml" 84 | $config.Run.Path = $file.FullName 85 | $config.Run.PassThru = $true 86 | $config.Output.Verbosity = $Output 87 | $results = Invoke-Pester -Configuration $config 88 | foreach ($result in $results) 89 | { 90 | $totalRun += $result.TotalCount 91 | $totalFailed += $result.FailedCount 92 | $result.Tests | Where-Object Result -ne 'Passed' | ForEach-Object { 93 | $testresults += [pscustomobject]@{ 94 | Block = $_.Block 95 | Name = "It $($_.Name)" 96 | Result = $_.Result 97 | Message = $_.ErrorRecord.DisplayErrorMessage 98 | } 99 | } 100 | } 101 | } 102 | } 103 | #endregion Test Commands 104 | 105 | $testresults | Sort-Object Describe, Context, Name, Result, Message | Format-List 106 | 107 | if ($totalFailed -eq 0) { Write-PSFMessage -Level Critical -Message "All $totalRun tests executed without a single failure!" } 108 | else { Write-PSFMessage -Level Critical -Message "$totalFailed tests out of $totalRun tests failed!" } 109 | 110 | if ($totalFailed -gt 0) 111 | { 112 | throw "$totalFailed / $totalRun tests failed!" 113 | } -------------------------------------------------------------------------------- /Refactor/tests/readme.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | This is the folder, where all the tests go. 4 | 5 | Those are subdivided in two categories: 6 | 7 | - General 8 | - Function 9 | 10 | ## General Tests 11 | 12 | General tests are function generic and test for general policies. 13 | 14 | These test scan answer questions such as: 15 | 16 | - Is my module following my style guides? 17 | - Does any of my scripts have a syntax error? 18 | - Do my scripts use commands I do not want them to use? 19 | - Do my commands follow best practices? 20 | - Do my commands have proper help? 21 | 22 | Basically, these allow a general module health check. 23 | 24 | These tests are already provided as part of the template. 25 | 26 | ## Function Tests 27 | 28 | A healthy module should provide unit and integration tests for the commands & components it ships. 29 | Only then can be guaranteed, that they will actually perform as promised. 30 | 31 | However, as each such test must be specific to the function it tests, there cannot be much in the way of templates. -------------------------------------------------------------------------------- /Refactor/xml/Refactor.Types.ps1xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Deserialized.Foo.Bar 6 | 7 | 8 | PSStandardMembers 9 | 10 | 11 | 12 | TargetTypeForDeserialization 13 | 14 | 15 | Foo.Bar 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Foo.Bar 24 | 25 | 26 | SerializationData 27 | 28 | PSFramework.Serialization.SerializationTypeConverter 29 | GetSerializationData 30 | 31 | 32 | 33 | 34 | PSFramework.Serialization.SerializationTypeConverter 35 | 36 | 37 | -------------------------------------------------------------------------------- /Refactor/xml/readme.md: -------------------------------------------------------------------------------- 1 | # XML 2 | 3 | This is the folder where project XML files go, notably: 4 | 5 | - Format XML 6 | - Type Extension XML 7 | 8 | External help files should _not_ be placed in this folder! 9 | 10 | ## Notes on Files and Naming 11 | 12 | There should be only one format file and one type extension file per project, as importing them has a notable impact on import times. 13 | 14 | - The Format XML should be named `Refactor.Format.ps1xml` 15 | - The Type Extension XML should be named `Refactor.Types.ps1xml` 16 | 17 | ## Tools 18 | 19 | ### New-PSMDFormatTableDefinition 20 | 21 | This function will take an input object and generate format xml for an auto-sized table. 22 | 23 | It provides a simple way to get started with formats. 24 | 25 | ### Get-PSFTypeSerializationData 26 | 27 | ``` 28 | C# Warning! 29 | This section is only interest if you're using C# together with PowerShell. 30 | ``` 31 | 32 | This function generates type extension XML that allows PowerShell to convert types written in C# to be written to file and restored from it without being 'Deserialized'. Also works for jobs or remoting, if both sides have the `PSFramework` module and type extension loaded. 33 | 34 | In order for a class to be eligible for this, it needs to conform to the following rules: 35 | 36 | - Have the `[Serializable]` attribute 37 | - Be public 38 | - Have an empty constructor 39 | - Allow all public properties/fields to be set (even if setting it doesn't do anything) without throwing an exception. 40 | 41 | ``` 42 | non-public properties and fields will be lost in this process! 43 | ``` -------------------------------------------------------------------------------- /azFunctionResources/clientModule/function.ps1: -------------------------------------------------------------------------------- 1 | function %functionname% 2 | { 3 | %parameter% 4 | 5 | process 6 | { 7 | $invokeParameters = Get-InternalConnectionData -Method '%method%' -Parameter $PSBoundParameters -FunctionName '%condensedname%' 8 | Invoke-RestMethod @invokeParameters | ConvertFrom-PSFClixml 9 | } 10 | } -------------------------------------------------------------------------------- /azFunctionResources/clientModule/functions/Connect-Refactor.ps1: -------------------------------------------------------------------------------- 1 | function Connect-Refactor 2 | { 3 | <# 4 | .SYNOPSIS 5 | Configures the connection to the Refactor Azure Function. 6 | 7 | .DESCRIPTION 8 | Configures the connection to the Refactor Azure Function. 9 | 10 | .PARAMETER Uri 11 | Url to connect to the Refactor Azure function. 12 | 13 | .PARAMETER UnprotectedToken 14 | The unencrypted access token to the Refactor Azure function. ONLY use this from secure locations or non-sensitive functions! 15 | 16 | .PARAMETER ProtectedToken 17 | An encrypted access token to the Refactor Azure function. Use this to persist an access token in a way only the current user on the current system can access. 18 | 19 | .PARAMETER Register 20 | Using this command, the module will remember the connection settings persistently across PowerShell sessions. 21 | CAUTION: When using unencrypted token data (such as specified through the -UnprotectedToken parameter), the authenticating token will be stored in clear-text! 22 | 23 | .EXAMPLE 24 | PS C:\> Connect-Refactor -Uri 'https://demofunctionapp.azurewebsites.net/api/' 25 | 26 | Establishes a connection to Refactor 27 | #> 28 | [CmdletBinding()] 29 | param ( 30 | [string] 31 | $Uri, 32 | 33 | [string] 34 | $UnprotectedToken, 35 | 36 | [System.Management.Automation.PSCredential] 37 | $ProtectedToken, 38 | 39 | [switch] 40 | $Register 41 | ) 42 | 43 | process 44 | { 45 | if (Test-PSFParameterBinding -ParameterName UnprotectedToken) 46 | { 47 | Set-PSFConfig -Module 'Refactor' -Name 'Client.UnprotectedToken' -Value $UnprotectedToken 48 | if ($Register) { Register-PSFConfig -Module 'Refactor' -Name 'Client.UnprotectedToken' } 49 | } 50 | if (Test-PSFParameterBinding -ParameterName Uri) 51 | { 52 | Set-PSFConfig -Module 'Refactor' -Name 'Client.Uri' -Value $Uri 53 | if ($Register) { Register-PSFConfig -Module 'Refactor' -Name 'Client.Uri' } 54 | } 55 | if (Test-PSFParameterBinding -ParameterName ProtectedToken) 56 | { 57 | Set-PSFConfig -Module 'Refactor' -Name 'Client.ProtectedToken' -Value $ProtectedToken 58 | if ($Register) { Register-PSFConfig -Module 'Refactor' -Name 'Client.ProtectedToken' } 59 | } 60 | 61 | } 62 | } -------------------------------------------------------------------------------- /azFunctionResources/clientModule/internal/configurations/connection.ps1: -------------------------------------------------------------------------------- 1 | Set-PSFConfig -Module 'Refactor' -Name 'Client.Uri' -Value $null -Initialize -Validation 'string' -Description "Url to connect to the Refactor Azure function" 2 | Set-PSFConfig -Module 'Refactor' -Name 'Client.UnprotectedToken' -Value '' -Initialize -Validation 'string' -Description "The unencrypted access token to the Refactor Azure function. ONLY use this from secure locations or non-sensitive functions!" 3 | Set-PSFConfig -Module 'Refactor' -Name 'Client.ProtectedToken' -Value $null -Initialize -Validation 'credential' -Description "An encrypted access token to the Refactor Azure function. Use this to persist an access token in a way only the current user on the current system can access." -------------------------------------------------------------------------------- /azFunctionResources/clientModule/internal/functions/Get-InternalConnectionData.ps1: -------------------------------------------------------------------------------- 1 | function Get-InternalConnectionData 2 | { 3 | <# 4 | .SYNOPSIS 5 | Creates parameter hashtables for Invoke-RestMethod calls. 6 | 7 | .DESCRIPTION 8 | Creates parameter hashtables for Invoke-RestMethod calls. 9 | This is the main abstraction layer for public functions. 10 | 11 | .PARAMETER Method 12 | The Rest Method to use when calling this function. 13 | 14 | .PARAMETER Parameters 15 | The PSBoundParameters object. Will be passed online using PowerShell Serialization. 16 | 17 | .PARAMETER FunctionName 18 | The name of the Azure Function to call. 19 | This should always be the condensed name of the function. 20 | #> 21 | [CmdletBinding()] 22 | param ( 23 | [string] 24 | $Method, 25 | 26 | $Parameters, 27 | 28 | [string] 29 | $FunctionName 30 | ) 31 | 32 | process 33 | { 34 | try { $uri = '{0}{1}' -f (Get-PSFConfigValue -FullName 'Refactor.Client.Uri' -NotNull), $FunctionName } 35 | catch { $PSCmdlet.ThrowTerminatingError($_) } 36 | $header = @{ } 37 | 38 | #region Authentication 39 | $unprotectedToken = Get-PSFConfigValue -FullName 'Refactor.Client.UnprotectedToken' 40 | $protectedToken = Get-PSFConfigValue -FullName 'Refactor.Client.ProtectedToken' 41 | 42 | $authenticationDone = $false 43 | if ($protectedToken -and -not $authenticationDone) 44 | { 45 | $uri += '?code={0}' -f $protectedToken.GetNetworkCredential().Password 46 | $authenticationDone = $true 47 | } 48 | if ($unprotectedToken -and -not $authenticationDone) 49 | { 50 | $uri += '?code={0}' -f $unprotectedToken 51 | $authenticationDone = $true 52 | } 53 | if (-not $authenticationDone) 54 | { 55 | throw "No Authentication configured!" 56 | } 57 | #endregion Authentication 58 | 59 | 60 | @{ 61 | Method = $Method 62 | Uri = $uri 63 | Headers = $header 64 | Body = (@{ 65 | __SerializedParameters = ($Parameters | ConvertTo-PSFHashtable | ConvertTo-PSFClixml) 66 | __PSSerialize = $true 67 | } | ConvertTo-Json) 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /azFunctionResources/clientModule/moduleroot.psm1: -------------------------------------------------------------------------------- 1 | $script:ModuleRoot = $PSScriptRoot 2 | 3 | foreach ($file in (Get-ChildItem -Path "$script:ModuleRoot\internal\configurations" -Recurse -Filter '*.ps1')) 4 | { 5 | . $file.FullName 6 | } 7 | foreach ($file in (Get-ChildItem -Path "$script:ModuleRoot\internal\functions" -Recurse -Filter '*.ps1')) 8 | { 9 | . $file.FullName 10 | } 11 | foreach ($file in (Get-ChildItem -Path "$script:ModuleRoot\functions" -Recurse -Filter '*.ps1')) 12 | { 13 | . $file.FullName 14 | } -------------------------------------------------------------------------------- /azFunctionResources/functionOverride/Get-Example.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "function", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "Request", 8 | "methods": [ 9 | "delete" 10 | ] 11 | }, 12 | { 13 | "type": "http", 14 | "direction": "out", 15 | "name": "Response" 16 | } 17 | ], 18 | "disabled": true 19 | } -------------------------------------------------------------------------------- /azFunctionResources/functionOverride/Get-Example.ps1: -------------------------------------------------------------------------------- 1 | function Get-Example 2 | { 3 | <# 4 | .NOTES 5 | This file will be used to override the auto-generated CLIENT MODULE function implementation. 6 | 7 | Using the vsts-createFunctionClientModule.ps1 task script, you can autogenerate a client module. 8 | That module can be used to connect to the published Azure Function Module. 9 | However sometimes you may want to override the default client function for a given command, 10 | in order to better customize the way it behaves. 11 | 12 | Creating a ps1 file with the name of the specific function in this folder will use this file, 13 | rather than creating a default copy. 14 | 15 | NOTE: 16 | There will be no further automatic change detection! 17 | If you later update the Azure Function, you need to manually update the client function as well. 18 | #> 19 | [CmdletBinding()] 20 | Param ( 21 | 22 | ) 23 | 24 | begin 25 | { 26 | 27 | } 28 | process 29 | { 30 | 31 | } 32 | end 33 | { 34 | 35 | } 36 | } -------------------------------------------------------------------------------- /azFunctionResources/functionOverride/Get-Example.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | # Override the rest methods used for the API endpoint 3 | # RestMethods = 'delete' 4 | 5 | # Override inclusion into client module 6 | # NoClientFunction 7 | } -------------------------------------------------------------------------------- /azFunctionResources/host-az.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "managedDependency": { 4 | "Enabled": true 5 | } 6 | } -------------------------------------------------------------------------------- /azFunctionResources/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0" 3 | } -------------------------------------------------------------------------------- /azFunctionResources/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "FUNCTIONS_WORKER_RUNTIME": "powershell", 5 | "AzureWebJobsStorage": "--connection string for storage account---" 6 | } 7 | } -------------------------------------------------------------------------------- /azFunctionResources/profile.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | This is the globl profile file for the Azure Function App. 3 | This file will have been executed first, before any function runs. 4 | Use this to create a common execution environment, 5 | but keep in mind that the profile execution time is added to the function startup time for ALL functions. 6 | #> 7 | 8 | if ($env:MSI_SECRET -and (Get-Module -ListAvailable Az.Accounts)) 9 | { 10 | Connect-AzAccount -Identity 11 | } -------------------------------------------------------------------------------- /azFunctionResources/profileFunctions/Convert-AzureFunctionParameter.ps1: -------------------------------------------------------------------------------- 1 | function Convert-AzureFunctionParameter 2 | { 3 | <# 4 | .SYNOPSIS 5 | Extracts the parameters passed into the rest method. 6 | 7 | .DESCRIPTION 8 | Extracts the parameters passed into the rest method of an Azure Function. 9 | Returns a hashtable, similar to what would be found on a $PSBoundParameters variable. 10 | 11 | .PARAMETER Request 12 | The request to process 13 | 14 | .EXAMPLE 15 | PS C:\> Convert-AzureFunctionParameter -Request $request 16 | 17 | Converts the $request object into a regular hashtable. 18 | #> 19 | [OutputType([System.Collections.Hashtable])] 20 | [CmdletBinding()] 21 | param ( 22 | $Request 23 | ) 24 | 25 | $parameterObject = [pscustomobject]@{ 26 | Parameters = @{ } 27 | Serialize = $false 28 | } 29 | 30 | foreach ($key in $Request.Query.Keys) 31 | { 32 | # Do NOT include the authentication key 33 | if ($key -eq 'code') { continue } 34 | $parameterObject.Parameters[$key] = $Request.Query.$key 35 | } 36 | foreach ($key in $Request.Body.Keys) 37 | { 38 | $parameterObject.Parameters[$key] = $Request.Body.$key 39 | } 40 | if ($parameterObject.Parameters.__PSSerialize) 41 | { 42 | $parameterObject.Serialize = $true 43 | $null = $parameterObject.Parameters.Remove('__PSSerialize') 44 | } 45 | if ($parameterObject.Parameters.__SerializedParameters) 46 | { 47 | $parameterObject.Parameters = $parameterObject.Parameters.__SerializedParameters | ConvertFrom-PSFClixml 48 | } 49 | 50 | $parameterObject 51 | } -------------------------------------------------------------------------------- /azFunctionResources/profileFunctions/Write-AzureFunctionOutput.ps1: -------------------------------------------------------------------------------- 1 | function Write-AzureFunctionOutput 2 | { 3 | <# 4 | .SYNOPSIS 5 | Write output equally well from Azure Functions or locally. 6 | 7 | .DESCRIPTION 8 | Write output equally well from Azure Functions or locally. 9 | When calling this command, call return straight after it. 10 | Use Write-AzureFunctionStatus first if an error should be returned, then specify an error text here. 11 | 12 | .PARAMETER Value 13 | The value data to return. 14 | Either an error message 15 | 16 | .PARAMETER Serialize 17 | Return the output object as compressed clixml string. 18 | You can use ConvertFrom-PSFClixml to restore the object on the recipient-side. 19 | 20 | .EXAMPLE 21 | PS C:\> Write-AzureFunctionOutput -Value $result 22 | 23 | Writes the content of $result as output. 24 | 25 | .EXAMPLE 26 | PS C:\> Write-AzureFunctionOutput -Value $result -Serialize 27 | 28 | Writes the content of $result as output. 29 | If called from Azure Functions, it will convert the output as compressed clixml string. 30 | 31 | #> 32 | [CmdletBinding()] 33 | param ( 34 | [Parameter(Mandatory = $true)] 35 | $Value, 36 | 37 | [switch] 38 | $Serialize, 39 | 40 | [System.Net.HttpStatusCode] 41 | $Status = [System.Net.HttpStatusCode]::OK 42 | ) 43 | 44 | if ($Serialize) 45 | { 46 | $Value = $Value | ConvertTo-PSFClixml 47 | } 48 | 49 | Push-OutputBinding -Name Response -Value ( 50 | [HttpResponseContext]@{ 51 | StatusCode = $Status 52 | Body = $Value 53 | } 54 | ) 55 | } -------------------------------------------------------------------------------- /azFunctionResources/readme.md: -------------------------------------------------------------------------------- 1 | # Azure Function Resources 2 | 3 | This folder is used to store Azure Function specific meta data and resources. 4 | 5 | This folder is also used to allow the user to easily create a custom function-specific configuration, for exanmple in order to change the trigger settings. 6 | 7 | To specify custom, 'Per Function' configuration json, just place the desired configuration file as 'functionname.json' into this folder (it does not matter if it is the PowerShell function name or the condensed version used for publishing on Azure). -------------------------------------------------------------------------------- /azFunctionResources/requirements.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | Az = '1.*' 3 | } -------------------------------------------------------------------------------- /azFunctionResources/run.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | $Request, 3 | 4 | $TriggerMetadata 5 | ) 6 | 7 | $parameterObject = Convert-AzureFunctionParameter -Request $Request 8 | $parameters = $parameterObject.Parameters 9 | try { $data = %functionname% @parameters } 10 | catch 11 | { 12 | Write-AzureFunctionOutput -Value "Failed to execute: $_" -Status InternalServerError 13 | return 14 | } 15 | 16 | Write-AzureFunctionOutput -Value $data -Serialize:$parameterObject.Serialize -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | pool: 2 | name: Hosted VS2017 3 | steps: 4 | - task: PowerShell@2 5 | displayName: Prerequisites 6 | inputs: 7 | targetType: filePath 8 | filePath: './build/vsts-prerequisites.ps1' 9 | 10 | - task: PowerShell@2 11 | displayName: Validate 12 | inputs: 13 | targetType: filePath 14 | filePath: './build/vsts-validate.ps1' 15 | 16 | - task: PublishTestResults@2 17 | displayName: 'Publish Test Results **/TEST-*.xml' 18 | inputs: 19 | testResultsFormat: NUnit 20 | condition: always() 21 | -------------------------------------------------------------------------------- /build/AzureFunction.readme.md: -------------------------------------------------------------------------------- 1 | # Setting up the release pipeline: 2 | 3 | ## Preliminary 4 | 5 | Setting up a release pipeline, set the trigger to do continuous integration against the master branch only. 6 | In Stage 1 set up a tasksequence: 7 | 8 | ## 1) PowerShell Task: Prerequisites 9 | 10 | Have it execute `vsts-prerequisites.ps1` 11 | 12 | ## 2) PowerShell Task: Validate 13 | 14 | Have it execute `vsts-prerequisites.ps1` 15 | 16 | ## 3) PowerShell Task: Build 17 | 18 | Have it execute `vsts-build.ps1`. 19 | The task requires two parameters: 20 | 21 | - `-LocalRepo` 22 | - `-WorkingDirectory $(System.DefaultWorkingDirectory)/_�name�` 23 | 24 | ## 4) Publish Test Results 25 | 26 | Configure task to pick up nunit type of tests (rather than the default junit). 27 | Configure task to execute, even if previous steps failed or the task sequence was cancelled. 28 | 29 | ## 5) PowerShell Task: Package Function 30 | 31 | Have it execute `vsts-packageFunction.ps1` 32 | 33 | ## 6) Azure Function AppDeploy 34 | 35 | Configure to publish to the correct function app. -------------------------------------------------------------------------------- /build/vsts-build.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | This script publishes the module to the gallery. 3 | It expects as input an ApiKey authorized to publish the module. 4 | 5 | Insert any build steps you may need to take before publishing it here. 6 | #> 7 | param ( 8 | $ApiKey, 9 | 10 | $WorkingDirectory, 11 | 12 | $Repository = 'PSGallery', 13 | 14 | [switch] 15 | $LocalRepo, 16 | 17 | [switch] 18 | $SkipPublish, 19 | 20 | [switch] 21 | $AutoVersion 22 | ) 23 | 24 | #region Handle Working Directory Defaults 25 | if (-not $WorkingDirectory) 26 | { 27 | if ($env:RELEASE_PRIMARYARTIFACTSOURCEALIAS) 28 | { 29 | $WorkingDirectory = Join-Path -Path $env:SYSTEM_DEFAULTWORKINGDIRECTORY -ChildPath $env:RELEASE_PRIMARYARTIFACTSOURCEALIAS 30 | } 31 | else { $WorkingDirectory = $env:SYSTEM_DEFAULTWORKINGDIRECTORY } 32 | } 33 | if (-not $WorkingDirectory) { $WorkingDirectory = Split-Path $PSScriptRoot } 34 | #endregion Handle Working Directory Defaults 35 | 36 | # Prepare publish folder 37 | Write-PSFMessage -Level Important -Message "Creating and populating publishing directory" 38 | $publishDir = New-Item -Path $WorkingDirectory -Name publish -ItemType Directory -Force 39 | Copy-Item -Path "$($WorkingDirectory)\Refactor" -Destination $publishDir.FullName -Recurse -Force 40 | 41 | #region Gather text data to compile 42 | $text = @() 43 | $processed = @() 44 | 45 | # Gather Stuff to run before 46 | foreach ($filePath in (& "$($PSScriptRoot)\..\Refactor\internal\scripts\preimport.ps1")) 47 | { 48 | if ([string]::IsNullOrWhiteSpace($filePath)) { continue } 49 | 50 | $item = Get-Item $filePath 51 | if ($item.PSIsContainer) { continue } 52 | if ($item.FullName -in $processed) { continue } 53 | $text += [System.IO.File]::ReadAllText($item.FullName) 54 | $processed += $item.FullName 55 | } 56 | 57 | # Gather commands 58 | Get-ChildItem -Path "$($publishDir.FullName)\Refactor\internal\functions\" -Recurse -File -Filter "*.ps1" | ForEach-Object { 59 | $text += [System.IO.File]::ReadAllText($_.FullName) 60 | } 61 | Get-ChildItem -Path "$($publishDir.FullName)\Refactor\functions\" -Recurse -File -Filter "*.ps1" | ForEach-Object { 62 | $text += [System.IO.File]::ReadAllText($_.FullName) 63 | } 64 | 65 | # Gather stuff to run afterwards 66 | foreach ($filePath in (& "$($PSScriptRoot)\..\Refactor\internal\scripts\postimport.ps1")) 67 | { 68 | if ([string]::IsNullOrWhiteSpace($filePath)) { continue } 69 | 70 | $item = Get-Item $filePath 71 | if ($item.PSIsContainer) { continue } 72 | if ($item.FullName -in $processed) { continue } 73 | $text += [System.IO.File]::ReadAllText($item.FullName) 74 | $processed += $item.FullName 75 | } 76 | #endregion Gather text data to compile 77 | 78 | #region Update the psm1 file 79 | $fileData = Get-Content -Path "$($publishDir.FullName)\Refactor\Refactor.psm1" -Raw 80 | $fileData = $fileData.Replace('""', '""') 81 | $fileData = $fileData.Replace('""', ($text -join "`n`n")) 82 | [System.IO.File]::WriteAllText("$($publishDir.FullName)\Refactor\Refactor.psm1", $fileData, [System.Text.Encoding]::UTF8) 83 | #endregion Update the psm1 file 84 | 85 | #region Updating the Module Version 86 | if ($AutoVersion) 87 | { 88 | Write-PSFMessage -Level Important -Message "Updating module version numbers." 89 | try { [version]$remoteVersion = (Find-Module 'Refactor' -Repository $Repository -ErrorAction Stop).Version } 90 | catch 91 | { 92 | Stop-PSFFunction -Message "Failed to access $($Repository)" -EnableException $true -ErrorRecord $_ 93 | } 94 | if (-not $remoteVersion) 95 | { 96 | Stop-PSFFunction -Message "Couldn't find Refactor on repository $($Repository)" -EnableException $true 97 | } 98 | $newBuildNumber = $remoteVersion.Build + 1 99 | [version]$localVersion = (Import-PowerShellDataFile -Path "$($publishDir.FullName)\Refactor\Refactor.psd1").ModuleVersion 100 | Update-ModuleManifest -Path "$($publishDir.FullName)\Refactor\Refactor.psd1" -ModuleVersion "$($localVersion.Major).$($localVersion.Minor).$($newBuildNumber)" 101 | } 102 | #endregion Updating the Module Version 103 | 104 | #region Publish 105 | if ($SkipPublish) { return } 106 | if ($LocalRepo) 107 | { 108 | # Dependencies must go first 109 | Write-PSFMessage -Level Important -Message "Creating Nuget Package for module: PSFramework" 110 | New-PSMDModuleNugetPackage -ModulePath (Get-Module -Name PSFramework).ModuleBase -PackagePath . 111 | Write-PSFMessage -Level Important -Message "Creating Nuget Package for module: Refactor" 112 | New-PSMDModuleNugetPackage -ModulePath "$($publishDir.FullName)\Refactor" -PackagePath . 113 | } 114 | else 115 | { 116 | # Publish to Gallery 117 | Write-PSFMessage -Level Important -Message "Publishing the Refactor module to $($Repository)" 118 | Publish-Module -Path "$($publishDir.FullName)\Refactor" -NuGetApiKey $ApiKey -Force -Repository $Repository 119 | } 120 | #endregion Publish -------------------------------------------------------------------------------- /build/vsts-createFunctionClientModule.ps1: -------------------------------------------------------------------------------- 1 |  2 | <# 3 | .SYNOPSIS 4 | Build script that generates a client module for REST API endpoints of a Azure PowerShell Functions project. 5 | 6 | .DESCRIPTION 7 | Build script that generates a client module for REST API endpoints of a Azure PowerShell Functions project. 8 | 9 | .PARAMETER ApiKey 10 | The API key to use to publish the module to a Nuget Repository 11 | 12 | .PARAMETER WorkingDirectory 13 | The root folder from which to build the module. 14 | 15 | .PARAMETER Repository 16 | The name of the repository to publish to. 17 | Defaults to PSGallery. 18 | 19 | .PARAMETER LocalRepo 20 | Instead of publishing to a gallery, drop a nuget package in the root folder. 21 | This package can then be picked up in a later step for publishing to Azure Artifacts. 22 | 23 | .PARAMETER ModuleName 24 | The name to give to the client module. 25 | By default, the client module will be named '.Client'. 26 | 27 | .PARAMETER IncludeFormat 28 | Include the format xml of the source module for the client module. 29 | 30 | .PARAMETER IncludeType 31 | Include the type extension xml of the source module for the client module. 32 | 33 | .PARAMETER IncludeAssembly 34 | Include the binaries of the source module for the client module. 35 | #> 36 | param ( 37 | $ApiKey, 38 | 39 | $WorkingDirectory, 40 | 41 | $Repository = 'PSGallery', 42 | 43 | [switch] 44 | $LocalRepo, 45 | 46 | $ModuleName, 47 | 48 | [switch] 49 | $IncludeFormat, 50 | 51 | [switch] 52 | $IncludeType, 53 | 54 | [switch] 55 | $IncludeAssembly 56 | ) 57 | 58 | #region Handle Working Directory Defaults 59 | if (-not $WorkingDirectory) 60 | { 61 | if ($env:RELEASE_PRIMARYARTIFACTSOURCEALIAS) 62 | { 63 | $WorkingDirectory = Join-Path -Path $env:SYSTEM_DEFAULTWORKINGDIRECTORY -ChildPath $env:RELEASE_PRIMARYARTIFACTSOURCEALIAS 64 | } 65 | else { $WorkingDirectory = $env:SYSTEM_DEFAULTWORKINGDIRECTORY } 66 | } 67 | #endregion Handle Working Directory Defaults 68 | 69 | Write-PSFMessage -Level Host -Message 'Starting Build: Client Module' 70 | $parentModule = 'Refactor' 71 | if (-not $ModuleName) { $ModuleName = 'Refactor.Client' } 72 | Write-PSFMessage -Level Host -Message 'Creating Folder Structure' 73 | $workingRoot = New-Item -Path $WorkingDirectory -Name $ModuleName -ItemType Directory 74 | $publishRoot = Join-Path -Path $WorkingDirectory -ChildPath 'publish\Refactor' 75 | Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\clientModule\functions" -Destination "$($workingRoot.FullName)\" -Recurse 76 | Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\clientModule\internal" -Destination "$($workingRoot.FullName)\" -Recurse 77 | Copy-Item -Path "$($publishRoot)\en-us" -Destination "$($workingRoot.FullName)\" -Recurse 78 | $functionFolder = Get-Item -Path "$($workingRoot.FullName)\functions" 79 | 80 | #region Create Functions 81 | $encoding = [PSFEncoding]'utf8' 82 | $functionsText = Get-Content -Path "$($WorkingDirectory)\azFunctionResources\clientModule\function.ps1" -Raw 83 | 84 | Write-PSFMessage -Level Host -Message 'Creating Functions' 85 | foreach ($functionSourceFile in (Get-ChildItem -Path "$($publishRoot)\functions" -Recurse -Filter '*.ps1')) 86 | { 87 | Write-PSFMessage -Level Host -Message " Processing function: $($functionSourceFile.BaseName)" 88 | $condensedName = $functionSourceFile.BaseName -replace '-', '' 89 | 90 | #region Load Overrides 91 | $override = @{ } 92 | if (Test-Path -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).psd1") 93 | { 94 | $override = Import-PowerShellDataFile -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).psd1" 95 | } 96 | if (Test-Path -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($condensedName).psd1") 97 | { 98 | $override = Import-PowerShellDataFile -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($condensedName).psd1" 99 | } 100 | if ($override.NoClientFunction) 101 | { 102 | Write-PSFMessage -Level Host -Message " Override 'NoClientFunction' detected, skipping!" 103 | continue 104 | } 105 | 106 | # If there is an definition override, use it and continue 107 | if (Test-Path -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).ps1") 108 | { 109 | Write-PSFMessage -Level Host -Message " Override function definition detected, using override" 110 | Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).ps1" -Destination $functionFolder.FullName 111 | continue 112 | } 113 | 114 | # Figure out the Rest Method to use 115 | $methodName = 'Post' 116 | if ($override.RestMethods) 117 | { 118 | $methodName = $override.RestMethods | Where-Object { $_ -ne 'Get' } | Select-Object -First 1 119 | } 120 | 121 | #endregion Load Overrides 122 | 123 | $currentFunctionsText = $functionsText -replace '%functionname%', $functionSourceFile.BaseName -replace '%condensedname%', $condensedName -replace '%method%', $methodName 124 | 125 | $parsedFunction = Read-PSMDScript -Path $functionSourceFile.FullName 126 | $functionAst = $parsedFunction.Ast.EndBlock.Statements | Where-Object { 127 | $_ -is [System.Management.Automation.Language.FunctionDefinitionAst] 128 | } | Select-Object -First 1 129 | 130 | $end = $functionAst.Body.ParamBlock.Extent.EndOffSet 131 | $start = $functionAst.Body.Extent.StartOffSet + 1 132 | $currentFunctionsText = $currentFunctionsText.Replace('%parameter%', $functionAst.Body.Extent.Text.SubString(1, ($end - $start))) 133 | 134 | Write-PSFMessage -Level Host -Message " Creating file: $($functionFolder.FullName)\$($functionSourceFile.Name)" 135 | [System.IO.File]::WriteAllText("$($functionFolder.FullName)\$($functionSourceFile.Name)", $currentFunctionsText, $encoding) 136 | } 137 | $functionsToExport = (Get-ChildItem -Path $functionFolder.FullName -Recurse -Filter *.ps1).BaseName | Sort-Object 138 | #endregion Create Functions 139 | 140 | #region Create Core Module Files 141 | # Get Manifest of published version, in order to catch build-phase changes such as module version. 142 | $originalManifestData = Import-PowerShellDataFile -Path "$publishRoot\Refactor.psd1" 143 | $prereqHash = @{ 144 | ModuleName = 'PSFramework' 145 | ModuleVersion = (Get-Module PSFramework).Version 146 | } 147 | $paramNewModuleManifest = @{ 148 | Path = ('{0}\{1}.psd1' -f $workingRoot.FullName, $ModuleName) 149 | FunctionsToExport = $functionsToExport 150 | CompanyName = $originalManifestData.CompanyName 151 | Author = $originalManifestData.Author 152 | Description = $originalManifestData.Description 153 | ModuleVersion = $originalManifestData.ModuleVersion 154 | RootModule = ('{0}.psm1' -f $ModuleName) 155 | Copyright = $originalManifestData.Copyright 156 | TypesToProcess = @() 157 | FormatsToProcess = @() 158 | RequiredAssemblies = @() 159 | RequiredModules = @($prereqHash) 160 | CompatiblePSEditions = 'Core', 'Desktop' 161 | PowerShellVersion = '5.1' 162 | } 163 | 164 | if ($IncludeAssembly) { $paramNewModuleManifest.RequiredAssemblies = $originalManifestData.RequiredAssemblies } 165 | if ($IncludeFormat) { $paramNewModuleManifest.FormatsToProcess = $originalManifestData.FormatsToProcess } 166 | if ($IncludeType) { $paramNewModuleManifest.TypesToProcess = $originalManifestData.TypesToProcess } 167 | Write-PSFMessage -Level Host -Message "Creating Module Manifest for module: $ModuleName" 168 | New-ModuleManifest @paramNewModuleManifest 169 | 170 | Write-PSFMessage -Level Host -Message "Copying additional module files" 171 | Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\clientModule\moduleroot.psm1" -Destination "$($workingRoot.FullName)\$($ModuleName).psm1" 172 | Copy-Item -Path "$($WorkingDirectory)\LICENSE" -Destination "$($workingRoot.FullName)\" 173 | #endregion Create Core Module Files 174 | 175 | #region Transfer Additional Content 176 | if ($IncludeAssembly) 177 | { 178 | Copy-Item -Path "$publishRoot\bin" -Destination "$($workingRoot.FullName)\" -Recurse 179 | } 180 | if ($IncludeFormat -or $IncludeType) 181 | { 182 | Copy-Item -Path "$publishRoot\xml" -Destination "$($workingRoot.FullName)\" -Recurse 183 | } 184 | #endregion Transfer Additional Content 185 | 186 | #region Publish 187 | if ($LocalRepo) 188 | { 189 | # Dependencies must go first 190 | Write-PSFMessage -Level Important -Message "Creating Nuget Package for module: PSFramework" 191 | New-PSMDModuleNugetPackage -ModulePath (Get-Module -Name PSFramework).ModuleBase -PackagePath . -WarningAction SilentlyContinue 192 | Write-PSFMessage -Level Important -Message "Creating Nuget Package for module: Refactor" 193 | New-PSMDModuleNugetPackage -ModulePath $workingRoot.FullName -PackagePath . -EnableException 194 | } 195 | else 196 | { 197 | # Publish to Gallery 198 | Write-PSFMessage -Level Important -Message "Publishing the Refactor module to $($Repository)" 199 | Publish-Module -Path $workingRoot.FullName -NuGetApiKey $ApiKey -Force -Repository $Repository 200 | } 201 | #endregion Publish -------------------------------------------------------------------------------- /build/vsts-packageFunction.ps1: -------------------------------------------------------------------------------- 1 |  2 | <# 3 | .SYNOPSIS 4 | Packages an Azure Functions project, ready to release. 5 | 6 | .DESCRIPTION 7 | Packages an Azure Functions project, ready to release. 8 | Should be part of the release pipeline, after ensuring validation. 9 | 10 | Look into the 'AzureFunctionRest' template for generating functions for the module if you do. 11 | 12 | .PARAMETER WorkingDirectory 13 | The root folder to work from. 14 | 15 | .PARAMETER Repository 16 | The name of the repository to use for gathering dependencies from. 17 | #> 18 | param ( 19 | $WorkingDirectory = "$($env:SYSTEM_DEFAULTWORKINGDIRECTORY)\_Refactor", 20 | 21 | $Repository = 'PSGallery', 22 | 23 | [switch] 24 | $IncludeAZ 25 | ) 26 | 27 | $moduleName = 'Refactor' 28 | 29 | # Prepare Paths 30 | Write-PSFMessage -Level Host -Message "Creating working folders" 31 | $moduleRoot = Join-Path -Path $WorkingDirectory -ChildPath 'publish' 32 | $workingRoot = New-Item -Path $WorkingDirectory -Name 'working' -ItemType Directory 33 | $modulesFolder = New-Item -Path $workingRoot.FullName -Name Modules -ItemType Directory 34 | 35 | # Fill out the modules folder 36 | Write-PSFMessage -Level Host -Message "Transfering built module data into working directory" 37 | Copy-Item -Path "$moduleRoot\$moduleName" -Destination $modulesFolder.FullName -Recurse -Force 38 | foreach ($dependency in (Import-PowerShellDataFile -Path "$moduleRoot\$moduleName\$moduleName.psd1").RequiredModules) 39 | { 40 | $param = @{ 41 | Repository = $Repository 42 | Name = $dependency.ModuleName 43 | Path = $modulesFolder.FullName 44 | } 45 | if ($dependency -is [string]) { $param['Name'] = $dependency } 46 | if ($dependency.RequiredVersion) 47 | { 48 | $param['RequiredVersion'] = $dependency.RequiredVersion 49 | } 50 | Write-PSFMessage -Level Host -Message "Preparing Dependency: $($param['Name'])" 51 | Save-Module @param 52 | } 53 | 54 | # Generate function configuration 55 | Write-PSFMessage -Level Host -Message 'Generating function configuration' 56 | $runTemplate = Get-Content -Path "$($WorkingDirectory)\azFunctionResources\run.ps1" -Raw 57 | foreach ($functionSourceFile in (Get-ChildItem -Path "$($moduleRoot)\$moduleName\functions" -Recurse -Filter '*.ps1')) 58 | { 59 | Write-PSFMessage -Level Host -Message " Processing function: $functionSourceFile" 60 | $condensedName = $functionSourceFile.BaseName -replace '-', '' 61 | $functionFolder = New-Item -Path $workingRoot.FullName -Name $condensedName -ItemType Directory 62 | 63 | #region Load Overrides 64 | $override = @{ } 65 | if (Test-Path -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).psd1") 66 | { 67 | $override = Import-PowerShellDataFile -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).psd1" 68 | } 69 | if (Test-Path -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($condensedName).psd1") 70 | { 71 | $override = Import-PowerShellDataFile -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($condensedName).psd1" 72 | } 73 | #endregion Load Overrides 74 | 75 | #region Create Function Configuration 76 | $restMethods = 'get', 'post' 77 | if ($override.RestMethods) { $restMethods = $override.RestMethods } 78 | 79 | Set-Content -Path "$($functionFolder.FullName)\function.json" -Value @" 80 | { 81 | "bindings": [ 82 | { 83 | "authLevel": "function", 84 | "type": "httpTrigger", 85 | "direction": "in", 86 | "name": "Request", 87 | "methods": [ 88 | "$($restMethods -join "`", 89 | `"")" 90 | ] 91 | }, 92 | { 93 | "type": "http", 94 | "direction": "out", 95 | "name": "Response" 96 | } 97 | ], 98 | "disabled": false 99 | } 100 | "@ 101 | #endregion Create Function Configuration 102 | 103 | #region Override Function Configuration 104 | if (Test-Path -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).json") 105 | { 106 | Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).json" -Destination "$($functionFolder.FullName)\function.json" -Force 107 | } 108 | if (Test-Path -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($condensedName).json") 109 | { 110 | Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($condensedName).json" -Destination "$($functionFolder.FullName)\function.json" -Force 111 | } 112 | #endregion Override Function Configuration 113 | 114 | # Generate the run.ps1 file 115 | $runText = $runTemplate -replace '%functionname%', $functionSourceFile.BaseName 116 | $runText | Set-Content -Path "$($functionFolder.FullName)\run.ps1" -Encoding UTF8 117 | } 118 | 119 | # Transfer common files 120 | Write-PSFMessage -Level Host -Message "Transfering core function data" 121 | if ($IncludeAZ) 122 | { 123 | Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\host-az.json" -Destination "$($workingroot.FullName)\host.json" 124 | Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\requirements.psd1" -Destination "$($workingroot.FullName)\" 125 | } 126 | else 127 | { 128 | Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\host.json" -Destination "$($workingroot.FullName)\" 129 | } 130 | Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\local.settings.json" -Destination "$($workingroot.FullName)\" 131 | 132 | # Build the profile file 133 | $text = @() 134 | $text += Get-Content -Path "$($WorkingDirectory)\azFunctionResources\profile.ps1" -Raw 135 | foreach ($functionFile in (Get-ChildItem "$($WorkingDirectory)\azFunctionResources\profileFunctions" -Recurse)) 136 | { 137 | $text += Get-Content -Path $functionFile.FullName -Raw 138 | } 139 | $text -join "`n`n" | Set-Content "$($workingroot.FullName)\profile.ps1" 140 | 141 | # Zip It 142 | Write-PSFMessage -Level Host -Message "Creating function archive in '$($WorkingDirectory)\$moduleName.zip'" 143 | Compress-Archive -Path "$($workingroot.FullName)\*" -DestinationPath "$($WorkingDirectory)\$moduleName.zip" -Force -------------------------------------------------------------------------------- /build/vsts-prerequisites.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [string] 3 | $Repository = 'PSGallery' 4 | ) 5 | 6 | $modules = @("Pester", "PSFramework", "PSModuleDevelopment", "PSScriptAnalyzer") 7 | 8 | # Automatically add missing dependencies 9 | $data = Import-PowerShellDataFile -Path "$PSScriptRoot\..\Refactor\Refactor.psd1" 10 | foreach ($dependency in $data.RequiredModules) { 11 | if ($dependency -is [string]) { 12 | if ($modules -contains $dependency) { continue } 13 | $modules += $dependency 14 | } 15 | else { 16 | if ($modules -contains $dependency.ModuleName) { continue } 17 | $modules += $dependency.ModuleName 18 | } 19 | } 20 | 21 | foreach ($module in $modules) { 22 | Write-Host "Installing $module" -ForegroundColor Cyan 23 | Install-Module $module -Force -SkipPublisherCheck -Repository $Repository 24 | Import-Module $module -Force -PassThru 25 | } -------------------------------------------------------------------------------- /build/vsts-validate.ps1: -------------------------------------------------------------------------------- 1 | # Guide for available variables and working with secrets: 2 | # https://docs.microsoft.com/en-us/vsts/build-release/concepts/definitions/build/variables?tabs=powershell 3 | 4 | # Needs to ensure things are Done Right and only legal commits to master get built 5 | 6 | # Run internal pester tests 7 | & "$PSScriptRoot\..\Refactor\tests\pester.ps1" -------------------------------------------------------------------------------- /docs/breakingChange/BeerFactory.break.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | BeerFactory = @{ 3 | '2.0.0' = @{ 4 | 'Get-Beer' = @{ 5 | Description = "Complete rewrite, requires different syntax and data input" 6 | } 7 | } 8 | '2.1.0' = @{ 9 | 'Get-Beer' = @{ 10 | Parameters = @{ 11 | Brand = 'Now expects a string' 12 | Size = 'No longer supports "Horn" as input' 13 | } 14 | } 15 | 'Remove-Beer' = @{ 16 | Description = 'Command was dropped due to heresy dispute.' 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /docs/transforms/command.transform.psd1: -------------------------------------------------------------------------------- 1 | <# 2 | # Command 3 | 4 | The "Command" token provider allows renaming commands and their parameters. 5 | It does NOT apply to your own function definitions, but the commands actually called. 6 | #> 7 | @{ 8 | # Must be included in all files. The version notation allows avoiding breaking changes in future updates 9 | Version = 1 10 | 11 | # The token provider to use. 12 | Type = 'Command' 13 | 14 | # The actual entries to process. This is where we place the individual transformation rules 15 | Content = @{ 16 | 17 | "Get-AzureADUser" = @{ 18 | # Name of the command, as it is used 19 | Name = "Get-AzureADUser" 20 | # (optional) New name to rename it to 21 | NewName = "Get-MgUser" 22 | # (optional) A comment to always include in the result 23 | MsgInfo = "Filter and search parameters cannot be mapped straight, may require manual attention" 24 | # (optional) A warning to always include in the result 25 | MsgWarning = 'Some warning text' 26 | # (optional) An error to always include in the result 27 | MsgError = 'Some error text' 28 | # (optional) Parameters to rename 29 | Parameters = @{ 30 | Search = "Filter" # Rename Search on "Get-AzureADUser" to "Filter" on "Get-MgUser" 31 | } 32 | # (optional) Include an informative message in the result in case the specified parameters is used 33 | InfoParameters = @{ 34 | Search = 'The search condition requires manual adjustment' 35 | } 36 | # (optional) Include a warning message in the result in case the specified parameters is used 37 | WarningParameters = @{ 38 | Search = 'The search condition requires manual adjustment' 39 | } 40 | # (optional) Include an error message in the result in case the specified parameters is used 41 | ErrorParameters = @{ 42 | Search = 'The search condition requires manual adjustment' 43 | } 44 | # (optional) Include an informative message in the result, if not all parameters can be resolved 45 | UnknownInfo = 'Significant breaking change happened in this command''s parameters, may require manual control.' 46 | # (optional) Include a warning message in the result, if not all parameters can be resolved 47 | UnknownWarning = 'Significant breaking change happened in this command''s parameters, may require manual control.' 48 | # (optional) Include an error message in the result, if not all parameters can be resolved 49 | UnknownError = 'Significant breaking change happened in this command''s parameters, may require manual control.' 50 | } 51 | 'Set-AzureADApplication' = @{ 52 | Name = 'Set-AzureADApplication' 53 | NewName = 'Set-MgADApplication' 54 | Parameters = @{ 55 | PublicClient = 'Public' 56 | } 57 | } 58 | 'Set-AzureADUser' = @{ 59 | Name = 'Set-AzureADUser' 60 | NewName = 'Set-AzADUser' 61 | Parameters = @{ 62 | EmployeeID = 'EmployeeID2' 63 | MailNickname = 'MailNickname2' 64 | Country = 'Country2' 65 | } 66 | InfoParameters = @{ 67 | Country = 'The Country parameter has a but in v7.1. See https://google.com' 68 | } 69 | WarningParameters = @{ 70 | CompanyName = 'The CompanyName parameter has been deprecated' 71 | } 72 | ErrorParameters = @{ 73 | UPNOrObjectId = 'The UPNOrObjectId parameter is broken' 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /docs/transforms/readme.md: -------------------------------------------------------------------------------- 1 | # Transform Example Files 2 | 3 | This folder contains example transformation files that illustrate the various ways you can apply the predefined token providers. 4 | The actual file-names are arbitrary and all token providers support both psd1 and json formats. 5 | All examples will at least be present in psd1, as those can be commented, allowing for some documentation inside of the file. 6 | -------------------------------------------------------------------------------- /library/Refactor/Refactor.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32505.173 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{AEBFC360-4C5A-4D17-BA91-498320ED5742}") = "Refactor", "Refactor\Refactor.csproj", "{A1A8F02B-4AF3-45DE-B11D-7D012BCFA6CD}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {A1A8F02B-4AF3-45DE-B11D-7D012BCFA6CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {A1A8F02B-4AF3-45DE-B11D-7D012BCFA6CD}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {A1A8F02B-4AF3-45DE-B11D-7D012BCFA6CD}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {A1A8F02B-4AF3-45DE-B11D-7D012BCFA6CD}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {FF5D9D1E-BA1C-4A20-B1D1-C059E18F9010} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /library/Refactor/Refactor/AstToken.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Management.Automation.Language; 3 | 4 | namespace Refactor 5 | { 6 | /// 7 | /// Generic AST token. 8 | /// 9 | public class AstToken : ScriptToken 10 | { 11 | /// 12 | /// The kind of token this is. 13 | /// Must match the name of the token provider to use. 14 | /// 15 | public override string Type { get { return "Ast"; } } 16 | 17 | /// 18 | /// The actual Text within the file that should be replaced after executing transform. 19 | /// 20 | public string Text { get { return Ast.Extent.Text; } } 21 | 22 | /// 23 | /// The start offset within the original code. 24 | /// Used to identify the code's position in the original text when replacing with transformed content. 25 | /// 26 | public int StartOffset { get { return Ast.Extent.StartOffset; } } 27 | 28 | /// 29 | /// Total length of the ast 30 | /// 31 | public int Length => Ast.Extent.EndOffset - Ast.Extent.StartOffset; 32 | 33 | /// 34 | /// The text the Ast should have 35 | /// 36 | public string NewText; 37 | 38 | /// 39 | /// Create a new ast token based off an ast object. 40 | /// 41 | /// The AST describing a powershell element 42 | public AstToken(Ast AstObject) 43 | { 44 | Ast = AstObject; 45 | Name = AstObject.GetType().Name; 46 | NewText = Ast.Extent.Text; 47 | } 48 | 49 | /// 50 | /// The changes applied to this token. 51 | /// 52 | /// The changes applied to this token. 53 | public override List GetChanges() 54 | { 55 | var changes = new List(); 56 | Change change = new Change(); 57 | change.Before = Ast.Extent.Text; 58 | change.After = NewText; 59 | change.Offset = Ast.Extent.StartOffset; 60 | change.Token = this; 61 | change.Path = Ast?.Extent.File; 62 | if (change.After != change.Before) 63 | changes.Add(change); 64 | return changes; 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /library/Refactor/Refactor/BreakingChange.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Refactor 8 | { 9 | /// 10 | /// Result Container for a Breaking Change Scan 11 | /// 12 | public class BreakingChange 13 | { 14 | // Code Metadata 15 | /// 16 | /// Path to the file being scanned 17 | /// 18 | public string Path; 19 | 20 | /// 21 | /// Name of the file being scanned 22 | /// 23 | public string Name; 24 | 25 | /// 26 | /// Line within the file where the result was found 27 | /// 28 | public int Line; 29 | 30 | // Command Data 31 | /// 32 | /// Name of the command that represents a breaking change 33 | /// 34 | public string Command; 35 | 36 | /// 37 | /// Name of the parameter affected by the breaking change 38 | /// 39 | public string Parameter; 40 | 41 | // Message 42 | /// 43 | /// What kind of break message is this. Generally only errors or warnings. 44 | /// 45 | public MessageType Type; 46 | 47 | /// 48 | /// Description of the breaking change 49 | /// 50 | public string Description; 51 | 52 | // Breaking Change Metadata 53 | /// 54 | /// Name of the module the command is from 55 | /// 56 | public string Module; 57 | 58 | /// 59 | /// Version in which the breaking change was introduced 60 | /// 61 | public string Version; 62 | 63 | /// 64 | /// Tags assigned to this breaking change 65 | /// 66 | public string[] Tags; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /library/Refactor/Refactor/Change.cs: -------------------------------------------------------------------------------- 1 | namespace Refactor 2 | { 3 | /// 4 | /// Type representing a single script change / string replacement operation 5 | /// 6 | public class Change 7 | { 8 | /// 9 | /// The text before applying the change 10 | /// 11 | public string Before; 12 | 13 | /// 14 | /// The text after applying the change 15 | /// 16 | public string After; 17 | 18 | /// 19 | /// The starting index in the source code from where to begin the replacement 20 | /// 21 | public int Offset; 22 | 23 | /// 24 | /// Path of the file in which changes should be applied 25 | /// 26 | public string Path; 27 | 28 | /// 29 | /// The token generating this change 30 | /// 31 | public ScriptToken Token; 32 | 33 | /// 34 | /// Any additional data that should be included in this change object 35 | /// 36 | public object Data; 37 | 38 | internal bool IsValid(ScriptFile File) 39 | { 40 | int index = File.GetEffectiveIndex(Offset); 41 | return ScriptFile.GetStringContent(File.Content, index, Before.Length) == Before; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /library/Refactor/Refactor/CommandToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Management.Automation.Language; 5 | 6 | namespace Refactor 7 | { 8 | /// 9 | /// Object representing a single command in a script 10 | /// 11 | public class CommandToken : ScriptToken 12 | { 13 | /// 14 | /// The kind of token this is. 15 | /// Must match the name of the token provider to use. 16 | /// 17 | public override string Type { get { return "Command"; } } 18 | 19 | /// 20 | /// The actual Text within the file that should be replaced after executing transform. 21 | /// 22 | public string Text { get { return Ast.Extent.Text; } } 23 | 24 | /// 25 | /// The start offset within the original code. 26 | /// Used to identify the code's position in the original text when replacing with transformed content. 27 | /// 28 | public int StartOffset { get { return Ast.Extent.StartOffset; } } 29 | 30 | /// 31 | /// The updated text after running the token transformation 32 | /// 33 | public string NewText; 34 | 35 | /// 36 | /// The parameters that are KNOWN to be used in the command 37 | /// 38 | public Dictionary Parameters = new Dictionary(StringComparer.OrdinalIgnoreCase); 39 | 40 | /// 41 | /// Whether we are confident having identified all the parameters used 42 | /// 43 | public bool ParametersKnown = true; 44 | 45 | /// 46 | /// Any splats used in this command 47 | /// 48 | public Dictionary Splats = new Dictionary(); 49 | 50 | /// 51 | /// Whether this command has any splats 52 | /// 53 | public bool HasSplat; 54 | 55 | /// 56 | /// Store additional changes here as needed. 57 | /// Intended to catch parameter renames that need to be applied to splatting hashtables. 58 | /// 59 | public List ParameterChanges = new List(); 60 | 61 | /// 62 | /// Create a new command token off a CommandAst object 63 | /// 64 | /// The Ast off which to build the Command Token 65 | public CommandToken(CommandAst Command) 66 | { 67 | Ast = Command; 68 | Name = ((StringConstantExpressionAst)Command.CommandElements[0]).Value; 69 | var parameters = Command.CommandElements.Where(o => o.GetType() == typeof(CommandParameterAst)); 70 | var nonParameters = Command.CommandElements.Where(o => o.GetType() != typeof(CommandParameterAst) && o != Command.CommandElements[0] && !IsSplat(o)); 71 | foreach (CommandParameterAst parameter in parameters) 72 | Parameters[parameter.ParameterName] = parameter.ParameterName; 73 | if (parameters.Count() != nonParameters.Count()) 74 | ParametersKnown = false; 75 | } 76 | 77 | /// 78 | /// Whether a given Ast object is a splat. 79 | /// 80 | /// The Ast object to check 81 | /// Whether a given Ast object is a splat. 82 | private bool IsSplat(Ast AstObject) 83 | { 84 | if (AstObject == null) 85 | return false; 86 | if (AstObject.GetType() != typeof(VariableExpressionAst)) 87 | return false; 88 | 89 | return ((VariableExpressionAst)AstObject).Splatted; 90 | } 91 | 92 | /// 93 | /// Returns changes the token would apply. 94 | /// Run after calling transform 95 | /// 96 | /// The changes applied by this token after its transformation 97 | public override List GetChanges() 98 | { 99 | List result = new List(); 100 | 101 | Change change = new Change(); 102 | change.Before = Text; 103 | change.After = NewText; 104 | change.Offset = StartOffset; 105 | change.Path = Ast.Extent.File; 106 | change.Token = this; 107 | change.Data = Ast; 108 | result.Add(change); 109 | 110 | if (ParameterChanges.Count > 0) 111 | result.AddRange(ParameterChanges); 112 | 113 | return result; 114 | } 115 | 116 | /// 117 | /// Add a new change to this token 118 | /// 119 | /// The text before the change 120 | /// The text after the change 121 | /// The starting offset in the source text being modified 122 | /// Any additional data to include (such as the Ast maybe) 123 | public void AddChange(string Before, string After, int Offset, object Data = null) 124 | { 125 | Change change = new Change(); 126 | change.Before = Before; 127 | change.After = After; 128 | change.Offset = Offset; 129 | change.Data = Data; 130 | change.Token = this; 131 | change.Path = Ast?.Extent.File; 132 | ParameterChanges.Add(change); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /library/Refactor/Refactor/Component/AstResult.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Management.Automation.Language; 3 | 4 | namespace Refactor.Component 5 | { 6 | /// 7 | /// Result set of an Ast Token scan. Returned by Read-ReAstComponent, processed by Write-ReAstComponent. 8 | /// 9 | public class AstResult 10 | { 11 | /// 12 | /// What kind of AST was it? 13 | /// 14 | public string Type { get => Token.Ast.GetType().Name; } 15 | 16 | /// 17 | /// Name of the file the AST token was generated from. 18 | /// 19 | public string FileName { get => File.Path.Split('\\', '/').Last(); } 20 | 21 | /// 22 | /// Starting line in the file that the AST token was generated from. 23 | /// 24 | public int Line { get => Token.Ast.Extent.StartLineNumber; } 25 | 26 | /// 27 | /// Path of the file the AST token was generated from. 28 | /// 29 | public string Path { get => File.Path; } 30 | 31 | /// 32 | /// Text of the AST 33 | /// 34 | public string Text { get => Token.Text; } 35 | 36 | /// 37 | /// New text of the AST 38 | /// 39 | public string NewText 40 | { 41 | get { return Token.NewText; } 42 | set { Token.NewText = value; } 43 | } 44 | 45 | /// 46 | /// Ast object found 47 | /// 48 | public Ast Ast { get => Token.Ast; } 49 | 50 | /// 51 | /// The token generated from the AST token provider 52 | /// 53 | public AstToken Token; 54 | 55 | /// 56 | /// The open ScriptFile, used for applying changes 57 | /// 58 | public ScriptFile File; 59 | 60 | /// 61 | /// The scan result of any given target processed by Read-ReAstComponent. 62 | /// In case you need to link all results from a single scan. 63 | /// 64 | public ScriptResult Result; 65 | 66 | /// 67 | /// Generate a new AST Result 68 | /// 69 | /// The token being represented 70 | /// The scriptfile scanned 71 | /// The result object containing all tokens from a given scan. 72 | public AstResult(AstToken Token, ScriptFile File, ScriptResult Result) 73 | { 74 | this.Token = Token; 75 | this.File = File; 76 | this.Result = Result; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /library/Refactor/Refactor/Component/ScriptFileConverted.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Refactor.Component 8 | { 9 | /// 10 | /// A summary object of a post-conversion scriptfile that was generated from Write-ReAstComponent. 11 | /// 12 | public class ScriptFileConverted 13 | { 14 | /// 15 | /// Name of the file converted 16 | /// 17 | public string Name; 18 | 19 | /// 20 | /// Path to the file that was converted (whether in situ or via OutPath to another path, this points at the original source) 21 | /// 22 | public string Path; 23 | 24 | /// 25 | /// Text-content of the file after it was transformed. 26 | /// 27 | public string Text; 28 | 29 | /// 30 | /// The ID of the search result that was converted. Just in case one needs to track things. 31 | /// 32 | public string ConversionID { get => Result.ID; } 33 | 34 | private ScriptResult Result; 35 | 36 | /// 37 | /// Create a new script file object representing a converted object. 38 | /// 39 | /// The search result that was applied. 40 | public ScriptFileConverted(ScriptResult Result) 41 | { 42 | this.Result = Result; 43 | Path = Result.File.Path; 44 | Name = Result.File.Path.Split('\\', '/').Last(); 45 | Text = Result.File.Content; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /library/Refactor/Refactor/Component/ScriptResult.cs: -------------------------------------------------------------------------------- 1 | using Refactor; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Refactor.Component 9 | { 10 | /// 11 | /// Result of a single token scan using Read-ReAstComponent 12 | /// 13 | public class ScriptResult 14 | { 15 | /// 16 | /// The scriptfile scanned 17 | /// 18 | public ScriptFile File; 19 | 20 | /// 21 | /// The list of tokens found 22 | /// 23 | public List Tokens = new List(); 24 | 25 | /// 26 | /// The types of AST scanned for. 27 | /// 28 | public string[] Types; 29 | 30 | /// 31 | /// Unique ID to differentiate between the executions 32 | /// 33 | public readonly string ID = Guid.NewGuid().ToString(); 34 | 35 | /// 36 | /// Default string display of this object 37 | /// 38 | /// The ID of the scan 39 | public override string ToString() 40 | { 41 | return ID; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /library/Refactor/Refactor/FunctionToken.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Management.Automation.Language; 3 | 4 | namespace Refactor 5 | { 6 | /// 7 | /// Token representing a function definition 8 | /// 9 | public class FunctionToken : ScriptToken 10 | { 11 | /// 12 | /// The kind of token this is. 13 | /// Must match the name of the token provider to use. 14 | /// 15 | public override string Type { get { return "Function"; } } 16 | 17 | /// 18 | /// The actual Text within the file that should be replaced after executing transform. 19 | /// 20 | public string Text { get { return Ast.Extent.Text; } } 21 | 22 | /// 23 | /// The start offset within the original code. 24 | /// Used to identify the code's position in the original text when replacing with transformed content. 25 | /// 26 | public int StartOffset { get { return Ast.Extent.StartOffset; } } 27 | 28 | /// 29 | /// Total length of the function definition 30 | /// 31 | public int Length => Ast.Extent.EndOffset - Ast.Extent.StartOffset; 32 | 33 | /// 34 | /// The changes applied to this Token. 35 | /// Filled by calling Transform() 36 | /// 37 | public List Changes = new List(); 38 | 39 | /// 40 | /// Create a new function token based off a function definition ast. 41 | /// 42 | /// The AST describing a function definition 43 | public FunctionToken(FunctionDefinitionAst Definition) 44 | { 45 | Ast = Definition; 46 | Name = Definition.Name; 47 | } 48 | 49 | /// 50 | /// The changes applied to this token. 51 | /// 52 | /// The changes applied to this token. 53 | public override List GetChanges() 54 | { 55 | var changes = new List(); 56 | changes.AddRange(Changes); 57 | return changes; 58 | } 59 | 60 | /// 61 | /// Add a new change to this token 62 | /// 63 | /// The text before the change 64 | /// The text after the change 65 | /// The starting offset in the source text being modified 66 | /// Any additional data to include (such as the Ast maybe) 67 | public void AddChange(string Before, string After, int Offset, object Data = null) 68 | { 69 | Change change = new Change(); 70 | change.Before = Before; 71 | change.After = After; 72 | change.Offset = Offset; 73 | change.Data = Data; 74 | change.Token = this; 75 | change.Path = Ast?.Extent.File; 76 | Changes.Add(change); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /library/Refactor/Refactor/GenericToken.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Refactor 4 | { 5 | /// 6 | /// A generic token class, intended for all-script Token provider 7 | /// 8 | public class GenericToken : ScriptToken 9 | { 10 | /// 11 | /// The kind of token this is. 12 | /// Must match the name of the token provider to use. 13 | /// 14 | public override string Type => TokenType; 15 | private string TokenType; 16 | 17 | /// 18 | /// A free field to add arbitrary data 19 | /// 20 | public object Data; 21 | 22 | /// 23 | /// The changes to apply during transform 24 | /// 25 | public List Changes = new List(); 26 | 27 | /// 28 | /// Returns changes the token would apply. 29 | /// Run after calling transform 30 | /// 31 | /// The changes applied by this token after its transformation 32 | public override List GetChanges() 33 | { 34 | return Changes; 35 | } 36 | 37 | /// 38 | /// Add a new change to this token 39 | /// 40 | /// The text before the change 41 | /// The text after the change 42 | /// The starting offset in the source text being modified 43 | /// Any additional data to include (such as the Ast maybe) 44 | public void AddChange(string Before, string After, int Offset, object Data = null) 45 | { 46 | Change change = new Change(); 47 | change.Before = Before; 48 | change.After = After; 49 | change.Offset = Offset; 50 | change.Data = Data; 51 | change.Token = this; 52 | change.Path = Ast?.Extent.File; 53 | Changes.Add(change); 54 | } 55 | 56 | /// 57 | /// Create a new generic token object 58 | /// 59 | /// The type of the token. Must match the Token Provider name employing the object. 60 | /// The name of the token. used to match against transforms. 61 | public GenericToken(string Type, string Name) 62 | { 63 | TokenType = Type; 64 | this.Name = Name; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /library/Refactor/Refactor/Host.cs: -------------------------------------------------------------------------------- 1 | namespace Refactor 2 | { 3 | /// 4 | /// Global tools, data and settings related to the Refactor module library 5 | /// 6 | public static class Host 7 | { 8 | /// 9 | /// Command used to list all available token providers 10 | /// 11 | public static string ProviderCommand = "Get-ReTokenProvider"; 12 | 13 | /// 14 | /// Command used convert tokens 15 | /// 16 | public static string TransformCommand = "Convert-ReScriptToken"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /library/Refactor/Refactor/Message.cs: -------------------------------------------------------------------------------- 1 | namespace Refactor 2 | { 3 | /// 4 | /// Status message of a transformation action 5 | /// 6 | public class Message 7 | { 8 | /// 9 | /// What level of information are we talking about? 10 | /// 11 | public MessageType Type = MessageType.Information; 12 | 13 | /// 14 | /// The actual message text 15 | /// 16 | public string Text; 17 | 18 | /// 19 | /// Any additional data to attach to the message 20 | /// 21 | public object Data; 22 | 23 | /// 24 | /// The Token on which things went wrong 25 | /// 26 | public ScriptToken Token; 27 | 28 | /// 29 | /// Create a new message object 30 | /// 31 | /// What level of information are we talking about? 32 | /// The actual message text 33 | /// Any additional data to attach to the message 34 | /// The Token on which things went wrong 35 | public Message(MessageType Type, string Text, object Data, ScriptToken Token) 36 | { 37 | this.Type = Type; 38 | this.Text = Text; 39 | this.Data = Data; 40 | this.Token = Token; 41 | } 42 | 43 | /// 44 | /// Text representation of the message 45 | /// 46 | /// text 47 | public override string ToString() 48 | { 49 | return $"{Type}: {Text}"; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /library/Refactor/Refactor/MessageType.cs: -------------------------------------------------------------------------------- 1 | namespace Refactor 2 | { 3 | /// 4 | /// The category of a Message object 5 | /// 6 | public enum MessageType 7 | { 8 | /// 9 | /// General Information 10 | /// 11 | Information = 1, 12 | 13 | /// 14 | /// Something was not entirely a success 15 | /// 16 | Warning = 2, 17 | 18 | /// 19 | /// Something went really wrong 20 | /// 21 | Error = 3 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /library/Refactor/Refactor/PSHelp.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Refactor 8 | { 9 | /// 10 | /// Represents the Comment Based Help of a function 11 | /// 12 | public class PSHelp 13 | { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /library/Refactor/Refactor/PSHelpExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Refactor 8 | { 9 | /// 10 | /// An example text for the help of a function 11 | /// 12 | public class PSHelpExample 13 | { 14 | /// 15 | /// The example code, showing how to use this command 16 | /// 17 | public string Code; 18 | 19 | /// 20 | /// The text describing what is happening 21 | /// 22 | public string Description = "INSERT DESCRIPTION"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /library/Refactor/Refactor/Refactor.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net48 5 | 6 | 7 | 8 | ..\..\..\Refactor\bin 9 | ..\..\..\Refactor\bin\Refactor.xml 10 | 11 | 12 | 13 | ..\..\..\Refactor\bin 14 | ..\..\..\Refactor\bin\Refactor.xml 15 | 16 | 17 | 18 | false 19 | 20 | 21 | 22 | 23 | C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll 24 | false 25 | False 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /library/Refactor/Refactor/ScriptToken.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Management.Automation; 3 | using System.Management.Automation.Language; 4 | 5 | namespace Refactor 6 | { 7 | /// 8 | /// The base class for token objects. 9 | /// Extend from this if you want to define your own custom token type 10 | /// 11 | public abstract class ScriptToken 12 | { 13 | /// 14 | /// Name of the token. 15 | /// Used for identifying transforms and matched against their index. 16 | /// 17 | public string Name; 18 | 19 | /// 20 | /// AST object representing the code content of the Token. 21 | /// Is optional 22 | /// 23 | public Ast Ast; 24 | 25 | /// 26 | /// The kind of token this is. 27 | /// Must match the name of the token provider to use. 28 | /// 29 | public abstract string Type { get; } 30 | 31 | /// 32 | /// The first line of the token. 33 | /// Cosmetic property for user convenience. 34 | /// 35 | public int Line 36 | { 37 | set { _Line = value; } 38 | get 39 | { 40 | if (_Line > 0) 41 | return _Line; 42 | if (Ast != null) 43 | return Ast.Extent.StartLineNumber; 44 | return 0; 45 | } 46 | } 47 | private int _Line; 48 | 49 | /// 50 | /// Whether the changes wrought by the token can be applied if only some of them remain valid 51 | /// 52 | public bool AllowPartial; 53 | 54 | /// 55 | /// List of messages that happened while either transforming or generating the token 56 | /// 57 | public List Messages = new List(); 58 | 59 | /// 60 | /// Writes a message to the token 61 | /// 62 | /// What kind of message? 63 | /// The message to write 64 | /// Any additional data to append 65 | public void WriteMessage(MessageType Type, string Message, object Data = null) 66 | { 67 | Messages.Add(new Message(Type, Message, Data, this)); 68 | } 69 | 70 | /// 71 | /// Convert the token-related text by applying transformation rule as implemented by the provider 72 | /// 73 | /// The new text that should replace the original text represented by this token. 74 | public void Transform() 75 | { 76 | using (PowerShell runspace = PowerShell.Create(RunspaceMode.CurrentRunspace)) 77 | { 78 | runspace.AddCommand(Host.TransformCommand); 79 | runspace.AddArgument(this); 80 | runspace.Invoke(); 81 | } 82 | } 83 | 84 | /// 85 | /// Returns changes the token would apply. 86 | /// Run after calling transform 87 | /// 88 | /// The changes applied by this token after its transformation 89 | public abstract List GetChanges(); 90 | 91 | /// 92 | /// The default string representation of a token 93 | /// 94 | /// The default string representation of a token 95 | public override string ToString() 96 | { 97 | return $"{Type} -> {Name}"; 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /library/Refactor/Refactor/SearchResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Management.Automation.Language; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Refactor 9 | { 10 | /// 11 | /// Result of an Ast search 12 | /// 13 | public class SearchResult 14 | { 15 | /// 16 | /// Starting line in the file 17 | /// 18 | public int Start => Ast.Extent.StartLineNumber; 19 | 20 | /// 21 | /// Ending line in the file 22 | /// 23 | public int End => Ast.Extent.EndLineNumber; 24 | 25 | /// 26 | /// name of the file from which the searchresult came 27 | /// 28 | public string FileName => Ast.Extent.File.Split('\\','/').Last(); 29 | 30 | /// 31 | /// Path to the file searched 32 | /// 33 | public string File => Ast.Extent.File; 34 | 35 | /// 36 | /// The actual Ast object 37 | /// 38 | public Ast Data => Ast; 39 | 40 | /// 41 | /// What kind of object it is 42 | /// 43 | public string Type => Ast.GetType().Name; 44 | 45 | private Ast Ast; 46 | 47 | /// 48 | /// Create a new search result 49 | /// 50 | /// The Ast object found 51 | public SearchResult(Ast Ast) 52 | { 53 | if (Ast == null) 54 | throw new ArgumentNullException("Ast"); 55 | this.Ast = Ast; 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /library/Refactor/Refactor/Splat.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Management.Automation.Language; 4 | 5 | namespace Refactor 6 | { 7 | /// 8 | /// A splat object, including the resolved parameters it will potentially bind 9 | /// 10 | public class Splat 11 | { 12 | /// 13 | /// Ast of the splat binding. 14 | /// NOT of where the hashtable is first declared, but where it is ultimately used. 15 | /// 16 | public Ast Ast; 17 | 18 | /// 19 | /// Parameters bound through the splat 20 | /// 21 | public Dictionary Parameters = new Dictionary(StringComparer.OrdinalIgnoreCase); 22 | 23 | /// 24 | /// Whether we can authoratively claim to know which parameters may be bound through this splat. 25 | /// 26 | public bool ParametersKnown = true; 27 | 28 | /// 29 | /// Additional instances where the hashtable is modified before being used for splatting. 30 | /// Allows transforms of Command to rename hashtable properties on parameter renames 31 | /// 32 | public Ast[] Assignments; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /library/Refactor/Refactor/TokenProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace Refactor 4 | { 5 | /// 6 | /// All the information needed to process a specific tokentype 7 | /// 8 | public class TokenProvider 9 | { 10 | /// 11 | /// Name of the provider 12 | /// 13 | public string Name; 14 | /// 15 | /// The property to use for indexing transforms 16 | /// 17 | public string TransformIndex; 18 | /// 19 | /// The parameters that must be specified when registering transforms 20 | /// 21 | public string[] TransformParametersMandatory; 22 | /// 23 | /// The parameters that may be specified when registering transforms. 24 | /// Use to build out dynamic parameters for the specific register call 25 | /// 26 | public string[] TransformParameters; 27 | /// 28 | /// Code that parses an Ast provided and generates tokens from it 29 | /// 30 | public ScriptBlock Tokenizer; 31 | /// 32 | /// Code that takes a token and applies relevant transforms to it 33 | /// 34 | public ScriptBlock Converter; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /library/Refactor/Refactor/TransformationResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Refactor 5 | { 6 | /// 7 | /// Result of all changes applied to a single file 8 | /// 9 | public class TransformationResult 10 | { 11 | /// 12 | /// path to the file being transformed 13 | /// 14 | public string Path; 15 | /// 16 | /// Name of the file being transformed 17 | /// 18 | public string FileName { get { return new System.IO.FileInfo(Path).Name; } } 19 | /// 20 | /// Whether the transformation was 100 successful 21 | /// 22 | public bool Success { get { return Results.Where(o => !o.Success).Count() == 0; } } 23 | /// 24 | /// Count of transformations executed 25 | /// 26 | public int Count { get { return Results.Count; } } 27 | /// 28 | /// Count of successful transformations executed 29 | /// 30 | public int SuccessCount { get { return Results.Where(o => o.Success).Count(); } } 31 | 32 | /// 33 | /// Number of information messages written during the transformation 34 | /// 35 | public int InfoCount => Messages.Where(o => o.Type == MessageType.Information).Count(); 36 | /// 37 | /// Number of warning messages written during the transformation 38 | /// 39 | public int WarningCount => Messages.Where(o => o.Type == MessageType.Warning).Count(); 40 | /// 41 | /// Number of error messages written during the transformation 42 | /// 43 | public int ErrorCount => Messages.Where(o => o.Type == MessageType.Error).Count(); 44 | 45 | /// 46 | /// All individual transformation results that have been applied - successful or otherwise 47 | /// 48 | public List Results = new List(); 49 | 50 | /// 51 | /// Create a new transformation result report 52 | /// 53 | /// Path to the script file being transformed 54 | public TransformationResult(string Path) 55 | { 56 | this.Path = Path; 57 | } 58 | 59 | #region ScriptFile Transform Message handling 60 | /// 61 | /// The messages that happened during this transformation 62 | /// 63 | public List Messages = new List(); 64 | 65 | /// 66 | /// Copy over all messages from the token object and restore the original messages 67 | /// 68 | /// The token to drain the messages from 69 | internal void DrainMessages(ScriptToken Token) 70 | { 71 | Messages.AddRange(Token.Messages); 72 | Token.Messages = tokenOriginalMessages; 73 | } 74 | 75 | /// 76 | /// Cache the current messages and create a new list just for this transformation 77 | /// 78 | /// The Token for which to prep the messages 79 | internal void PrepMessages(ScriptToken Token) 80 | { 81 | tokenOriginalMessages = Token.Messages; 82 | Token.Messages = new List(); 83 | } 84 | private List tokenOriginalMessages; 85 | #endregion ScriptFile Transform Message handling 86 | } 87 | } -------------------------------------------------------------------------------- /library/Refactor/Refactor/TransformationResultEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Refactor 4 | { 5 | /// 6 | /// Result of a single token transformation 7 | /// 8 | public class TransformationResultEntry 9 | { 10 | /// 11 | /// Whether the transformation was successful 12 | /// 13 | public bool Success; 14 | 15 | /// 16 | /// The change implemented 17 | /// 18 | public Change Change; 19 | 20 | /// 21 | /// The Token the transformation was applied to 22 | /// 23 | public ScriptToken Token; 24 | 25 | /// 26 | /// Any message included in the transformation 27 | /// 28 | public string Message; 29 | 30 | /// 31 | /// Any errors that happened during execution 32 | /// 33 | public Exception Error; 34 | 35 | /// 36 | /// Path to the file being transformed 37 | /// 38 | public string Path; 39 | 40 | /// 41 | /// Create a new, empty transformation result entry 42 | /// 43 | public TransformationResultEntry() 44 | { 45 | 46 | } 47 | /// 48 | /// Create a new, filled out transformation result entry 49 | /// 50 | /// Path to the file being transformed 51 | /// Whether the transformation was successful 52 | /// The actual change that was performed 53 | /// The Token the transformation was applied to 54 | /// Any message included in the transformation 55 | /// Any errors that happened during execution 56 | public TransformationResultEntry(string Path, bool Success, Change Change, ScriptToken Token, string Message = "", Exception Error = null) 57 | { 58 | this.Path = Path; 59 | this.Success = Success; 60 | this.Change = Change; 61 | this.Message = Message; 62 | this.Token = Token; 63 | this.Error = Error; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /testfiles/breakingChange/beer.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | 4 | ) 5 | 6 | $beers = Get-Beer -Size Maß 7 | 8 | foreach ($beer in $beers) { 9 | if ($beer.Brand -ne 'Kölsch') { 10 | Drink-Beer $beer 11 | } 12 | } 13 | 14 | Get-Beer -Brand Kölsch | Remove-Beer -------------------------------------------------------------------------------- /testfiles/commands.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [string] 4 | $UsersPath 5 | ) 6 | 7 | if (-not (Get-AzContext)) { 8 | throw "Connect with azure first! Connect-AzAccount ftw" 9 | } 10 | 11 | $users = Get-AzADUser 12 | $userConfig = Import-Csv $UsersPath 13 | 14 | $param = @{ 15 | Country = 'USA' 16 | 'CompanyName' = 'Contoso' 17 | "Foo" = 42 18 | } 19 | 20 | dir @param 21 | 22 | # Dummy code to simulate different hashtable assignments 23 | $property = 'EmployeeHireDate' 24 | $properties = @{ Name = 'MailNickname' } 25 | 26 | foreach ($entry in $userConfig) { 27 | if ($entry.UPN -notin $users.UserPrincipalName) { continue } 28 | $param.EmployeeID = $entry.ID 29 | $param['EmployeeID'] = $entry.ID 30 | $param.$property = $entry.JoinedAt 31 | $param[$property] = $entry.JoinedAt 32 | $param["$property"] = $entry.JoinedAt 33 | $param.Add('MailNickname', $entry.MailNickname) 34 | $param.Add($properties.Name, $entry.MailNickname) 35 | $param.$($properties.Name) = $entry.MailNickname 36 | $param[$($properties.Name)] = $entry.MailNickname 37 | 38 | Set-AzureADUser -UPNOrObjectId $entry.UPN @param 39 | } 40 | 41 | return 42 | 43 | # Some dummy code for this test 44 | Get-AzureADApplication | Set-AzureADApplication -IsDisabled $false ` 45 | -PublicClient $false -------------------------------------------------------------------------------- /testfiles/help.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Does something 4 | 5 | .DESCRIPTION 6 | Does something 7 | More details 8 | 9 | .PARAMETER ParamA 10 | Whatever 11 | 12 | .PARAMETER ParamB 13 | Whatever Else 14 | 15 | .PARAMETER ParamC 16 | Kind of unimportant 17 | So who cares about ParamC? 18 | 19 | .EXAMPLE 20 | PS C:\> Get-Test1 -ParamA $value 21 | 22 | Does something. 23 | Probably. 24 | 25 | .NOTES 26 | Version: 1.0.0 27 | Author: Max 28 | Company Contoso Ltd. 29 | #> 30 | function Get-Test1 { 31 | [CmdletBinding()] 32 | param ( 33 | [Parameter(Mandatory = $true)] 34 | $ParamA, 35 | [ValidateSet(1,2,3)] 36 | $ParamB, 37 | $ParamC 38 | ) 39 | } 40 | 41 | function Get-Test2 { 42 | <# 43 | .SYNOPSIS 44 | Does something 45 | 46 | .DESCRIPTION 47 | Does something 48 | More details 49 | 50 | .PARAMETER ParamA 51 | Whatever 52 | 53 | .PARAMETER ParamB 54 | Whatever Else 55 | 56 | .PARAMETER ParamC 57 | Kind of unimportant 58 | So who cares about ParamC? 59 | 60 | .EXAMPLE 61 | PS C:\> Get-Test2 -ParamA $value 62 | 63 | Does something. 64 | Probably. 65 | 66 | .NOTES 67 | Version: 1.0.0 68 | Author: Max 69 | Company Contoso Ltd. 70 | #> 71 | [CmdletBinding()] 72 | param ( 73 | [Parameter(Mandatory = $true)] 74 | $ParamA, 75 | [ValidateSet(1,2,3)] 76 | $ParamB, 77 | $ParamC 78 | ) 79 | } 80 | 81 | function Get-Test3 { 82 | <# 83 | .SYNOPSIS 84 | Does something 85 | 86 | .DESCRIPTION 87 | Does something 88 | More details 89 | 90 | .EXAMPLE 91 | PS C:\> Get-Test3 -ParamA $value 92 | 93 | Does something. 94 | Probably. 95 | 96 | .NOTES 97 | Version: 1.0.0 98 | Author: Max 99 | Company Contoso Ltd. 100 | #> 101 | [CmdletBinding()] 102 | param ( 103 | # Whatever 104 | [Parameter(Mandatory = $true)] 105 | $ParamA, 106 | # Whatever Else 107 | [ValidateSet(1,2,3)] 108 | $ParamB, 109 | 110 | # Kind of unimportant 111 | # So who cares about ParamC? 112 | $ParamC 113 | ) 114 | } --------------------------------------------------------------------------------