Skip to content

PSUseConsistantWhiteSpace #2089

Closed
Closed

Description

Steps to reproduce

Attaching our PSD fille for the analyzer config

We have

PSUseConsistentWhitespace with IgnoreAssignmentOperatorInsideHashTable = $true
PSAlignAssignmentStatement with Enabled = $true and CheckHashTable = $true

It generally works as advertised... except...

When a using statement is present... we get a "Use space before and after binary and assignment operators.PSScriptAnalyzer(PSUseConsistentWhitespace)" error.

This does not work ...

using namespace DoesNotMatter

$SoSad = @{
    not     = 'other'
    theSame = 'casting sucks maybe?'
}

This works in our setup...

$SoHappy= @{
    not     = 'other'
    theSame = 'casting sucks.'
}

``
![Image](https://github.com/user-attachments/assets/d7e4d67f-8fdb-4ab3-8ede-a496db5c7f6b)

Expected behavior
-----------------
 
No linting error.

Actual behavior
---------------

Seems to check inside hashtables when a using statement of ANY kind is present. The fixer works still though so it doesn't feel like something inherent with the type, maybe something to do with how using are tokenized? 

D:\Symphony\devops-scripts\Symphony\Symphony.Azdo\Happy.ps1
Environment dataD:\Symphony\devops-scripts\Symphony\Symphony.Azdo\Sad.ps1
----------------

Present in a bunch of envs including our build farm...and using the latest version (1.24.0)

Attaching file

[PSScriptAnalyzerSettings.txt](https://github.com/user-attachments/files/19352089/PSScriptAnalyzerSettings.txt)

Activity

  1. AlexandraDorey commented on Mar 20, 2025

    @AlexandraDorey
    ContributorAuthor

    Well that was interesting...->

    I pulled the code to see what was happening cause I was curious, it seems to come down to somewhat weird behavior of the UsingStatementAst vs other kinds of Asts like say ScriptBlockAst which flip the SkipChildren to a Continue inside it's the internalvisit but the internal visit of the UsingStatementAst just passes the SkipChildren through instead of flipping it to a continue when visiting a

    InternalVisit of UsingStatementAst

    vs

    Internal Visit of NamedBlock

    I can't tell if this is a bug in the system automation stuff or some weird expected behavior, if you parse a file with using statements, once the visitor hits the script block, sets that as it's visit and then exits out after hitting the UsingStatementAst (usually the next one in my testing),

    Investigation reproduction:

    Input to pester test passing:

    #using namespace somethin
    
    $ht = @{
        variable = 3
        other    = 4
    }
    
    $alpha = @{
        beta  =
        delta = 4
    }
    '@

    Passing output of ast tree:

    ROOT <- ScriptBlockAst : 0 to 112
    ScriptBlockAst <- NamedBlockAst : 27 to 112
    NamedBlockAst <- AssignmentStatementAst : 27 to 71
    AssignmentStatementAst <- VariableExpressionAst : 27 to 30
    AssignmentStatementAst <- CommandExpressionAst : 33 to 71
    CommandExpressionAst <- HashtableAst : 33 to 71
    HashtableAst <- StringConstantExpressionAst : 40 to 48
    HashtableAst <- PipelineAst : 51 to 52
    PipelineAst <- CommandExpressionAst : 51 to 52
    CommandExpressionAst <- ConstantExpressionAst : 51 to 52
    HashtableAst <- StringConstantExpressionAst : 57 to 62
    HashtableAst <- PipelineAst : 68 to 69
    PipelineAst <- CommandExpressionAst : 68 to 69
    CommandExpressionAst <- ConstantExpressionAst : 68 to 69
    NamedBlockAst <- AssignmentStatementAst : 73 to 112
    AssignmentStatementAst <- VariableExpressionAst : 73 to 79
    AssignmentStatementAst <- CommandExpressionAst : 82 to 112
    CommandExpressionAst <- HashtableAst : 82 to 112
    HashtableAst <- StringConstantExpressionAst : 89 to 93
    HashtableAst <- PipelineAst : 101 to 110
    PipelineAst <- CommandAst : 101 to 110
    CommandAst <- StringConstantExpressionAst : 101 to 106
    CommandAst <- StringConstantExpressionAst : 107 to 108
    CommandAst <- ConstantExpressionAst : 109 to 110
    

    Input script when failing:

    using namespace something
    
    $ht = @{
        variable = 3
        other    = 4
    }
    
    $alpha = @{
        beta  =
        delta = 4
    }
    '@

    Failing output of ast tree:

    ROOT <- ScriptBlockAst : 0 to 112
    ScriptBlockAst <- UsingStatementAst : 0 to 25
    UsingStatementAst <- StringConstantExpressionAst : 16 to 25
    ScriptBlockAst <- NamedBlockAst : 27 to 112
    NamedBlockAst <- AssignmentStatementAst : 27 to 71
    AssignmentStatementAst <- VariableExpressionAst : 27 to 30
    AssignmentStatementAst <- CommandExpressionAst : 33 to 71
    CommandExpressionAst <- HashtableAst : 33 to 71
    HashtableAst <- StringConstantExpressionAst : 40 to 48
    HashtableAst <- PipelineAst : 51 to 52
    PipelineAst <- CommandExpressionAst : 51 to 52
    CommandExpressionAst <- ConstantExpressionAst : 51 to 52
    HashtableAst <- StringConstantExpressionAst : 57 to 62
    HashtableAst <- PipelineAst : 68 to 69
    PipelineAst <- CommandExpressionAst : 68 to 69
    CommandExpressionAst <- ConstantExpressionAst : 68 to 69
    NamedBlockAst <- AssignmentStatementAst : 73 to 112
    AssignmentStatementAst <- VariableExpressionAst : 73 to 79
    AssignmentStatementAst <- CommandExpressionAst : 82 to 112
    CommandExpressionAst <- HashtableAst : 82 to 112
    HashtableAst <- StringConstantExpressionAst : 89 to 93
    HashtableAst <- PipelineAst : 101 to 110
    PipelineAst <- CommandAst : 101 to 110
    CommandAst <- StringConstantExpressionAst : 101 to 106
    CommandAst <- StringConstantExpressionAst : 107 to 108
    CommandAst <- ConstantExpressionAst : 109 to 110
    

    I added some logging inside the Visit function of the FindAstPositionVisitor.cs class

     private AstVisitAction Visit(Ast ast)
            {
                Console.WriteLine($"Visiting {ast.GetType().Name}");
                if (ast.Extent.StartOffset > searchPosition.Offset || ast.Extent.EndOffset <= searchPosition.Offset)
                {
                    Console.WriteLine($"   Skipping Kids...");
                    return AstVisitAction.SkipChildren;
                }
                Console.WriteLine($"   Setting As Position...");
                AstPosition = ast;
                return AstVisitAction.Continue;
            }

    and the output when passing was:

    Visiting ScriptBlockAst
       Setting As Position...
    Visiting NamedBlockAst
       Setting As Position...
    Visiting AssignmentStatementAst
       Skipping Kids...
    Visiting AssignmentStatementAst
       Setting As Position...
    Visiting VariableExpressionAst
       Skipping Kids...
    Visiting CommandExpressionAst
       Setting As Position...
    Visiting HashtableAst
       Setting As Position...
    Visiting StringConstantExpressionAst
       Skipping Kids...
    Visiting PipelineAst
       Skipping Kids...
    Done visiting...
    

    and when failing it was:

    Visiting ScriptBlockAst
       Setting As Position...
    Visiting UsingStatementAst
       Skipping Kids...
    Done visiting...
    

    as a result the type of the AST returned is a ScriptBlockAst instead of a HashTableAst here the UseConsistantWhiteSpace.cs and an error is reported instead of being skipped...

    Quick Fix:

    I don't believe PSScriptAnalyzer does anything related using statements currently? if that's accurate then simply returning AstVisitAction.Continue instead of passing the UsingStatementAst down to your visit function function fixed it locally for me.

    Otherwise some specialized logic inside your Visit function will likely be required. Of course it could also be it's a bug in the system automation but I feel like that'll be longer fix unless y'all are on the same team :)

  2. bergmeister commented on Apr 1, 2025

    @bergmeister
    Collaborator

    Interesting. Feel free to try submit a potential fix, we have a suite of tests that will tell if anything else breaks so it's worth a try. PSSA just gets the AST from PowerShell's parser and works off that with the code that you've seen. You can use the ShowPsAst module to look at the AST whether the using statement makes a difference to what gets passed to PSSA, if that is not different then it should be fixable within PSSA

  3. AlexandraDorey commented on Apr 1, 2025

    @AlexandraDorey
    ContributorAuthor

    Created a Pull request if you could have it run the tests when you get a chance, they seem to be passing locally.
    #2091

  4. bergmeister commented on Apr 4, 2025

    @bergmeister
    Collaborator

    Fixed by #2091

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions