Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for excluding functions from coverage with attribute #2593

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

benjaminfuchs
Copy link

@benjaminfuchs benjaminfuchs commented Dec 21, 2024

PR Summary

Fixes #2268

This pull request introduces support for excluding functions from code coverage analysis using the [ExcludeFromCodeCoverageAttribute()] attribute.

Changes:

  • Updated the Get-CommandsInFile function to skip AST nodes within functions marked with [ExcludeFromCodeCoverageAttribute()].
  • Added IsExcludedByAttribute to check if a function has the exclusion attribute applied, either fully qualified or resolved through using namespace.
  • Implemented Get-NamespacesFromScript to identify and apply using namespace declarations for resolving unqualified attribute names (not sure if similar logic already exists in the codebase).
  • Updated tests to verify exclusion functionality.

PR Checklist

  • PR has meaningful title
  • Summary describes changes
  • PR is ready to be merged
    • If not, use the arrow next to Create Pull Request to mark it as a draft. PR can be marked Ready for review when it's ready.
  • Tests are added/update (if required)
  • Documentation is updated/added (if required)

@benjaminfuchs
Copy link
Author

If the approach is valid, I'll refine the tests and add proper documentation. Feedback is welcome!

@benjaminfuchs benjaminfuchs force-pushed the feature/exclude-attribute-coverage branch 3 times, most recently from 99eb66a to 1c9dd96 Compare December 21, 2024 22:37
@benjaminfuchs benjaminfuchs force-pushed the feature/exclude-attribute-coverage branch from 1c9dd96 to d1c84df Compare December 21, 2024 22:58
@benjaminfuchs benjaminfuchs force-pushed the feature/exclude-attribute-coverage branch from 53c13ac to 8608926 Compare December 29, 2024 12:48
Copy link
Collaborator

@fflaten fflaten left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for looking into this! It works as intended, though I've suggested some improvements.

While reviewing I realized we could make this more efficient, see comment about FindAll. So I'd like to discuss an alternative approach. See draft PR #2598 which should produce the same results.

)

for ($parent = $Ast.Parent; $null -ne $parent; $parent = $parent.Parent) {
if ($parent -is [System.Management.Automation.Language.FunctionDefinitionAst]) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please replace FunctionDefinitionAst with ScriptBlockAst in the logic so we can exclude whole scripts and other scriptblocks. Besides type change, you'll probably only need to remove .Body. in Get-AttributeNames.

# Script: ScriptBlockAst -> ParamBlockAst -> Attributes
$ast = {
    [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute()]
    param($a, $b)
    $a + $b
}.Ast

# Function: FunctionDefinitionAst -> ScriptBlockAst (Body) -> ParamBlockAst -> Attributes
$ast = {
    function t {
        [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute()]
        param($a, $b)
        $a + $b
    }
}.Ast


# Adv function: FunctionDefinitionAst -> ScriptBlockAst (Body) -> ParamBlockAst -> Attributes
$ast = {
    function t {
        [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute()]
        param($a, $b)
        
        begin { $a + $b }
        process {}
        end {}
    }
}.Ast

# Random scriptblock: ScriptBlockAst -> ParamBlockAst -> Attributes
$ast = {
    1..10 | ForEach-Object {
        [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute()]
        param($a, $b)
        $a + $b
    }
}.Ast

@@ -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')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FindAll will trigger this for commands in nested scriptblocks even though a parent is excluded. Not ideal to reverse (check parents) on every step.

$ast = {
    1 | ForEach-Object {
        [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute()]
        param($a, $b)
        $a + $b
        2 | ForEach-Object {
            3 | ForEach-Object {
                continue
             }
        }
    }
}.Ast
$ast.FindAll($predicate, $searchNestedScriptBlocks)

# Processing .. output added in IsExcludedByAttribute

Processing 1
Processing ForEach-Object {
        [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute()]
        param($a, $b)
        $a + $b
        2 | ForEach-Object {
            3 | ForEach-Object {
                continue
             }
        }
    }
Processing $a + $b
Processing 2
Processing ForEach-Object {
            3 | ForEach-Object {
                continue
             }
        }
Processing 3
Processing ForEach-Object {
                continue
             }
Processing continue

Expression Redirections Extent
---------- ------------ ------                                                                                                                                      
1          {}           1                                                                                                                                           
           {}           ForEach-Object {… 

We could avoid this by rewriting to use an AstVisitor, stopping early at a ScriptBlockAst where the attribute is set.

Comment on lines +368 to +369

return @()
Copy link
Collaborator

@fflaten fflaten Jan 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return @()

nit: unnecessary?

$topParent = Get-AstTopParent -Ast $Ast

if ($null -eq $topParent) {
return @()
Copy link
Collaborator

@fflaten fflaten Jan 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return @()
return

nit: unnecessary?

Comment on lines +388 to +390
foreach ($usingStatement in $usingStatements) {
$namespaces += $usingStatement.Name.Value
}
Copy link
Collaborator

@fflaten fflaten Jan 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foreach ($usingStatement in $usingStatements) {
$namespaces += $usingStatement.Name.Value
}
$namespaces = foreach ($usingStatement in $usingStatements) {
$usingStatement.Name.Value
}

more efficient to output directly or capture foreach {}

[System.Management.Automation.Language.Ast] $Ast
)

$namespaces = @()
Copy link
Collaborator

@fflaten fflaten Jan 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$namespaces = @()

more efficient to output directly or capture foreach {}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow [ExcludeFromCodeCoverage()] on the param() block to exclude a function from code coverage
2 participants