From 1c9dd96c5dee8bd9e42a517f945cd68ec9c85748 Mon Sep 17 00:00:00 2001 From: Benjamin Fuchs Date: Sat, 21 Dec 2024 16:12:17 +0100 Subject: [PATCH] Add support for excluding functions with [ExcludeFromCodeCoverageAttribute()] attribute --- src/functions/Coverage.ps1 | 83 ++++++++++++++++-- tst/functions/Coverage.Tests.ps1 | 144 +++++++++++++++++-------------- 2 files changed, 152 insertions(+), 75 deletions(-) diff --git a/src/functions/Coverage.ps1 b/src/functions/Coverage.ps1 index 781b9c331..b0465d430 100644 --- a/src/functions/Coverage.ps1 +++ b/src/functions/Coverage.ps1 @@ -286,28 +286,95 @@ function Get-CommandsInFile { if ($PSVersionTable.PSVersion.Major -ge 5) { # In PowerShell 5.0, dynamic keywords for DSC configurations are represented by the DynamicKeywordStatementAst - # class. They still trigger breakpoints, but are not a child class of CommandBaseAst anymore. + # class. They still trigger breakpoints, but are not a child class of CommandBaseAst anymore. # ReturnStatementAst is excluded as it's not behaving consistent. # "return" is not hit in 5.1 but fixed in a later version. Using "return 123" we get hit on 123 but not return. # See https://github.com/pester/Pester/issues/1465#issuecomment-604323645 $predicate = { - $args[0] -is [System.Management.Automation.Language.DynamicKeywordStatementAst] -or - $args[0] -is [System.Management.Automation.Language.CommandBaseAst] -or - $args[0] -is [System.Management.Automation.Language.BreakStatementAst] -or - $args[0] -is [System.Management.Automation.Language.ContinueStatementAst] -or - $args[0] -is [System.Management.Automation.Language.ExitStatementAst] -or - $args[0] -is [System.Management.Automation.Language.ThrowStatementAst] + if ($args[0] -is [System.Management.Automation.Language.DynamicKeywordStatementAst] -or + $args[0] -is [System.Management.Automation.Language.CommandBaseAst] -or + $args[0] -is [System.Management.Automation.Language.BreakStatementAst] -or + $args[0] -is [System.Management.Automation.Language.ContinueStatementAst] -or + $args[0] -is [System.Management.Automation.Language.ExitStatementAst] -or + $args[0] -is [System.Management.Automation.Language.ThrowStatementAst]) { + if (-not (IsExcludedByAttribute -Ast $args[0])) { + return $true + } + } } } else { - $predicate = { $args[0] -is [System.Management.Automation.Language.CommandBaseAst] } + $predicate = { + if ($args[0] -is [System.Management.Automation.Language.CommandBaseAst]) { + if (-not (IsExcludedByAttribute -Ast $args[0])) { + return $true + } + } + } } $searchNestedScriptBlocks = $true $ast.FindAll($predicate, $searchNestedScriptBlocks) } +function IsExcludedByAttribute { + param ( + [System.Management.Automation.Language.Ast] $Ast + ) + + $functionParents = @() + for ($parent = $Ast.Parent; $null -ne $parent; $parent = $parent.Parent) { + if ($parent -is [System.Management.Automation.Language.FunctionDefinitionAst]) { + $functionParents += $parent + } + } + + $parentsAttributeNames = @() + foreach ($functionParent in $functionParents) { + $paramBlock = $functionParent.Body.ParamBlock + if ($null -ne $paramBlock -and $paramBlock.Attributes) { + $parentsAttributeNames += $paramBlock.Attributes.TypeName.FullName + } + } + + foreach ($parentsAttributeName in $parentsAttributeNames) { + if ($parentsAttributeName.EndsWith('ExcludeFromCodeCoverageAttribute')) { + $namespaces = Get-NamespacesFromAstTopParent -Ast $Ast + if ($namespaces) { + foreach ($namespace in $namespaces) { + if ("$namespace.$parentsAttributeName" -eq 'System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute') { + return $true + } + } + } + } + } + + return $false +} + +function Get-NamespacesFromAstTopParent { + param ( + [System.Management.Automation.Language.Ast] $Ast + ) + + $namespaces = @() + $topParent = Get-AstTopParent -Ast $Ast + + if ($null -ne $topParent) { + $usingStatements = $topParent.FindAll({ + param ($node) $node -is [System.Management.Automation.Language.UsingStatementAst] -and $node.UsingStatementKind -eq 'Namespace' + }, $true) + + foreach ($usingStatement in $usingStatements) { + $namespaces += $usingStatement.Name.Value + } + } + + return $namespaces +} + function Test-CoverageOverlapsCommand { param ([object] $CoverageInfo, [System.Management.Automation.Language.Ast] $Command) diff --git a/tst/functions/Coverage.Tests.ps1 b/tst/functions/Coverage.Tests.ps1 index b108745fb..d801f4463 100644 --- a/tst/functions/Coverage.Tests.ps1 +++ b/tst/functions/Coverage.Tests.ps1 @@ -23,6 +23,8 @@ InPesterModuleScope { $null = New-Item -Path $testScriptPath -ItemType File -ErrorAction SilentlyContinue Set-Content -Path $testScriptPath -Value @' + using namespace System.Diagnostics.CodeAnalysis + function FunctionOne { function NestedFunction @@ -47,6 +49,14 @@ InPesterModuleScope { 'I am function two. I never get called.' } + function FunctionThree + { + [ExcludeFromCodeCoverageAttribute(Justification = 'I am not covered')] + param () + + 'I am function three. I never get called.' + } + FunctionOne '@ @@ -288,42 +298,42 @@ InPesterModuleScope { - + - + - + - + - + - + - + - + @@ -367,21 +377,21 @@ InPesterModuleScope { - - - + + - - - - - - - - - - + + + + + + + + + + + @@ -480,42 +490,42 @@ InPesterModuleScope { - + - + - + - + - + - + - + - + @@ -559,21 +569,21 @@ InPesterModuleScope { - - - + + - - - - - - - - - - + + + + + + + + + + + @@ -675,61 +685,61 @@ InPesterModuleScope { - - - - + + + + - + - + - + - + - + - - + + - - - + + - - - - - - - - - - + + + + + + + + + + +