Skip to content

Commit

Permalink
(#32) Add PSScriptAnalyzer
Browse files Browse the repository at this point in the history
This adds a task to run PSScriptAnalyzer. It utilizes the Cake.Powershell
module to run a script that will get ps1/psm1 files that are not excluded
and run them through the specified settings file.

Either a default formatting only run can be done, or custom run(s) can be
specified. A custom base analysis path, setting file and set of folder
exclusions can be specified for each run. For example, a custom run can
be created to check that the Chocolatey powershell helpers use only the
PSv2 cmdlets.

The default run is for formatting, it checks files against the build in
formatting related rules. This run can be expanded in the future, once
enough projects are up to a baseline of good scripting practices.
  • Loading branch information
TheCakeIsNaOH authored and gep13 committed Aug 21, 2023
1 parent f7e01f1 commit 4662ea8
Show file tree
Hide file tree
Showing 10 changed files with 382 additions and 1 deletion.
1 change: 1 addition & 0 deletions Chocolatey.Cake.Recipe/Content/analyzing.cake
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,5 @@ BuildParameters.Tasks.AnalyzeTask = Task("Analyze")
.IsDependentOn("InspectCode")
.IsDependentOn("Run-DotNetFormatCheck")
.IsDependentOn("CreateIssuesReport")
.IsDependentOn("Run-PSScriptAnalyzer")
.WithCriteria(() => BuildParameters.ShouldRunAnalyze, "Skipping because running analysis tasks is not enabled");
70 changes: 70 additions & 0 deletions Chocolatey.Cake.Recipe/Content/formatting-settings.psd1
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
@{
IncludeRules = @(
'PSUseBOMForUnicodeEncodedFile',
'PSMisleadingBacktick',
'PSAvoidUsingCmdletAliases',
'PSAvoidTrailingWhitespace',
'PSAvoidSemicolonsAsLineTerminators',
'PSUseCorrectCasing',
'PSPlaceOpenBrace',
'PSPlaceCloseBrace',
'PSAlignAssignmentStatement',
'PSUseConsistentWhitespace',
'PSUseConsistentIndentation'
)

Rules = @{

<#
PSAvoidUsingCmdletAliases = @{
'allowlist' = @('')
}#>

PSAvoidSemicolonsAsLineTerminators = @{
Enable = $true
}

PSUseCorrectCasing = @{
Enable = $true
}

PSPlaceOpenBrace = @{
Enable = $true
OnSameLine = $true
NewLineAfter = $true
IgnoreOneLineBlock = $false
}

PSPlaceCloseBrace = @{
Enable = $true
NewLineAfter = $true
IgnoreOneLineBlock = $false
NoEmptyLineBefore = $true
}

PSAlignAssignmentStatement = @{
Enable = $true
CheckHashtable = $true
}

PSUseConsistentIndentation = @{
Enable = $true
Kind = 'space'
PipelineIndentation = 'IncreaseIndentationForFirstPipeline'
IndentationSize = 4
}

PSUseConsistentWhitespace = @{
Enable = $true
CheckInnerBrace = $true
CheckOpenBrace = $true
CheckOpenParen = $true
CheckOperator = $true
CheckPipe = $true
CheckPipeForRedundantWhitespace = $false
CheckSeparator = $true
CheckParameter = $false
IgnoreAssignmentOperatorInsideHashTable = $true
}
}
}
33 changes: 33 additions & 0 deletions Chocolatey.Cake.Recipe/Content/install-module.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Copyright © 2023 Chocolatey Software, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

[cmdletBinding()]
Param(
[Parameter()]
[String]
$ModuleName,

[Parameter()]
[String]
$RequiredVersion
)

if (Get-Module -ListAvailable -Name $ModuleName) {
Write-Host "The $ModuleName PowerShell Module is already installed."
}
else {
Write-Host "Install Module $ModuleName..."
Install-Module -Name $ModuleName -RequiredVersion $RequiredVersion -Force
}
15 changes: 14 additions & 1 deletion Chocolatey.Cake.Recipe/Content/parameters.cake
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ public static class BuildParameters
public static Func<FilePathCollection> GetFilesToObfuscate { get; private set; }
public static Func<FilePathCollection> GetFilesToSign { get; private set; }
public static Func<List<ILMergeConfig>> GetILMergeConfigs { get; private set; }
public static Func<List<PSScriptAnalyzerSettings>> GetPSScriptAnalyzerSettings { get; private set; }
public static Func<FilePathCollection> GetMsisToSign { get; private set; }
public static Func<FilePathCollection> GetProjectsToPack { get; private set; }
public static Func<FilePathCollection> GetScriptsToSign { get; private set; }
Expand Down Expand Up @@ -181,6 +182,7 @@ public static class BuildParameters
public static bool ShouldRunTests { get; private set ;}
public static bool ShouldRunTransifex { get; set; }
public static bool ShouldRunxUnit { get; private set; }
public static bool ShouldRunPSScriptAnalyzer { get; private set; }
public static bool ShouldStrongNameOutputAssemblies { get; private set; }
public static bool ShouldStrongNameSignDependentAssemblies { get; private set; }
public static SlackCredentials Slack { get; private set; }
Expand Down Expand Up @@ -313,6 +315,7 @@ public static class BuildParameters
context.Information("ShouldRunTests: {0}", BuildParameters.ShouldRunTests);
context.Information("ShouldRunTransifex: {0}", BuildParameters.ShouldRunTransifex);
context.Information("ShouldRunxUnit: {0}", BuildParameters.ShouldRunxUnit);
context.Information("ShouldRunPSScriptAnalyzer: {0}", BuildParameters.ShouldRunPSScriptAnalyzer);
context.Information("ShouldStrongNameOutputAssemblies: {0}", BuildParameters.ShouldStrongNameOutputAssemblies);
context.Information("ShouldStrongNameSignDependentAssemblies: {0}", BuildParameters.ShouldStrongNameSignDependentAssemblies);
context.Information("SolutionDirectoryPath: {0}", context.MakeAbsolute((DirectoryPath)SolutionDirectoryPath));
Expand Down Expand Up @@ -352,6 +355,7 @@ public static class BuildParameters
Func<FilePathCollection> getFilesToObfuscate = null,
Func<FilePathCollection> getFilesToSign = null,
Func<List<ILMergeConfig>> getILMergeConfigs = null,
Func<List<PSScriptAnalyzerSettings>> getPSScriptAnalyzerSettings = null,
Func<FilePathCollection> getMsisToSign = null,
Func<FilePathCollection> getProjectsToPack = null,
Func<FilePathCollection> getScriptsToSign = null,
Expand Down Expand Up @@ -418,6 +422,7 @@ public static class BuildParameters
bool shouldRunTests = true,
bool? shouldRunTransifex = null,
bool shouldRunxUnit = true,
bool shouldRunPSScriptAnalyzer = true,
bool shouldStrongNameOutputAssemblies = true,
bool shouldStrongNameSignDependentAssemblies = true,
Func<BuildVersion, object[]> slackMessageArguments = null,
Expand Down Expand Up @@ -482,6 +487,7 @@ public static class BuildParameters
GetFilesToObfuscate = getFilesToObfuscate;
GetFilesToSign = getFilesToSign;
GetILMergeConfigs = getILMergeConfigs;
GetPSScriptAnalyzerSettings = getPSScriptAnalyzerSettings;
GetMsisToSign = getMsisToSign;
GetProjectsToPack = getProjectsToPack;
GetScriptsToSign = getScriptsToSign;
Expand Down Expand Up @@ -738,6 +744,13 @@ public static class BuildParameters
ShouldRunOpenCover = context.Argument<bool>("shouldRunOpenCover");
}

