Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions Engine/TokenOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,5 +245,165 @@ public Ast GetAstPosition(Token token)
return findAstVisitor.AstPosition;
}

/// <summary>
/// Returns a list of non-overlapping ranges (startOffset,endOffset) representing the start
/// and end of braced member access expressions. These are member accesses where the name is
/// enclosed in braces. The contents of such braces are treated literally as a member name.
/// Altering the contents of these braces by formatting is likely to break code.
/// </summary>
public List<Tuple<int, int>> GetBracedMemberAccessRanges()
{
// A list of (startOffset, endOffset) pairs representing the start
// and end braces of braced member access expressions.
var ranges = new List<Tuple<int, int>>();

var node = tokensLL.Value.First;
while (node != null)
{
switch (node.Value.Kind)
{
#if CORECLR
// TokenKind added in PS7
case TokenKind.QuestionDot:
#endif
case TokenKind.Dot:
break;
default:
node = node.Next;
continue;
}

// Note: We don't check if the dot is part of an existing range. When we find
// a valid range, we skip all tokens inside it - so we won't ever evaluate a token
// which already part of a previously found range.

// Backward scan:
// Determine if this 'dot' is part of a member access.
// Walk left over contiguous comment tokens that are 'touching'.
// After skipping comments, the preceding non-comment token must also be 'touching'
// and one of the expected TokenKinds.
var leftToken = node.Previous;
var rightToken = node;
while (leftToken != null && leftToken.Value.Kind == TokenKind.Comment)
{
if (leftToken.Value.Extent.EndOffset != rightToken.Value.Extent.StartOffset)
{
leftToken = null;
break;
}
rightToken = leftToken;
leftToken = leftToken.Previous;
}
if (leftToken == null)
{
// We ran out of tokens before finding a non-comment token to the left or there
// was intervening whitespace.
node = node.Next;
continue;
}

if (leftToken.Value.Extent.EndOffset != rightToken.Value.Extent.StartOffset)
{
// There's whitespace between the two tokens
node = node.Next;
continue;
}

// Limit to valid token kinds that can precede a 'dot' in a member access.
switch (leftToken.Value.Kind)
{
// Note: TokenKind.Number isn't in the list as 5.{Prop} is a syntax error
// (Unexpected token). Numbers also have no properties - only methods.
case TokenKind.Variable:
case TokenKind.Identifier:
case TokenKind.StringLiteral:
case TokenKind.StringExpandable:
case TokenKind.HereStringLiteral:
case TokenKind.HereStringExpandable:
case TokenKind.RParen:
case TokenKind.RCurly:
case TokenKind.RBracket:
// allowed
break;
default:
// not allowed
node = node.Next;
continue;
}

// Forward Scan:
// Check that the next significant token is an LCurly
// Starting from the token after the 'dot', walk right skipping trivia tokens:
// - Comment
// - NewLine
// - LineContinuation (`)
// These may be multi-line and need not be 'touching' the dot.
// The first non-trivia token encountered must be an opening curly brace (LCurly) for
// this dot to begin a braced member access. If it is not LCurly or we run out
// of tokens, this dot is ignored.
var scan = node.Next;
while (scan != null)
{
if (
scan.Value.Kind == TokenKind.Comment ||
scan.Value.Kind == TokenKind.NewLine ||
scan.Value.Kind == TokenKind.LineContinuation
)
{
scan = scan.Next;
continue;
}
break;
}

// If we reached the end without finding a significant token, or if the found token
// is not LCurly, continue.
if (scan == null || scan.Value.Kind != TokenKind.LCurly)
{
node = node.Next;
continue;
}

// We have a valid token, followed by a dot, followed by an LCurly.
// Find the matching RCurly and create the range.
var lCurlyNode = scan;

// Depth count braces to find the RCurly which closes the LCurly.
int depth = 0;
LinkedListNode<Token> rcurlyNode = null;
while (scan != null)
{
if (scan.Value.Kind == TokenKind.LCurly) depth++;
else if (scan.Value.Kind == TokenKind.RCurly)
{
depth--;
if (depth == 0)
{
rcurlyNode = scan;
break;
}
}
scan = scan.Next;
}

// If we didn't find a matching RCurly, something has gone wrong.
// Should an unmatched pair be caught by the parser as a parse error?
if (rcurlyNode == null)
{
node = node.Next;
continue;
}

ranges.Add(new Tuple<int, int>(
lCurlyNode.Value.Extent.StartOffset,
rcurlyNode.Value.Extent.EndOffset
));

// Skip all tokens inside the excluded range.
node = rcurlyNode.Next;
}

return ranges;
}
}
}
11 changes: 11 additions & 0 deletions Rules/UseConsistentWhitespace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -257,13 +257,20 @@ private IEnumerable<DiagnosticRecord> FindOpenBraceViolations(TokenOperations to

private IEnumerable<DiagnosticRecord> FindInnerBraceViolations(TokenOperations tokenOperations)
{
// Ranges which represent braced member access. Tokens within these ranges should be
// excluded from formatting.
var exclusionRanges = tokenOperations.GetBracedMemberAccessRanges();
foreach (var lCurly in tokenOperations.GetTokenNodes(TokenKind.LCurly))
{
if (lCurly.Next == null
|| !(lCurly.Previous == null || IsPreviousTokenOnSameLine(lCurly))
|| lCurly.Next.Value.Kind == TokenKind.NewLine
|| lCurly.Next.Value.Kind == TokenKind.LineContinuation
|| lCurly.Next.Value.Kind == TokenKind.RCurly
|| exclusionRanges.Any(range =>
lCurly.Value.Extent.StartOffset >= range.Item1 &&
lCurly.Value.Extent.EndOffset <= range.Item2
)
)
{
continue;
Expand All @@ -290,6 +297,10 @@ private IEnumerable<DiagnosticRecord> FindInnerBraceViolations(TokenOperations t
|| rCurly.Previous.Value.Kind == TokenKind.NewLine
|| rCurly.Previous.Value.Kind == TokenKind.LineContinuation
|| rCurly.Previous.Value.Kind == TokenKind.AtCurly
|| exclusionRanges.Any(range =>
rCurly.Value.Extent.StartOffset >= range.Item1 &&
rCurly.Value.Extent.EndOffset <= range.Item2
)
)
{
continue;
Expand Down
177 changes: 177 additions & 0 deletions Tests/Engine/TokenOperations.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,181 @@ $h = @{
$hashTableAst | Should -BeOfType [System.Management.Automation.Language.HashTableAst]
$hashTableAst.Extent.Text | Should -Be '@{ z = "hi" }'
}

Context 'Braced Member Access Ranges' {

BeforeDiscovery {
$RangeTests = @(
@{
Name = 'No braced member access'
ScriptDef = '$object.Prop'
ExpectedRanges = @()
}
@{
Name = 'No braced member access on braced variable name'
ScriptDef = '${object}.Prop'
ExpectedRanges = @()
}
@{
Name = 'Braced member access'
ScriptDef = '$object.{Prop}'
ExpectedRanges = @(
,@(8, 14)
)
}
@{
Name = 'Braced member access with spaces'
ScriptDef = '$object. { Prop }'
ExpectedRanges = @(
,@(9, 17)
)
}
@{
Name = 'Braced member access with newline'
ScriptDef = "`$object.`n{ Prop }"
ExpectedRanges = @(
,@(9, 17)
)
}
@{
Name = 'Braced member access with comment'
ScriptDef = "`$object. <#comment#>{Prop}"
ExpectedRanges = @(
,@(20, 26)
)
}
@{
Name = 'Braced member access with multi-line comment'
ScriptDef = "`$object. <#`ncomment`n#>{Prop}"
ExpectedRanges = @(
,@(22, 28)
)
}
@{
Name = 'Braced member access with inline comment'
ScriptDef = "`$object. #comment`n{Prop}"
ExpectedRanges = @(
,@(18, 24)
)
}
@{
Name = 'Braced member access with inner curly braces'
ScriptDef = "`$object.{{Prop}}"
ExpectedRanges = @(
,@(8, 16)
)
}
@{
Name = 'Indexed Braced member access'
ScriptDef = "`$object[0].{Prop}"
ExpectedRanges = @(
,@(11, 17)
)
}
@{
Name = 'Parenthesized Braced member access'
ScriptDef = "(`$object).{Prop}"
ExpectedRanges = @(
,@(10, 16)
)
}
@{
Name = 'Chained Braced member access'
ScriptDef = "`$object.{Prop}.{InnerProp}"
ExpectedRanges = @(
,@(8, 14)
,@(15, 26)
)
}
@{
Name = 'Multiple Braced member access in larger script'
ScriptDef = @'
$var = 1
$a.prop.{{inner}}
$a.{
$a.{Prop}
}
'@
ExpectedRanges = @(
,@(17, 26)
,@(30, 47)
)
}
)
}

It 'Should correctly identify range for <Name>' -ForEach $RangeTests {
$tokens = $null
$parseErrors = $null
$scriptAst = [System.Management.Automation.Language.Parser]::ParseInput($ScriptDef, [ref] $tokens, [ref] $parseErrors)
$tokenOperations = [Microsoft.Windows.PowerShell.ScriptAnalyzer.TokenOperations]::new($tokens, $scriptAst)
$ranges = $tokenOperations.GetBracedMemberAccessRanges()
$ranges.Count | Should -Be $ExpectedRanges.Count
for ($i = 0; $i -lt $ranges.Count; $i++) {
$ranges[$i].Item1 | Should -Be $ExpectedRanges[$i][0]
$ranges[$i].Item2 | Should -Be $ExpectedRanges[$i][1]
}
}

It 'Should not identify dot-sourcing as braced member access' {
$scriptText = @'
. {5+5}
$a=4;. {10+15}
'@
$tokens = $null
$parseErrors = $null
$scriptAst = [System.Management.Automation.Language.Parser]::ParseInput($scriptText, [ref] $tokens, [ref] $parseErrors)
$tokenOperations = [Microsoft.Windows.PowerShell.ScriptAnalyzer.TokenOperations]::new($tokens, $scriptAst)
$ranges = $tokenOperations.GetBracedMemberAccessRanges()
$ranges.Count | Should -Be 0
}

It 'Should not return a range for an incomplete bracket pair (parse error)' {
$scriptText = @'
$object.{MemberName
'@
$tokens = $null
$parseErrors = $null
$scriptAst = [System.Management.Automation.Language.Parser]::ParseInput($scriptText, [ref] $tokens, [ref] $parseErrors)
$tokenOperations = [Microsoft.Windows.PowerShell.ScriptAnalyzer.TokenOperations]::new($tokens, $scriptAst)
$ranges = $tokenOperations.GetBracedMemberAccessRanges()
$ranges.Count | Should -Be 0
}

It 'Should find the correct range for null-conditional braced member access' {
$scriptText = '$object?.{Prop}'
$tokens = $null
$parseErrors = $null
$scriptAst = [System.Management.Automation.Language.Parser]::ParseInput($scriptText, [ref] $tokens, [ref] $parseErrors)
$tokenOperations = [Microsoft.Windows.PowerShell.ScriptAnalyzer.TokenOperations]::new($tokens, $scriptAst)
$ranges = $tokenOperations.GetBracedMemberAccessRanges()
$ranges.Count | Should -Be 1
$ExpectedRanges = @(
,@(9, 15)
)
for ($i = 0; $i -lt $ranges.Count; $i++) {
$ranges[$i].Item1 | Should -Be $ExpectedRanges[$i][0]
$ranges[$i].Item2 | Should -Be $ExpectedRanges[$i][1]
}
} -Skip:$($PSVersionTable.PSVersion.Major -lt 7)

It 'Should find the correct range for nested null-conditional braced member access' {
$scriptText = '$object?.{Prop?.{InnerProp}}'
$tokens = $null
$parseErrors = $null
$scriptAst = [System.Management.Automation.Language.Parser]::ParseInput($scriptText, [ref] $tokens, [ref] $parseErrors)
$tokenOperations = [Microsoft.Windows.PowerShell.ScriptAnalyzer.TokenOperations]::new($tokens, $scriptAst)
$ranges = $tokenOperations.GetBracedMemberAccessRanges()
$ranges.Count | Should -Be 1
$ExpectedRanges = @(
,@(9, 28)
)
for ($i = 0; $i -lt $ranges.Count; $i++) {
$ranges[$i].Item1 | Should -Be $ExpectedRanges[$i][0]
$ranges[$i].Item2 | Should -Be $ExpectedRanges[$i][1]
}
} -Skip:$($PSVersionTable.PSVersion.Major -lt 7)

}

}
Loading