Skip to content

Commit

Permalink
Merge pull request #1 from Lombiq/issue/OSOE-150
Browse files Browse the repository at this point in the history
OSOE-150: Add PowerShell static code analysis
  • Loading branch information
Piedone authored Jul 21, 2022
2 parents 6cd6ea6 + ca91fb2 commit 0e584d9
Show file tree
Hide file tree
Showing 21 changed files with 551 additions and 6 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/publish-nuget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ on:

jobs:
call-publish-workflow:
uses: Lombiq/GitHub-Actions/.github/workflows/publish.yml@dev
uses: Lombiq/GitHub-Actions/.github/workflows/publish-nuget.yml@dev
with:
# These warnings are related to the tooling erroneously assuming that a ps1 file must be an
# (un)install script. In this repo the ps1 files are assets to be used by the consumer.
dotnet-pack-ignore-warning: NU5110 NU5111
secrets:
API_KEY: ${{ secrets.DEFAULT_NUGET_PUBLISH_API_KEY }}
37 changes: 37 additions & 0 deletions .github/workflows/test-analysis-failure.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Test Analysis Failure

# Runs for PRs opened for any branch, and pushes to the dev branch.
on:
pull_request:
push:
branches:
- dev

jobs:
call-test-analysis-failure-nuget:
name: Test Analysis Failure - NuGet PackageReference
uses: Lombiq/GitHub-Actions/.github/workflows/test-analysis-failure.yml@dev
with:
machine-types: "[\"ubuntu-latest\", \"windows-latest\"]"
build-directory: TestSolutions/Lombiq.Analyzers.PowerShell.PackageReference
timeout-minutes: 30
build-expected-code-analysis-errors: |
MSB3073: The command exited with non-zero code.
PSAvoidUsingEmptyCatchBlock: Empty catch block is used.
PSAvoidUsingCmdletAliases: 'echo' is an alias of 'Write-Output'.
PSUseApprovedVerbs: The cmdlet 'Violate-Analyzers' uses an unapproved verb.
PSUseSingularNouns: The cmdlet 'Violate-Analyzers' uses a plural noun.
call-test-analysis-failure-local:
name: Test Analysis Failure - Local ProjectReference
uses: Lombiq/GitHub-Actions/.github/workflows/test-analysis-failure.yml@dev
with:
machine-types: "[\"ubuntu-latest\", \"windows-latest\"]"
build-directory: TestSolutions/Lombiq.Analyzers.PowerShell.ProjectReference
timeout-minutes: 30
build-expected-code-analysis-errors: |
MSB3073: The command exited with non-zero code.
PSAvoidUsingEmptyCatchBlock: Empty catch block is used.
PSAvoidUsingCmdletAliases: 'echo' is an alias of 'Write-Output'.
PSUseApprovedVerbs: The cmdlet 'Violate-Analyzers' uses an unapproved verb.
PSUseSingularNouns: The cmdlet 'Violate-Analyzers' uses a plural noun.
2 changes: 1 addition & 1 deletion .github/workflows/verify-submodule-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ on:

jobs:
call-verify-workflow:
uses: Lombiq/GitHub-Actions/.github/workflows/verify.yml@dev
uses: Lombiq/GitHub-Actions/.github/workflows/verify-submodule-pull-request.yml@dev
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ wwwroot/
node_modules/
*.user
.pnpm-debug.log
.ps1-analyzer-stamp
/TestSolutions/Lombiq.Analyzers.PowerShell.*/Violate-Analyzers.ps1
.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\Libraries\Lombiq.HelpfulLibraries\Lombiq.HelpfulLibraries.Cli\Lombiq.HelpfulLibraries.Cli.csproj" />
<ProjectReference Include="..\..\..\test\Lombiq.Tests\Lombiq.Tests.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="CliWrap" Version="3.4.4" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using CliWrap;
using CliWrap.Buffered;
using Shouldly;
using System.IO;
using System.Threading.Tasks;
using Xunit;

namespace Lombiq.Analyzers.PowerShell.Tests.UnitTests;

