diff --git a/src/functions/Coverage.ps1 b/src/functions/Coverage.ps1
index 781b9c331..6f21e9e3d 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 ($paramBlock -and $paramBlock.Attributes) {
+ $parentsAttributeNames += $paramBlock.Attributes.TypeName.FullName
+ }
+ }
+
+ foreach ($parentsAttributeName in $parentsAttributeNames) {
+ if ($parentsAttributeName.EndsWith('ExcludeFromCodeCoverageAttribute')) {
+ $namespaces = Get-NamespacesFromScript -Ast $Ast
+ if ($namespaces) {
+ foreach ($namespace in $namespaces) {
+ if ("$namespace.$parentsAttributeName" -eq 'System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute') {
+ return $true
+ }
+ }
+ }
+ }
+ }
+
+ return $false
+}
+
+function Get-NamespacesFromScript {
+ param (
+ [System.Management.Automation.Language.Ast] $Ast
+ )
+
+ while ($null -ne $Ast.Parent) {
+ $Ast = $Ast.Parent
+ }
+
+ $usingStatements = $Ast.FindAll({
+ param ($node) $node -is [System.Management.Automation.Language.UsingStatementAst] -and $node.UsingStatementKind -eq 'Namespace'
+ }, $true)
+
+ $namespaces = @()
+ 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 {
-
-
-
-
+
+
+
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+