ShouldRunPSScriptAnalyzer = shouldRunPSScriptAnalyzer;

if (context.HasArgument("shouldRunPSScriptAnalyzer"))
{
ShouldRunPSScriptAnalyzer = context.Argument<bool>("shouldRunPSScriptAnalyzer");
}

ShouldRunReportGenerator = shouldRunReportGenerator;

if (context.HasArgument("shouldRunReportGenerator"))
Expand Down Expand Up @@ -772,7 +785,7 @@ public static class BuildParameters
}

ShouldRunxUnit = shouldRunxUnit;

if (context.HasArgument("shouldRunxUnit"))
{
ShouldRunxUnit = context.Argument<bool>("shouldRunxUnit");
Expand Down
5 changes: 5 additions & 0 deletions Chocolatey.Cake.Recipe/Content/paths.cake
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public class BuildPaths

var testResultsDirectory = buildDirectoryPath + "/TestResults";
var inspectCodeResultsDirectory = testResultsDirectory + "/InspectCode";
var scriptAnalyzerResultsDirectory = testResultsDirectory + "/PSScriptAnalyzer";
var NUnitTestResultsDirectory = testResultsDirectory + "/NUnit";
var xUnitTestResultsDirectory = testResultsDirectory + "/xUnit";

Expand Down Expand Up @@ -75,6 +76,7 @@ public class BuildPaths
nugetNuspecDirectory,
testResultsDirectory,
inspectCodeResultsDirectory,
scriptAnalyzerResultsDirectory,
NUnitTestResultsDirectory,
xUnitTestResultsDirectory,
testCoverageDirectory,
Expand Down Expand Up @@ -165,6 +167,7 @@ public class BuildDirectories
public DirectoryPath NuGetNuspecDirectory { get; private set; }
public DirectoryPath TestResults { get; private set; }
public DirectoryPath InspectCodeTestResults { get; private set; }
public DirectoryPath PSScriptAnalyzerResults { get; private set; }
public DirectoryPath NUnitTestResults { get; private set; }
public DirectoryPath xUnitTestResults { get; private set; }
public DirectoryPath TestCoverage { get; private set; }
Expand All @@ -188,6 +191,7 @@ public class BuildDirectories
DirectoryPath nugetNuspecDirectory,
DirectoryPath testResults,
DirectoryPath inspectCodeTestResults,
DirectoryPath scriptAnalyzerResults,
DirectoryPath nunitTestResults,
DirectoryPath xunitTestResults,
DirectoryPath testCoverage,
Expand All @@ -210,6 +214,7 @@ public class BuildDirectories
NuGetNuspecDirectory = nugetNuspecDirectory;
TestResults = testResults;
InspectCodeTestResults = inspectCodeTestResults;
PSScriptAnalyzerResults = scriptAnalyzerResults;
NUnitTestResults = nunitTestResults;
xUnitTestResults = xunitTestResults;
TestCoverage = testCoverage;
Expand Down
123 changes: 123 additions & 0 deletions Chocolatey.Cake.Recipe/Content/psscriptanalyzer.cake
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright © 2023 Chocolatey Software, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
//
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

BuildParameters.Tasks.PSScriptAnalyzerTask = Task("Run-PSScriptAnalyzer")
.WithCriteria(() => BuildParameters.ShouldRunPSScriptAnalyzer, "Skipping because PSScriptAnalyzer is not enabled")
.WithCriteria(() => BuildParameters.ShouldRunAnalyze, "Skipping because running analysis tasks is not enabled")
.Does(() => RequirePSModule("PSScriptAnalyzer", "1.21.0", () =>
RequirePSModule("ConvertToSARIF", "1.0.0", () => {
var powerShellAnalysisScript = GetFiles("./tools/Chocolatey.Cake.Recipe*/Content/run-psscriptanalyzer.ps1").FirstOrDefault();

if (powerShellAnalysisScript == null)
{
Warning("Unable to find PowerShell Analysis script, so unable to run analysis.");
return;
}

var outputFolder = MakeAbsolute(BuildParameters.Paths.Directories.PSScriptAnalyzerResults).FullPath;
EnsureDirectoryExists(outputFolder);

if (BuildParameters.GetPSScriptAnalyzerSettings != null)
{
foreach (var PSScriptAnalyzerSetting in BuildParameters.GetPSScriptAnalyzerSettings())
{
Information(string.Format("Running PSScriptAnalyzer {0}", PSScriptAnalyzerSetting.Name));

StartPowershellFile(MakeAbsolute(powerShellAnalysisScript), new PowershellSettings()
.WithModule("PSScriptAnalyzer")
.WithModule("ConvertToSARIF")
.WithModule("Microsoft.PowerShell.Management")
.WithModule("Microsoft.PowerShell.Utility")
.SetFormatOutput(false)
.SetLogOutput(true)
.OutputToAppConsole(true)
.WithArguments(args => {
args.AppendQuoted("AnalyzePath", PSScriptAnalyzerSetting.AnalysisPath.ToString())
.AppendQuoted("SettingsPath", PSScriptAnalyzerSetting.SettingsPath.ToString())
.AppendQuoted("OutputPath", outputFolder)
.AppendArray("ExcludePaths", PSScriptAnalyzerSetting.ExcludePaths);
}));
}
}
else
{
Information("There are no PSScriptAnalyzer Settings defined for this build, running with default format checking settings.");

var settingsFile = GetFiles("./tools/Chocolatey.Cake.Recipe*/Content/formatting-settings.psd1").FirstOrDefault();

if (settingsFile == null)
{
Warning("Unable to find PowerShell Analysis settings, so unable to run analysis.");
return;
}

var pwshSettings = new PowershellSettings()
.WithModule("PSScriptAnalyzer")
.WithModule("ConvertToSARIF")
.WithModule("Microsoft.PowerShell.Management")
.WithModule("Microsoft.PowerShell.Utility")
.SetFormatOutput(false)
.SetLogOutput(true)
.OutputToAppConsole(true)
.WithArguments(args => {
args.AppendQuoted("AnalyzePath", BuildParameters.RootDirectoryPath.ToString())
.AppendQuoted("SettingsPath", settingsFile.ToString())
.AppendQuoted("OutputPath", outputFolder)
.AppendArray("ExcludePaths", ToolSettings.PSScriptAnalyzerExcludePaths);
});

pwshSettings.ExceptionOnScriptError = false;

var resultCollection = StartPowershellFile(MakeAbsolute(powerShellAnalysisScript), pwshSettings);
var returnCode = int.Parse(resultCollection[0].BaseObject.ToString());

Information("Result: {0}", returnCode);

// NOTE: Ideally, we would have this throw an exception, however, during testing, invoking PSScriptAnalyzer
// sometimes caused random "The term 'Get-Command' is not recognized as the name of a cmdlet, function, script
// file, or operable program." errors, which meant that we can't rely on this returnCode. For now, we
// only want PSScriptAnalyzer to warn on violations, so we don't need to worry about this just now.
//if (returnCode != 0)
//{
// throw new ApplicationException("Script failed to execute");
//}
}
})
)
);

public class PSScriptAnalyzerSettings
{
public FilePath AnalysisPath { get; set; }
public FilePath SettingsPath { get; set; }
public List<String> ExcludePaths { get; set; }
public string Name { get; set; }

public PSScriptAnalyzerSettings()
{
Name = "Unnamed";
}

public PSScriptAnalyzerSettings(FilePath analysisPath,
FilePath settingsPath,
List<String> excludePaths = null,
string name = "Unnamed")
{
AnalysisPath = analysisPath;
SettingsPath = settingsPath;
ExcludePaths = excludePaths;
Name = name;
}
}
Loading

0 comments on commit 4662ea8

Please sign in to comment.