public class PowerShellAnalysisTests
{
private static readonly Command _powerShell = Cli.Wrap("pwsh");

private static readonly DirectoryInfo _testSolutions = new(Path.Combine("..", "..", "..", "..", "TestSolutions"));

[Fact]
public async Task DirectScriptInvocationShouldDisplayWarnings()
{
if (!await IsPsScriptAnalyzerInstalledAsync()) return;

var result = await _powerShell
.WithWorkingDirectory(Path.GetFullPath(_testSolutions.FullName))
.WithArguments(new[] { "-c", "../Lombiq.Analyzers.PowerShell/Invoke-Analyzer.ps1 -IncludeTestSolutions" })
.WithValidation(CommandResultValidation.None)
.ExecuteBufferedAsync();

result.ExitCode.ShouldNotBe(0);
MessageShouldContainViolationCodes(result.StandardError);
}

// Ideally this method should skip the test programmatically instead of returning a value. Once XUnit 2.4.2-pre.19
// or newer is available, we will have Assert.Skip(skipMessage). See commit
// https://github.com/xunit/assert.xunit/commit/e6a6d5d22bbc7097f8decad5b3c8cac8cf3fb386 for implementation
// and issue https://github.com/xunit/xunit/issues/2073 for more information.
private static async Task<bool> IsPsScriptAnalyzerInstalledAsync()
{
try
{
await _powerShell
// The test should only run if PSScriptAnalyzer is installed. Even though it can install itself in PS7
// we should avoid relying on that as it harms testing performance on CI. It should be pre-installed.
.WithArguments(new[] { "-c", "Invoke-ScriptAnalyzer -ScriptDefinition 'Write-Output 1'" })
.ExecuteBufferedAsync();
return true;
}
catch
{
return false;
}
}

private static void MessageShouldContainViolationCodes(string message)
{
message.ShouldContain("PSAvoidUsingEmptyCatchBlock");
message.ShouldContain("PSAvoidUsingCmdletAliases");
message.ShouldContain("PSUseApprovedVerbs");
message.ShouldContain("PSUseSingularNouns");
}
}
32 changes: 32 additions & 0 deletions Lombiq.Analyzers.PowerShell.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32210.238
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lombiq.Analyzers.PowerShell", "Lombiq.Analyzers.PowerShell\Lombiq.Analyzers.PowerShell.csproj", "{8802AF0F-D4CA-4ABA-8408-F4433013FF7B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub Workflows", "GitHub Workflows", "{A44B4000-AF9F-47DD-A785-4778B6179090}"
ProjectSection(SolutionItems) = preProject
.github\workflows\publish-nuget.yml = .github\workflows\publish-nuget.yml
.github\workflows\test-analysis-failure.yml = .github\workflows\test-analysis-failure.yml
.github\workflows\verify-submodule-pull-request.yml = .github\workflows\verify-submodule-pull-request.yml
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8802AF0F-D4CA-4ABA-8408-F4433013FF7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8802AF0F-D4CA-4ABA-8408-F4433013FF7B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8802AF0F-D4CA-4ABA-8408-F4433013FF7B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8802AF0F-D4CA-4ABA-8408-F4433013FF7B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4EC68381-6918-49B8-83EA-74C87E8AA97E}
EndGlobalSection
EndGlobal
106 changes: 106 additions & 0 deletions Lombiq.Analyzers.PowerShell/Invoke-Analyzer.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
param(
$SettingsPath = (Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) 'PSScriptAnalyzerSettings.psd1'),
[Switch] $ForGitHubActions,
[Switch] $ForMsBuild,
[Switch] $IncludeTestSolutions
)

# This is like Get-ChildItem -Recurse -Include $IncludeFile | ? { $_.FullName -notlike "*\$ExcludeDirectory\*" } but
# much faster. For example, this is relevant for ignoring node_modules.
# - Measure-Command { Find-Recursively -Path . -IncludeFile *.ps1 -ExcludeDirectory node_modules } => 3.83s
# - Measure-Command { Get-ChildItem -Recurse -Force -Include $IncludeFile | ? { $_.FullName -notlike "*\$ExcludeDirectory\*" } } => 111.27s
function Find-Recursively([string] $Path = '.', [string[]] $IncludeFile, [string] $ExcludeDirectory)
{
$ExcludeDirectory = $ExcludeDirectory.ToUpperInvariant()

function Find-Inner([System.IO.DirectoryInfo] $Here)
{
if ($Here.Name -like $ExcludeDirectory)
{
return
}

# The -Force switch is necessary to show hidden results, especially on Linux where entries starting with dot
# are hidden by default.
foreach ($child in (Get-ChildItem $Here.FullName -Force))
{
if ($child -is [System.IO.DirectoryInfo]) { Find-Inner $child }
elseif (($IncludeFile | ? { $child.name -like $_ }).Count) { $child }
}
}

Find-Inner (Get-Item .)
}

