diff --git a/src/functions/Coverage.ps1 b/src/functions/Coverage.ps1 index 781b9c331..3a4617fe0 100644 --- a/src/functions/Coverage.ps1 +++ b/src/functions/Coverage.ps1 @@ -286,7 +286,7 @@ 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. @@ -297,17 +297,101 @@ function Get-CommandsInFile { $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] + $args[0] -is [System.Management.Automation.Language.ThrowStatementAst] -and + -not (IsExcludedByAttribute -Ast $args[0] -TargetAttribute 'System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute') } } else { - $predicate = { $args[0] -is [System.Management.Automation.Language.CommandBaseAst] } + $predicate = { + $args[0] -is [System.Management.Automation.Language.CommandBaseAst] -and + -not (IsExcludedByAttribute -Ast $args[0] -TargetAttribute 'System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute') + } } $searchNestedScriptBlocks = $true $ast.FindAll($predicate, $searchNestedScriptBlocks) } +function IsExcludedByAttribute { + param ( + [System.Management.Automation.Language.Ast] $Ast, + [string] $TargetAttribute + ) + + for ($parent = $Ast.Parent; $null -ne $parent; $parent = $parent.Parent) { + if ($parent -is [System.Management.Automation.Language.FunctionDefinitionAst]) { + if (Test-ContainsAttribute -FunctionAst $parent -TargetAttribute $TargetAttribute) { + return $true + } + } + } + + return $false +} + +function Test-ContainsAttribute { + param ( + [System.Management.Automation.Language.FunctionDefinitionAst] $FunctionAst, + [string] $TargetAttribute + ) + + $AttributeNames = Get-AttributeNames -FunctionAst $FunctionAst + + foreach ($attributeName in $AttributeNames) { + if ($attributeName -eq $TargetAttribute) { + return $true + } + + if ($attributeName.Split('.')[-1] -eq $TargetAttribute.Split('.')[-1]) { + $Namespaces = Get-NamespacesFromAstTopParent -Ast $FunctionAst + foreach ($namespace in $Namespaces) { + $fullyQualifiedName = "$namespace.$attributeName" + if ($fullyQualifiedName -eq $TargetAttribute) { + return $true + } + } + } + } + + return $false +} + +function Get-AttributeNames { + param ( + [System.Management.Automation.Language.FunctionDefinitionAst] $FunctionAst + ) + + $paramBlock = $FunctionAst.Body.ParamBlock + if ($null -ne $paramBlock -and $paramBlock.Attributes) { + return $paramBlock.Attributes.TypeName.FullName + } + + return @() +} + +function Get-NamespacesFromAstTopParent { + param ( + [System.Management.Automation.Language.Ast] $Ast + ) + + $namespaces = @() + $topParent = Get-AstTopParent -Ast $Ast + + if ($null -eq $topParent) { + return @() + } + + $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 { - - - - + + + + - + - + - + - + - + - - + + - - - + + - - - - - - - - - - + + + + + + + + + + +