{"id":17586,"date":"2019-04-18T15:04:50","date_gmt":"2019-04-18T23:04:50","guid":{"rendered":"http:\/\/devblogs.microsoft.com\/powershell\/?p=17586"},"modified":"2024-04-26T10:43:49","modified_gmt":"2024-04-26T18:43:49","slug":"using-psscriptanalyzer-to-check-powershell-version-compatibility","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/powershell\/using-psscriptanalyzer-to-check-powershell-version-compatibility\/","title":{"rendered":"Using PSScriptAnalyzer to check PowerShell version compatibility"},"content":{"rendered":"<p><a href=\"https:\/\/github.com\/PowerShell\/PSScriptAnalyzer\">PSScriptAnalyzer<\/a> version 1.18 was released recently, and ships with powerful new rules that\ncan check PowerShell scripts for incompatibilities with other PowerShell versions and environments.<\/p>\n<p>In this blog post, the first in a series, we&#8217;ll see how to use these new rules to check a script for\nproblems running on PowerShell 3, 5.1 and 6.<\/p>\n<h2>Wait, what&#8217;s PSScriptAnalyzer?<\/h2>\n<p>PSScriptAnalzyer is a module providing static analysis, or <a href=\"https:\/\/en.wikipedia.org\/wiki\/Lint_(software)\">linting<\/a>, and some dynamic analysis\n(based on the state of your environment) for PowerShell. It&#8217;s able to find problems and fix bad\nhabits in PowerShell scripts as you create them, similar to the way the C# compiler will give you\nwarnings and find errors in C# code before it&#8217;s executed.<\/p>\n<p>If you use the VSCode PowerShell extension, you might have seen the &#8220;green squigglies&#8221; and problem\nreports that PSScriptAnalyzer generates for scripts you author:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/powershell.github.io\/PowerShell-Blog\/Images\/2019-03-01-Compatibility-checking-in-PSScriptAnalyzer\/pssa-vscode.jpg\" alt=\"Image of PSScriptAnalyzer linting in VSCode with green squigglies\" \/><\/p>\n<p>You can install PSScriptAnalyzer to use on your own scripts with:<\/p>\n<pre><code class=\"language-powershell\">Install-Module PSScriptAnalyzer -Scope CurrentUser<\/code><\/pre>\n<p>PSScriptAnalyzer works by running a series of rules on your scripts, each of which independently\nassesses some issue. For example <code>AvoidUsingCmdletAliases<\/code> checks that aliases aren&#8217;t used in\nscripts, and <code>MisleadingBackticks<\/code> checks that backticks at the ends of lines aren&#8217;t followed by\nwhitespace.<\/p>\n<p>For more information, see the\n<a href=\"https:\/\/devblogs.microsoft.com\/scripting\/psscriptanalyzer-deep-dive-part-1-of-4\/\">PSScriptAnalyzer deep dive blog series<\/a>.<\/p>\n<h2>Introducing the compatibility check rules<\/h2>\n<p>The new compatibility checking functionality is provided by three new rules:<\/p>\n<ul>\n<li><a href=\"https:\/\/learn.microsoft.com\/powershell\/utility-modules\/psscriptanalyzer\/rules\/usecompatiblesyntax\">PSUseCompatibleSyntax<\/a>, which checks whether a syntax used in a script will work in other\nPowerShell versions.<\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/powershell\/utility-modules\/psscriptanalyzer\/rules\/usecompatiblecommands\">PSUseCompatibleCommands<\/a>, which checks whether commands used in a script are available in\nother PowerShell environments.<\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/powershell\/utility-modules\/psscriptanalyzer\/rules\/usecompatibletypes\">PSUseCompatibleTypes<\/a>, which checks whether .NET types and static methods\/properties are\navailable in other PowerShell environments.<\/li>\n<\/ul>\n<p>The syntax check rule simply requires a list of PowerShell versions you want to target, and will\ntell you if a syntax used in your script won&#8217;t work in any of those versions.<\/p>\n<p>The command and type checking rules are more sophisticated and rely on profiles (catalogs of\ncommands and types available) from commonly used PowerShell platforms. They require configuration to\nuse these profiles, which we&#8217;ll go over below.<\/p>\n<p>For this post, we&#8217;ll look at configuring and using <strong>PSUseCompatibleSyntax<\/strong> and\n<strong>PSUseCompatibleCommands<\/strong> to check that a script works with different versions of PowerShell.\nWe&#8217;ll look at <strong>PSUseCompatibleTypes<\/strong> in a later post, although it&#8217;s configured very similarly to\n<strong>PSUseCompatibleCommands<\/strong>.<\/p>\n<h2>Working example: A small PowerShell script<\/h2>\n<p>Imagine we have a small (and contrived) archival script saved to <code>.\\archiveScript.ps1<\/code>:<\/p>\n<pre><code class=\"language-powershell\"># Import helper module to get folders to archive\r\nImport-Module -FullyQualifiedName @{ ModuleName = ArchiveHelper; ModuleVersion = '1.1' }\r\n\r\n$paths = Get-FoldersToArchive -RootPath 'C:\\Documents\\DocumentsToArchive\\'\r\n$archiveBasePath = '\\\\ArchiveServer\\DocumentArchive\\'\r\n\r\n# Dictionary to collect hashes\r\n$hashes = [System.Collections.Generic.Dictionary[string, string]]::new()\r\nforeach ($path in $paths)\r\n{\r\n    # Get the hash of the file and turn it into a base64 string\r\n    $hash = (Get-FileHash -LiteralPath $path).Hash\r\n\r\n    # Add this file to the hash catalog\r\n    $hashes[$hash] = $path\r\n\r\n    # Now give the archive a unique name and zip it up\r\n    $name = Split-Path -LeafBase $path\r\n    Compress-Archive -LiteralPath $path -DestinationPath (Join-Path $archiveBasePath \"$name-$hash.zip\")\r\n}\r\n\r\n# Add the hash catalog to the archive directory\r\nConvertTo-Json $hashes | Out-File -LiteralPath (Join-Path $archiveBasePath \"catalog.json\") -NoNewline<\/code><\/pre>\n<p>This script was written in PowerShell 6.2, and we&#8217;ve tested that it works there. But we also want to\nrun it on other machines, some of which run PowerShell 5.1 and some of which run PowerShell 3.0.<\/p>\n<p>Ideally we will test it on those other platforms, but it would be nice if we could try to iron out\nas many bugs as possible ahead of time.<\/p>\n<h2>Checking syntax with PSUseCompatibleSyntax<\/h2>\n<p>The first and easiest rule to apply is <strong>PSUseCompatibleSyntax<\/strong>. We&#8217;re going to create some\nsettings for PSScriptAnalyzer to enable the rule, and then run analysis on our script to get back\nany <em>diagnostics<\/em> about compatibility.<\/p>\n<p>Running PSScriptAnalyzer is straightforward. It comes as a PowerShell module, so once it&#8217;s installed\non your module path you just invoke it on your file with <code>Invoke-ScriptAnalyzer<\/code>, like this:<\/p>\n<pre><code class=\"language-powershell\">Invoke-ScriptAnalyzer -Path '.\\archiveScript.ps1`<\/code><\/pre>\n<p>A very simple invocation like this one will run PSScriptAnalyzer using its default rules and\nconfigurations on the script you point it to.<\/p>\n<p>However, because they require more targeted configuration, the compatibility rules are not enabled\nby default. Instead, we need to supply some settings to run the syntax check rule. In particular,\n<strong>PSUseCompatibleSyntax<\/strong> requires a list of the PowerShell versions you are targeting with your\nscript.<\/p>\n<pre><code class=\"language-powershell\">$settings = @{\r\n    Rules = @{\r\n        PSUseCompatibleSyntax = @{\r\n            # This turns the rule on (setting it to false will turn it off)\r\n            Enable = $true\r\n\r\n            # List the targeted versions of PowerShell here\r\n            TargetVersions = @(\r\n                '3.0',\r\n                '5.1',\r\n                '6.2'\r\n            )\r\n        }\r\n    }\r\n}\r\n\r\nInvoke-ScriptAnalyzer -Path .\\archiveScript.ps1 -Settings $settings<\/code><\/pre>\n<p>Running this will present us with the following output:<\/p>\n<pre><code class=\"language-Output\">RuleName                            Severity     ScriptName Line  Message\r\n--------                            --------     ---------- ----  -------\r\nPSUseCompatibleSyntax               Warning      archiveScr 8     The constructor syntax\r\n                                                 ipt.ps1          '[System.Collections.Generic.Dictionary[string,\r\n                                                                  string]]::new()' is not available by default in\r\n                                                                  PowerShell versions 3,4\r\n<\/code><\/pre>\n<p>This is telling us that the <code>[dictionary[string, string]]::new()<\/code> syntax we used won&#8217;t work in\nPowerShell 3. Better than that, in this case the rule has actually proposed a fix:<\/p>\n<pre><code class=\"language-powershell\">$diagnostics = Invoke-ScriptAnalyzer -Path .\\archiveScript.ps1 -Settings $settings\r\n$diagnostics[0].SuggestedCorrections<\/code><\/pre>\n<pre><code class=\"language-Output\">File              : C:\\Users\\roholt\\Documents\\Dev\\sandbox\\VersionedScript\\archiveScript.ps1\r\nDescription       : Use the 'New-Object @($arg1, $arg2, ...)' syntax instead for compatibility with PowerShell versions 3,4\r\nStartLineNumber   : 8\r\nStartColumnNumber : 11\r\nEndLineNumber     : 8\r\nEndColumnNumber   : 73\r\nText              : New-Object 'System.Collections.Generic.Dictionary[string,string]'\r\nLines             : {New-Object 'System.Collections.Generic.Dictionary[string,string]'}\r\nStart             : Microsoft.Windows.PowerShell.ScriptAnalyzer.Position\r\nEnd               : Microsoft.Windows.PowerShell.ScriptAnalyzer.Position<\/code><\/pre>\n<p>The suggested correction is to use <code>New-Object<\/code> instead. The way this is suggested might seem\nslightly unhelpful here with all the position information, but we&#8217;ll see later why this is useful.<\/p>\n<p>This dictionary example is a bit artificial of course (since a hashtable would come more naturally),\nbut having a spanner thrown into the works in PowerShell 3 or 4 because of a <code>::new()<\/code> is not\nuncommon. The <strong>PSUseCompatibleSyntax<\/strong> rule will also warn you about classes, workflows and <code>using<\/code>\nstatements depending on the versions of PowerShell you&#8217;re authoring for.<\/p>\n<p>We&#8217;re not going to make the suggested change just yet, since there&#8217;s more to show you first.<\/p>\n<h2>Checking command usage with PSUseCompatibleCommands<\/h2>\n<p>We now want to check the commands. Because command compatibility is a bit more complicated than\nsyntax (since the availability of commands depends on more than what version of PowerShell is being\nrun), we have to target <em>profiles<\/em> instead.<\/p>\n<p>Profiles are catalogs of information taken from stock machines running common PowerShell\nenvironments. The ones shipped in PSScriptAnalyzer can&#8217;t always match your working environment\nperfectly, but they come pretty close (there&#8217;s also a way to generate your own profile, detailed in\na later blog post). In our case, we&#8217;re trying to target PowerShell 3.0, PowerShell 5.1 and\nPowerShell 6.2 on Windows. We have the first two profiles, but in the last case we&#8217;ll need to target\n6.1 instead. These targets are very close, so warnings will still be pertinent to using PowerShell\n6.2. Later when a 6.2 profile is made available, we&#8217;ll be able to switch over to that.<\/p>\n<p>We need to look under the <a href=\"https:\/\/learn.microsoft.com\/powershell\/utility-modules\/psscriptanalyzer\/rules\/usecompatiblecommands\">PSUseCompatibleCommands<\/a> documentation for a list of profiles\navailable by default. For our desired targets we pick:<\/p>\n<ul>\n<li>PowerShell 6.1 on Windows Server 2019 (<code>win-8_x64_10.0.14393.0_6.1.3_x64_4.0.30319.42000_core<\/code>)<\/li>\n<li>PowerShell 5.1 on Windows Server 2019 (<code>win-8_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework<\/code>)<\/li>\n<li>PowerShell 3.0 on Windows Server 2012 (<code>win-8_x64_6.2.9200.0_3.0_x64_4.0.30319.42000_framework<\/code>)<\/li>\n<\/ul>\n<p>The long names on the right are canonical profile identifiers, which we use in the settings:<\/p>\n<pre><code class=\"language-powershell\">$settings = @{\r\n    Rules = @{\r\n        PSUseCompatibleCommands = @{\r\n            # Turns the rule on\r\n            Enable = $true\r\n\r\n            # Lists the PowerShell platforms we want to check compatibility with\r\n            TargetProfiles = @(\r\n                'win-8_x64_10.0.14393.0_6.1.3_x64_4.0.30319.42000_core',\r\n                'win-8_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework',\r\n                'win-8_x64_6.2.9200.0_3.0_x64_4.0.30319.42000_framework'\r\n            )\r\n        }\r\n    }\r\n}\r\n\r\nInvoke-ScriptAnalyzer -Path .\/archiveScript.ps1 -Settings $settings<\/code><\/pre>\n<p>There might be a delay the first time you execute this because the rules have to load the catalogs\ninto a cache. Each catalog of a PowerShell platform contains details of all the modules and .NET\nassemblies available to PowerShell on that platform, which can be as many as 1700 commands with\n15,000 parameters and 100 assemblies with 10,000 types. But once it&#8217;s loaded, further compatibility\nanalysis will be fast. We get output like this:<\/p>\n<pre><code class=\"language-Output\">RuleName                            Severity     ScriptName Line  Message\r\n--------                            --------     ---------- ----  -------\r\nPSUseCompatibleCommands             Warning      archiveScr 2     The parameter 'FullyQualifiedName' is not available for\r\n                                                 ipt.ps1          command 'Import-Module' by default in PowerShell version\r\n                                                                  '3.0' on platform 'Microsoft Windows Server 2012\r\n                                                                  Datacenter'\r\nPSUseCompatibleCommands             Warning      archiveScr 12    The command 'Get-FileHash' is not available by default in\r\n                                                 ipt.ps1          PowerShell version '3.0' on platform 'Microsoft Windows\r\n                                                                  Server 2012 Datacenter'\r\nPSUseCompatibleCommands             Warning      archiveScr 18    The parameter 'LeafBase' is not available for command\r\n                                                 ipt.ps1          'Split-Path' by default in PowerShell version\r\n                                                                  '5.1.17763.316' on platform 'Microsoft Windows Server\r\n                                                                  2019 Datacenter'\r\nPSUseCompatibleCommands             Warning      archiveScr 18    The parameter 'LeafBase' is not available for command\r\n                                                 ipt.ps1          'Split-Path' by default in PowerShell version '3.0' on\r\n                                                                  platform 'Microsoft Windows Server 2012 Datacenter'\r\nPSUseCompatibleCommands             Warning      archiveScr 19    The command 'Compress-Archive' is not available by\r\n                                                 ipt.ps1          default in PowerShell version '3.0' on platform\r\n                                                                  'Microsoft Windows Server 2012 Datacenter'\r\nPSUseCompatibleCommands             Warning      archiveScr 23    The parameter 'NoNewline' is not available for command\r\n                                                 ipt.ps1          'Out-File' by default in PowerShell version '3.0' on\r\n                                                                  platform 'Microsoft Windows Server 2012 Datacenter'<\/code><\/pre>\n<p>This is telling us that:<\/p>\n<ul>\n<li><code>Import-Module<\/code> doesn&#8217;t support <code>-FullyQualifiedName<\/code> in PowerShell 3.0;<\/li>\n<li><code>Get-FileHash<\/code> doesn&#8217;t exist in PowerShell 3.0;<\/li>\n<li><code>Split-Path<\/code> doesn&#8217;t have <code>-LeafBase<\/code> in PowerShell 5.1 or PowerShell 3.0;<\/li>\n<li><code>Compress-Archive<\/code> isn&#8217;t available in PowerShell 3.0, and;<\/li>\n<li><code>Out-File<\/code> doesn&#8217;t support <code>-NoNewline<\/code> in PowerShell 3.0<\/li>\n<\/ul>\n<p>One thing you&#8217;ll notice is that the <code>Get-FoldersToArchive<\/code> function is not being warned about. This\nis because the compatibility rules are designed to ignore user-provided commands; a command will\nonly be marked as incompatible if it&#8217;s present in <em>some<\/em> profile and not in one of your targets.<\/p>\n<p>Again, we can change the script to fix these warnings, but before we do, I want to show you how to\nmake this a more continuous experience; as you change your script, you want to know if the changes\nyou make break compatibility, and that&#8217;s easy to do with the steps below.<\/p>\n<h2>Using a settings file for repeated invocation<\/h2>\n<p>The first thing we want is to make the PSScriptAnalyzer invocation more automated and reproducible.\nA nice step toward this is taking the settings hashtable we made and turning it into a declarative\ndata file, separating out the &#8220;what&#8221; from the &#8220;how&#8221;.<\/p>\n<p>PSScriptAnalyzer will accept a path to a PSD1 in the <code>-Settings<\/code> parameter, so all we need to do is\nturn our hashtable into a PSD1 file, which we&#8217;ll make <code>.\/PSScriptAnalyzerSettings.psd1<\/code>. Notice we\ncan merge the settings for both <strong>PSUseCompatibleSyntax<\/strong> and <strong>PSUseCompatibleCommands<\/strong>:<\/p>\n<pre><code class=\"language-powershell\"># PSScriptAnalyzerSettings.psd1\r\n# Settings for PSScriptAnalyzer invocation.\r\n@{\r\n    Rules = @{\r\n        PSUseCompatibleCommands = @{\r\n            # Turns the rule on\r\n            Enable = $true\r\n\r\n            # Lists the PowerShell platforms we want to check compatibility with\r\n            TargetProfiles = @(\r\n                'win-8_x64_10.0.14393.0_6.1.3_x64_4.0.30319.42000_core',\r\n                'win-8_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework',\r\n                'win-8_x64_6.2.9200.0_3.0_x64_4.0.30319.42000_framework'\r\n            )\r\n        }\r\n        PSUseCompatibleSyntax = @{\r\n            # This turns the rule on (setting it to false will turn it off)\r\n            Enable = $true\r\n\r\n            # Simply list the targeted versions of PowerShell here\r\n            TargetVersions = @(\r\n                '3.0',\r\n                '5.1',\r\n                '6.2'\r\n            )\r\n        }\r\n    }\r\n}<\/code><\/pre>\n<p>Now we can run the PSScriptAnalyzer again on the script using the settings file:<\/p>\n<pre><code class=\"language-powershell\">Invoke-ScriptAnalyzer -Path .\/archiveScript.ps1 -Settings .\/PSScriptAnalyzerSettings.psd1<\/code><\/pre>\n<p>This gives the output:<\/p>\n<pre><code class=\"language-Output\">RuleName                            Severity     ScriptName Line  Message\r\n--------                            --------     ---------- ----  -------\r\nPSUseCompatibleCommands             Warning      archiveScr 1     The parameter 'FullyQualifiedName' is not available for\r\n                                                 ipt.ps1          command 'Import-Module' by default in PowerShell version\r\n                                                                  '3.0' on platform 'Microsoft Windows Server 2012\r\n                                                                  Datacenter'\r\nPSUseCompatibleCommands             Warning      archiveScr 9     The command 'Get-FileHash' is not available by default in\r\n                                                 ipt.ps1          PowerShell version '3.0' on platform 'Microsoft Windows\r\n                                                                  Server 2012 Datacenter'\r\nPSUseCompatibleCommands             Warning      archiveScr 12    The parameter 'LeafBase' is not available for command\r\n                                                 ipt.ps1          'Split-Path' by default in PowerShell version '3.0' on\r\n                                                                  platform 'Microsoft Windows Server 2012 Datacenter'\r\nPSUseCompatibleCommands             Warning      archiveScr 12    The parameter 'LeafBase' is not available for command\r\n                                                 ipt.ps1          'Split-Path' by default in PowerShell version\r\n                                                                  '5.1.17763.316' on platform 'Microsoft Windows Server\r\n                                                                  2019 Datacenter'\r\nPSUseCompatibleCommands             Warning      archiveScr 13    The command 'Compress-Archive' is not available by\r\n                                                 ipt.ps1          default in PowerShell version '3.0' on platform\r\n                                                                  'Microsoft Windows Server 2012 Datacenter'\r\nPSUseCompatibleCommands             Warning      archiveScr 16    The parameter 'NoNewline' is not available for command\r\n                                                 ipt.ps1          'Out-File' by default in PowerShell version '3.0' on\r\n                                                                  platform 'Microsoft Windows Server 2012 Datacenter'\r\nPSUseCompatibleSyntax               Warning      archiveScr 6     The constructor syntax\r\n                                                 ipt.ps1          '[System.Collections.Generic.Dictionary[string,\r\n                                                                  string]]::new()' is not available by default in\r\n                                                                  PowerShell versions 3,4<\/code><\/pre>\n<p>Now we don&#8217;t depend on any variables anymore, and have a separate spefication of the analysis you\nwant. Using this, you could put this into continuous integration environments for example to check\nthat changes in scripts don&#8217;t break compatibility.<\/p>\n<p>But what we really want is to know that PowerShell scripts stay compatible <em>as you edit them<\/em>.\nThat&#8217;s what the settings file is building to, and also where it&#8217;s easiest to make the changes you\nneed to make your script compatible. For that, we want to integrate with the VSCode PowerShell\nextension.<\/p>\n<h2>Integrating with VSCode for on-the-fly compatibility checking<\/h2>\n<p>As explained at the start of this post, VSCode PowerShell extension has builtin support for\nPSScriptAnalyzer. In fact, as of version 1.12.0, the PowerShell extension ships with\nPSScriptAnalyzer 1.18, meaning you don&#8217;t need to do anything other than create a settings file to do\ncompatibility analysis.<\/p>\n<p>We already have our settings file ready to go from the last step, so all we have to do is point the\nPowerShell extension to the file in the VSCode settings.<\/p>\n<p>You can open the settings with <kbd>Ctrl<\/kbd>+<kbd>,<\/kbd> (use <kbd>Cmd<\/kbd>+<kbd>,<\/kbd> on\nmacOS). In the <strong>Settings<\/strong> view, we want <code>PowerShell &gt; Script Analysis: Settings Path<\/code>. In the\n<code>settings.json<\/code> view this is <code>\"powershell.scriptAnalysis.settingsPath\"<\/code>. Entering a relative path\nhere will find a settings file in our workspace, so we just put <code>.\/PSScriptAnalyzerSettings.psd1<\/code>:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/powershell.github.io\/PowerShell-Blog\/Images\/2019-03-01-Compatibility-checking-in-PSScriptAnalyzer\/settings.jpg\" alt=\"VSCode settings GUI with PSScriptAnalyzer settings path configured to &quot;.\/PSScriptAnalyzerSettings.psd1&quot;\" \/><\/p>\n<p>In the <code>settings.json<\/code> view this will look like:<\/p>\n<pre><code class=\"language-json\">\"powershell.scriptAnalysis.settingsPath\": \".\/PSScriptAnalyzerSettings.psd1\"<\/code><\/pre>\n<p>Now, opening the script in VSCode we see &#8220;green squigglies&#8221; for compatibility warnings:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/powershell.github.io\/PowerShell-Blog\/Images\/2019-03-01-Compatibility-checking-in-PSScriptAnalyzer\/squigglies.jpg\" alt=\"VSCode window containing script, with green squigglies underneath incompatible code\" \/><\/p>\n<p>In the problems pane, you&#8217;ll get a full desrciption of all the incompatibilities:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/powershell.github.io\/PowerShell-Blog\/Images\/2019-03-01-Compatibility-checking-in-PSScriptAnalyzer\/problems.jpg\" alt=\"VSCode problems pane, listing and describing identified compatibility issues\" \/><\/p>\n<p>Let&#8217;s fix the syntax problem first. If you remember, PSScriptAnalyzer supplies a suggested\ncorrection to this problem. VSCode integrates with PSScriptAnalyzer&#8217;s suggested corrections and can\napply them if you click on the lightbulb or with <kbd>Ctrl<\/kbd>+<kbd>Space<\/kbd> when the region is\nunder the cursor:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/powershell.github.io\/PowerShell-Blog\/Images\/2019-03-01-Compatibility-checking-in-PSScriptAnalyzer\/suggestion.jpg\" alt=\"VSCode suggesting New-Object instead of ::new() syntax\" \/><\/p>\n<p>Applying this change, the script is now:<\/p>\n<pre><code class=\"language-powershell\">Import-Module -FullyQualifiedName @{ ModuleName = ArchiveHelper; ModuleVersion = '1.1' }\r\n\r\n$paths = Get-FoldersToArchive -RootPath 'C:\\Documents\\DocumentsToArchive\\'\r\n$archivePath = '\\\\ArchiveServer\\DocumentArchive\\'\r\n\r\n$hashes = New-Object 'System.Collections.Generic.Dictionary[string,string]'\r\nforeach ($path in $paths)\r\n{\r\n    $hash = (Get-FileHash -LiteralPath $path).Hash\r\n    $hashes[$hash] = $path\r\n    $name = Split-Path -LeafBase $path\r\n    Compress-Archive -LiteralPath $path -DestinationPath (Join-Path $archivePath \"$name-$hash.zip\")\r\n}\r\n\r\nConvertTo-Json $hashes | Out-File -LiteralPath (Join-Path $archivePath \"catalog.json\") -NoNewline<\/code><\/pre>\n<p>The other incompatibilities don&#8217;t have corrections; for now <strong>PSUseCompatibleCommands<\/strong> knows what\ncommands are available on each platform, but not what to substitute with when a command isn&#8217;t\navailable. So we just need to apply some PowerShell knowledge:<\/p>\n<ul>\n<li>Instead of <code>Import-Module -FullyQualifiedName @{...}<\/code> we use\n<code>Import-Module -Name ... -Version ...<\/code><\/li>\n<li>Instead of <code>Get-FileHash<\/code>, we&#8217;re going to need to use .NET directly and write a function<\/li>\n<li>Instead of <code>Split-Path -LeafBase<\/code>, we can use <code>[System.IO.Path]::GetFileNameWithoutExtension()<\/code><\/li>\n<li>Instead of <code>Compress-Archive<\/code> we&#8217;ll need to use more .NET methods in a function, and<\/li>\n<li>Instead of <code>Out-File -NoNewline<\/code> we can use <code>New-Item -Value<\/code><\/li>\n<\/ul>\n<p>We end up with something like this (the specific implementation is unimportant, but we have\nsomething that will work in all versions):<\/p>\n<pre><code class=\"language-powershell\">Import-Module -Name ArchiveHelper -Version '1.1'\r\n\r\nfunction CompatibleGetFileHash\r\n{\r\n    param(\r\n        [string]\r\n        $LiteralPath\r\n    )\r\n\r\n    try\r\n    {\r\n        $hashAlg = [System.Security.Cryptography.SHA256]::Create()\r\n        $file = [System.IO.File]::Open($LiteralPath, 'Open', 'Read')\r\n        $file.Position = 0\r\n        $hashBytes = $hashAlg.ComputeHash($file)\r\n        return [System.BitConverter]::ToString($hashBytes).Replace('-', '')\r\n    }\r\n    finally\r\n    {\r\n        $file.Dispose()\r\n        $hashAlg.Dispose()\r\n    }\r\n}\r\n\r\nfunction CompatibleCompressArchive\r\n{\r\n    param(\r\n        [string]\r\n        $LiteralPath,\r\n\r\n        [string]\r\n        $DestinationPath\r\n    )\r\n\r\n    if ($PSVersion.Major -le 3)\r\n    {\r\n        # PSUseCompatibleTypes identifies that [System.IO.Compression.ZipFile]\r\n        # isn't available by default in PowerShell 3 and we have to do this.\r\n        # We'll cover that rule in the next blog post.\r\n        Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Ignore\r\n    }\r\n\r\n    [System.IO.Compression.ZipFile]::Create(\r\n        $LiteralPath,\r\n        $DestinationPath,\r\n        'Optimal',\r\n        &lt;# includeBaseDirectory #&gt; $true)\r\n}\r\n\r\n$paths = Get-FoldersToArchive -RootPath 'C:\\Documents\\DocumentsToArchive\\'\r\n$archivePath = '\\\\ArchiveServer\\DocumentArchive\\'\r\n\r\n$hashes = New-Object 'System.Collections.Generic.Dictionary[string,string]'\r\nforeach ($path in $paths)\r\n{\r\n    $hash = CompatibleGetFileHash -LiteralPath $path\r\n    $hashes[$hash] = $path\r\n    $name = [System.IO.Path]::GetFileNameWithoutExtension($path)\r\n    CompatibleCompressArchive -LiteralPath $path -DestinationPath (Join-Path $archivePath \"$name-$hash.zip\")\r\n}\r\n\r\n$jsonStr = ConvertTo-Json $hashes\r\nNew-Item -Path (Join-Path $archivePath \"catalog.json\") -Value $jsonStr<\/code><\/pre>\n<p>You should notice that as you type, VSCode displays new analysis of what you&#8217;re writing and the\ngreen squigglies drop away. When we&#8217;re done we get a clean bill of health for script compatibility:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/powershell.github.io\/PowerShell-Blog\/Images\/2019-03-01-Compatibility-checking-in-PSScriptAnalyzer\/clean.jpg\" alt=\"VSCode window with script and problems pane, with no green squigglies and no problems\" \/><\/p>\n<p>This means you&#8217;ll now be able to use this script across all the PowerShell versions you need to\ntarget. Better, you now have a configuration in your workspace so as you write more scripts, there\nis continual checking for compatibility. And if your compatibility targets change, all you need to\ndo is change your configuration file in one place to point to your desired targets, at which point\nyou&#8217;ll get analysis for your updated target platforms.<\/p>\n<h2>Summary<\/h2>\n<p>Hopefully in this blog post you got some idea of the new compatibility rules that come with\nPSScriptAnalyzer 1.18.<\/p>\n<p>We&#8217;ve covered how to set up and use the syntax compatibility checking rule,\n<strong>PSUseCompatibleSyntax<\/strong>, and the command checking rule, <strong>PSUseCompatibleCommands<\/strong>, both using a\nhashtable configuration and a settings PSD1 file.<\/p>\n<p>We&#8217;ve also looked at using the compatibility rules in with the PowerShell extension for VSCode,\nwhere they come by default from version 1.12.0.<\/p>\n<p>If you&#8217;ve got the latest release of the PowerShell extension for VSCode (1.12.1), you&#8217;ll be able to\nset your configuration file and instantly get compatibility checking.<\/p>\n<p>In the next blog post, we&#8217;ll look at how to use these rules and <strong>PSUseCompatibleTypes<\/strong> (which\nchecks if .NET types and static methods are available on target platforms) can be used to help you\nwrite scripts that work cross platform across Windows and Linux using both Windows PowerShell and\nPowerShell Core.<\/p>\n<hr \/>\n<p>Rob Holt<\/p>\n<p>Software Engineer<\/p>\n<p>PowerShell Team<\/p>\n","protected":false},"excerpt":{"rendered":"<p>PSScriptAnalyzer version 1.18 was released recently, and ships with powerful new rules that can check PowerShell scripts for incompatibilities with other PowerShell versions and environments. In this blog post, the first in a series, we&#8217;ll see how to use these new rules to check a script for problems running on PowerShell 3, 5.1 and 6. [&hellip;]<\/p>\n","protected":false},"author":3098,"featured_media":13641,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[3182],"class_list":["post-17586","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-powershell","tag-psscriptanalyzer"],"acf":[],"blog_post_summary":"<p>PSScriptAnalyzer version 1.18 was released recently, and ships with powerful new rules that can check PowerShell scripts for incompatibilities with other PowerShell versions and environments. In this blog post, the first in a series, we&#8217;ll see how to use these new rules to check a script for problems running on PowerShell 3, 5.1 and 6. [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/powershell\/wp-json\/wp\/v2\/posts\/17586","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/powershell\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/powershell\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/powershell\/wp-json\/wp\/v2\/users\/3098"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/powershell\/wp-json\/wp\/v2\/comments?post=17586"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/powershell\/wp-json\/wp\/v2\/posts\/17586\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/powershell\/wp-json\/wp\/v2\/media\/13641"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/powershell\/wp-json\/wp\/v2\/media?parent=17586"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/powershell\/wp-json\/wp\/v2\/categories?post=17586"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/powershell\/wp-json\/wp\/v2\/tags?post=17586"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}