function Write-FileError([string] $Message, [string] $Path, [int] $Line = 0, [int] $Column = 0)
{
if ($Path) { $Path = Get-Item $Path }

if ($ForGitHubActions)
{
$Message = $Message -replace '\s*(\r?\n\s*)+', ' '
Write-Output "::error$(if ($Path) { " file=$Path,line=$Line,col=$Column::$Message" } else { "::$Message" })"
}
elseif ($ForMsBuild)
{
if (-not $Message.Contains(":")) { $Message = ": $Message" }

if ($Path)
{
[Console]::Error.WriteLine("$Path($Line,$Column): error $Message")
}
else
{
[Console]::Error.WriteLine(": error $Message")
}
}
else
{
Write-Error $(if ($Path) { "[$Path|ln ${Line}:${Column}] $Message" } else { $Message })
}
}

if (Test-Path $SettingsPath)
{
$SettingsPath = Get-Item $SettingsPath
}
else
{
Write-FileError "The settings file `"$SettingsPath`" does not exist."
exit -1
}

$installVersion = "1.20.0"
if ((Get-InstalledModule PSScriptAnalyzer -ErrorAction SilentlyContinue).Version -ne [Version]$installVersion)
{
try
{
# Attempt to install it automatically. This will fail on Windows PowerShell because you have to be admin.
Install-Module -Name PSScriptAnalyzer -Force -RequiredVersion $installVersion
}
catch
{
$infoUrl = "https://docs.microsoft.com/en-us/powershell/utility-modules/psscriptanalyzer/overview?view=ps-modules#installing-psscriptanalyzer"
Write-FileError ("Unable to detect Invoke-ScriptAnalyzer and failed to install PSScriptAnalyzer. If you " +
"are on Windows Powershell, open an administrator shell and type `"Install-Module -Name " +
"PSScriptAnalyzer -Force -RequiredVersion $installVersion`". Otherwise see $infoUrl to learn more.")
exit -2
}
}

$results = Find-Recursively -IncludeFile "*.ps1", "*.psm1", "*.psd1" -ExcludeDirectory node_modules |
? { # Exclude /TestSolutions/Violate-Analyzers.ps1 and /TestSolutions/*/Violate-Analyzers.ps1
$IncludeTestSolutions -or -not (
$_.Name -eq 'Violate-Analyzers.ps1' -and
($_.Directory.Name -eq 'TestSolutions' -or $_.Directory.Parent.Name -eq 'TestSolutions')) } |
% { Invoke-ScriptAnalyzer $_.FullName -Settings $SettingsPath.FullName } |
# Only Warning and above (ignore "Information" type results).
? { $_.Severity -ge [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticSeverity]::Warning }

foreach ($result in $results)
{
$message = $result.RuleName + ": " + $result.Message
Write-FileError -Path $result.ScriptPath -Line $result.Line -Column $result.Column $message
}

exit $results.Count
15 changes: 15 additions & 0 deletions Lombiq.Analyzers.PowerShell/License.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Copyright © 2022, [Lombiq Technologies Ltd.](https://lombiq.com)

All rights reserved.

For more information and requests about licensing please [contact us through our website](https://lombiq.com/contact-us).

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

* Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PowerShellAnalyzersNuGet>true</PowerShellAnalyzersNuGet>
</PropertyGroup>
</Project>
34 changes: 34 additions & 0 deletions Lombiq.Analyzers.PowerShell/Lombiq.Analyzers.PowerShell.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">

<!-- This csproj file is only needed for NuGet package creation and to make the files visible in IDE. It doesn't
contain anything that needs to be built. -->

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<DefaultItemExcludes>$(DefaultItemExcludes);.git*</DefaultItemExcludes>
<IsPublishable>false</IsPublishable>
</PropertyGroup>

<PropertyGroup>
<Title>Lombiq PowerShell Analyzers</Title>
<Authors>Lombiq Technologies</Authors>
<Copyright>Copyright © 2022, Lombiq Technologies Ltd.</Copyright>
<Description>Lombiq PowerShell Analyzers: PowerShell static code analysis via PSScriptAnalyzer (https://github.com/PowerShell/PSScriptAnalyzer) and Lombiq's recommended configuration for it. Integrated with GitHub Actions. See the project website for detailed documentation.</Description>
<PackageIcon>NuGetIcon.png</PackageIcon>
<PackageTags>Lombiq;Powershell;MSBuild</PackageTags>
<RepositoryUrl>https://github.com/Lombiq/PowerShell-Analyzers</RepositoryUrl>
<PackageProjectUrl>https://github.com/Lombiq/PowerShell-Analyzers</PackageProjectUrl>
<PackageLicenseFile>License.md</PackageLicenseFile>
</PropertyGroup>

<ItemGroup>
<None Include="License.md" Pack="true" PackagePath="" />
<None Include="..\Readme.md" />
<None Include="NuGetIcon.png" Pack="true" PackagePath="" />
<None Include="Lombiq.Analyzers.PowerShell.targets" Pack="true" PackagePath="build\Lombiq.Analyzers.PowerShell.targets" />
<None Include="Lombiq.Analyzers.PowerShell.NuGet.props" Pack="true" PackagePath="build\Lombiq.Analyzers.PowerShell.props" />
<None Include="Invoke-Analyzer.ps1" Pack="true" PackagePath="" />
<None Include="PSScriptAnalyzerSettings.psd1" Pack="true" PackagePath="" />
</ItemGroup>

</Project>
49 changes: 49 additions & 0 deletions Lombiq.Analyzers.PowerShell/Lombiq.Analyzers.PowerShell.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<PropertyGroup>
<!-- It's enabled by default. Copy this into your csproj file if you want to disable stamp creation.
<CreatePowerShellAnalyzersStampFile>false</CreatePowerShellAnalyzersStampFile> -->
<PowerShellAnalyzersStampFile>.ps1-analyzer-stamp</PowerShellAnalyzersStampFile>

<PowerShellAnalyzersRootDirectory Condition="'$(PowerShellAnalyzersRootDirectory)' == '' AND Exists($(SolutionDir))"
>$(SolutionDir)</PowerShellAnalyzersRootDirectory>
<PowerShellAnalyzersRootDirectory Condition="'$(PowerShellAnalyzersRootDirectory)' == '' AND !Exists($(SolutionDir))"
>$(ProjectDir)</PowerShellAnalyzersRootDirectory>

<PowerShellAnalyzersScriptDirectory Condition="'$(PowerShellAnalyzersNuGet)' != 'true'">$(MSBuildThisFileDirectory)</PowerShellAnalyzersScriptDirectory>
<PowerShellAnalyzersScriptDirectory Condition="'$(PowerShellAnalyzersNuGet)' == 'true'">$(MSBuildThisFileDirectory)..\</PowerShellAnalyzersScriptDirectory>

<PowerShellAnalyzersArguments Condition="'$(PowerShellAnalyzersArguments)' == ''">-ForMsBuild</PowerShellAnalyzersArguments>
</PropertyGroup>

<Target
Name="PowerShellValidation"
AfterTargets="AfterResolveReferences"
BeforeTargets="Compile"
Inputs="**\*.ps1"
Outputs="$(PowerShellAnalyzersStampFile)">

<!-- Check if PowerShell 7+ (also known as PowerShell Core or pwsh) is installed. This also gets the correct result
in case pwsh is an alias to powershell. -->
<Exec Command="pwsh -Version" IgnoreExitCode="true">
<Output TaskParameter="ExitCode" PropertyName="PowerShellCoreExitCode"/>
</Exec>

<!-- Select PowerShell executable. -->
<PropertyGroup Condition="'$(PowerShellCoreExitCode)' == '0'">
<PowerShellExecutable>pwsh</PowerShellExecutable>
</PropertyGroup>
<PropertyGroup Condition="'$(PowerShellCoreExitCode)' != '0'">
<PowerShellExecutable>powershell</PowerShellExecutable>
</PropertyGroup>

<Exec Command="$(PowerShellExecutable) -File $(PowerShellAnalyzersScriptDirectory)Invoke-Analyzer.ps1 $(PowerShellAnalyzersArguments)"
WorkingDirectory="$(PowerShellAnalyzersRootDirectory)"
IgnoreStandardErrorWarningFormat="$(PowerShellAnalyzersArguments.Contains('-ForGitHubAction'))"/>

<Touch Condition="'$(CreatePowerShellAnalyzersStampFile)' != 'false'"
Files="$(PowerShellAnalyzersStampFile)"
AlwaysCreate="true" />
</Target>

</Project>
Binary file added Lombiq.Analyzers.PowerShell/NuGetIcon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 0e584d9

Please sign in to comment.