Skip to content

Commit 9ef1119

Browse files
author
Kapil Borle
authored
Merge pull request #706 from PowerShell/kapilmb/brace-rule-exceptions
Add ability to ignore one line constructs in brace rules
2 parents 5d8a96b + 7b7d7dd commit 9ef1119

File tree

7 files changed

+213
-18
lines changed

7 files changed

+213
-18
lines changed

Engine/TokenOperations.cs

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ public IEnumerable<Token> GetOpenBracesInCommandElements()
6161
return GetBraceInCommandElement(TokenKind.LCurly);
6262
}
6363

64-
6564
/// <summary>
6665
/// Return tokens of kind RCurly that end a scriptblock expression in an command element.
6766
///
@@ -74,6 +73,44 @@ public IEnumerable<Token> GetCloseBracesInCommandElements()
7473
return GetBraceInCommandElement(TokenKind.RCurly);
7574
}
7675

76+
/// <summary>
77+
/// Returns pairs of associatd braces.
78+
/// </summary>
79+
/// <returns>Tuples of tokens such that item1 is LCurly token and item2 is RCurly token.</returns>
80+
public IEnumerable<Tuple<Token, Token>> GetBracePairs()
81+
{
82+
var openBraceStack = new Stack<Token>();
83+
foreach (var token in tokens)
84+
{
85+
if (token.Kind == TokenKind.LCurly)
86+
{
87+
openBraceStack.Push(token);
88+
continue;
89+
}
90+
91+
if (token.Kind == TokenKind.RCurly
92+
&& openBraceStack.Count > 0)
93+
{
94+
yield return new Tuple<Token, Token>(openBraceStack.Pop(), token);
95+
}
96+
}
97+
}
98+
99+
/// <summary>
100+
/// Returns brace pairs that are on the same line.
101+
/// </summary>
102+
/// <returns>Tuples of tokens such that item1 is LCurly token and item2 is RCurly token.</returns>
103+
public IEnumerable<Tuple<Token, Token>> GetBracePairsOnSameLine()
104+
{
105+
foreach (var bracePair in GetBracePairs())
106+
{
107+
if (bracePair.Item1.Extent.StartLineNumber == bracePair.Item2.Extent.StartLineNumber)
108+
{
109+
yield return bracePair;
110+
}
111+
}
112+
}
113+
77114
private IEnumerable<Token> GetBraceInCommandElement(TokenKind tokenKind)
78115
{
79116
var cmdElemAsts = ast.FindAll(x => x is CommandElementAst && x is ScriptBlockExpressionAst, true);
@@ -110,6 +147,86 @@ private IEnumerable<Token> GetBraceInCommandElement(TokenKind tokenKind)
110147
}
111148
}
112149

150+
public IEnumerable<Token> GetCloseBraceInOneLineIfStatement()
151+
{
152+
return GetBraceInOneLineIfStatment(TokenKind.RCurly);
153+
}
154+
155+
public IEnumerable<Token> GetOpenBraceInOneLineIfStatement()
156+
{
157+
return GetBraceInOneLineIfStatment(TokenKind.LCurly);
158+
}
159+
160+
// TODO Fix code duplication in the following method and GetBraceInCommandElement
161+
private IEnumerable<Token> GetBraceInOneLineIfStatment(TokenKind tokenKind)
162+
{
163+
var ifStatementAsts = ast.FindAll(ast =>
164+
{
165+
var ifAst = ast as IfStatementAst;
166+
if (ifAst == null)
167+
{
168+
return false;
169+
}
170+
171+
return ifAst.Extent.StartLineNumber == ifAst.Extent.EndLineNumber;
172+
},
173+
true);
174+
175+
if (ifStatementAsts == null)
176+
{
177+
yield break;
178+
}
179+
180+
var braceTokens = new List<Token>();
181+
foreach (var ast in ifStatementAsts)
182+
{
183+
var ifStatementAst = ast as IfStatementAst;
184+
foreach (var clause in ifStatementAst.Clauses)
185+
{
186+
var tokenIf
187+
= tokenKind == TokenKind.LCurly
188+
? GetTokens(clause.Item2).FirstOrDefault()
189+
: GetTokens(clause.Item2).LastOrDefault();
190+
if (tokenIf != null)
191+
{
192+
yield return tokenIf;
193+
}
194+
}
195+
196+
if (ifStatementAst.ElseClause == null)
197+
{
198+
continue;
199+
}
200+
201+
var tokenElse
202+
= tokenKind == TokenKind.LCurly
203+
? GetTokens(ifStatementAst.ElseClause).FirstOrDefault()
204+
: GetTokens(ifStatementAst.ElseClause).LastOrDefault();
205+
if (tokenElse != null)
206+
{
207+
yield return tokenElse;
208+
}
209+
}
210+
}
211+
212+
private IEnumerable<Token> GetTokens(Ast ast)
213+
{
214+
int k = 0;
215+
while (k < tokens.Length && tokens[k].Extent.EndOffset <= ast.Extent.StartOffset)
216+
{
217+
k++;
218+
}
219+
220+
while (k < tokens.Length && tokens[k].Extent.EndOffset <= ast.Extent.EndOffset)
221+
{
222+
var token = tokens[k++];
223+
if (token.Extent.StartOffset >= ast.Extent.StartOffset)
224+
{
225+
yield return token;
226+
}
227+
}
228+
}
229+
113230
public IEnumerable<LinkedListNode<Token>> GetTokenNodes(TokenKind kind)
114231
{
115232
return GetTokenNodes((token) => token.Kind == kind);

RuleDocumentation/PlaceCloseBrace.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Close brace placement should follow a consistent style. It should be on a new li
1212
PSPlaceCloseBrace = @{
1313
Enable = $true
1414
NoEmptyLineBefore = $false
15+
IgnoreOneLineBlock = $true
1516
}
1617
}
1718
```
@@ -23,3 +24,8 @@ Enable or disable the rule during ScriptAnalyzer invocation.
2324

2425
#### NoEmptyLineBefore: bool (Default value is `$false`)
2526
Create violation if there is an empty line before a close brace.
27+
28+
#### IgnoreOneLineBlock: bool (Default value is `$true`)
29+
Indicates if close braces in a one line block should be ignored or not.
30+
E.g. $x = if ($true) { "blah" } else { "blah blah" }
31+
In the above example, if the property is set to true then the rule will not fire a violation.

RuleDocumentation/PlaceOpenBrace.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Open brace placement should follow a consistent style. It can either follow KR s
1313
Enable = $true
1414
OnSameLine = $true
1515
NewLineAfter = $true
16+
IgnoreOneLineBlock = $true
1617
}
1718
}
1819
```
@@ -27,3 +28,8 @@ Provide an option to enforce the open brace to be on the same line as preceding
2728

2829
#### NewLineAfter: bool (Default value is `$true`)
2930
Enforce a new line character after an open brace. The default value is true.
31+
32+
#### IgnoreOneLineBlock: bool (Default value is `$true`)
33+
Indicates if open braces in a one line block should be ignored or not.
34+
E.g. $x = if ($true) { "blah" } else { "blah blah" }
35+
In the above example, if the property is set to true then the rule will not fire a violation.

Rules/PlaceCloseBrace.cs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ public class PlaceCloseBrace : ConfigurableRule
3636
[ConfigurableRuleProperty(defaultValue:false)]
3737
public bool NoEmptyLineBefore { get; protected set; }
3838

39+
/// <summary>
40+
/// Indicates if close braces in a one line block should be ignored or not.
41+
/// E.g. $x = if ($true) { "blah" } else { "blah blah" }
42+
/// In the above example, if the property is set to true then the rule will
43+
/// not fire a violation.
44+
///
45+
/// Default value if true.
46+
/// </summary>
47+
[ConfigurableRuleProperty(defaultValue: true)]
48+
public bool IgnoreOneLineBlock { get; protected set; }
49+
3950
private HashSet<Token> tokensToIgnore;
4051

4152
/// <summary>
@@ -58,14 +69,24 @@ public override IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string file
5869

5970
// TODO Should have the following options
6071
// * no-empty-lines-before
61-
// * align (if close brance and open brace on new lines align with open brace,
62-
// if close brace is on new line but open brace is not align with the first keyword on open brace line)
6372

6473
var tokens = Helper.Instance.Tokens;
6574
var diagnosticRecords = new List<DiagnosticRecord>();
6675
var curlyStack = new Stack<Tuple<Token, int>> ();
67-
tokensToIgnore = new HashSet<Token> (
68-
new TokenOperations(tokens, ast).GetCloseBracesInCommandElements());
76+
77+
// TODO move part common with PlaceOpenBrace to one place
78+
var tokenOps = new TokenOperations(tokens, ast);
79+
tokensToIgnore = new HashSet<Token> (tokenOps.GetCloseBracesInCommandElements());
80+
81+
// Ignore close braces that are part of a one line if-else statement
82+
// E.g. $x = if ($true) { "blah" } else { "blah blah" }
83+
if (IgnoreOneLineBlock)
84+
{
85+
foreach (var pair in tokenOps.GetBracePairsOnSameLine())
86+
{
87+
tokensToIgnore.Add(pair.Item2);
88+
}
89+
}
6990

7091
for (int k = 0; k < tokens.Length; k++)
7192
{

Rules/PlaceOpenBrace.cs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,17 @@ public class PlaceOpenBrace : ConfigurableRule
4545
[ConfigurableRuleProperty(defaultValue: true)]
4646
public bool NewLineAfter { get; protected set; }
4747

48+
/// <summary>
49+
/// Indicates if open braces in a one line block should be ignored or not.
50+
/// E.g. $x = if ($true) { "blah" } else { "blah blah" }
51+
/// In the above example, if the property is set to true then the rule will
52+
/// not fire a violation.
53+
///
54+
/// Default value if true.
55+
/// </summary>
56+
[ConfigurableRuleProperty(defaultValue: true)]
57+
public bool IgnoreOneLineBlock { get; protected set; }
58+
4859
private List<Func<Token[], Ast, string, IEnumerable<DiagnosticRecord>>> violationFinders
4960
= new List<Func<Token[], Ast, string, IEnumerable<DiagnosticRecord>>>();
5061

@@ -100,8 +111,19 @@ public override IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string file
100111
// In the above case even if OnSameLine == false, we should not
101112
// flag the open brace as it would move the brace to the next line
102113
// and will invalidate the command
103-
tokensToIgnore = new HashSet<Token>(
104-
new TokenOperations(tokens, ast).GetOpenBracesInCommandElements());
114+
var tokenOps = new TokenOperations(tokens, ast);
115+
tokensToIgnore = new HashSet<Token>(tokenOps.GetOpenBracesInCommandElements());
116+
117+
// Ignore open braces that are part of a one line if-else statement
118+
// E.g. $x = if ($true) { "blah" } else { "blah blah" }
119+
if (IgnoreOneLineBlock)
120+
{
121+
foreach (var pair in tokenOps.GetBracePairsOnSameLine())
122+
{
123+
tokensToIgnore.Add(pair.Item1);
124+
}
125+
}
126+
105127
foreach (var violationFinder in violationFinders)
106128
{
107129
diagnosticRecords.AddRange(violationFinder(tokens, ast, fileName));

Tests/Rules/PlaceCloseBrace.tests.ps1

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
Import-Module PSScriptAnalyzer
2-
$ruleName = "PSPlaceCloseBrace"
2+
$ruleConfiguration = @{
3+
Enable = $true
4+
NoEmptyLineBefore = $true
5+
IgnoreOneLineBlock = $true
6+
}
7+
38
$settings = @{
49
IncludeRules = @("PSPlaceCloseBrace")
510
Rules = @{
6-
PSPlaceCloseBrace = @{
7-
Enable = $true
8-
NoEmptyLineBefore = $true
9-
}
11+
PSPlaceCloseBrace = $ruleConfiguration
1012
}
1113
}
1214

13-
1415
Describe "PlaceCloseBrace" {
1516
Context "When a close brace is not on a new line" {
1617
BeforeAll {
@@ -83,11 +84,21 @@ $hashtable = @{
8384
$def = @'
8485
Get-Process * | % { "blah" }
8586
'@
87+
$ruleConfiguration.'IgnoreOneLineBlock' = $false
8688
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings
8789
}
8890

8991
It "Should not find a violation" {
9092
$violations.Count | Should Be 0
9193
}
94+
95+
It "Should ignore violations for one line if statement" {
96+
$def = @'
97+
$x = if ($true) { "blah" } else { "blah blah" }
98+
'@
99+
$ruleConfiguration.'IgnoreOneLineBlock' = $true
100+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings
101+
$violations.Count | Should Be 0
102+
}
92103
}
93-
}
104+
}

Tests/Rules/PlaceOpenBrace.tests.ps1

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ $ruleConfiguration = @{
33
Enable = $true
44
OnSameLine = $true
55
NewLineAfter = $true
6+
IgnoreOneLineIf = $true
67
}
78

89
$settings = @{
@@ -14,7 +15,7 @@ $settings = @{
1415

1516
Describe "PlaceOpenBrace" {
1617
Context "When an open brace must be on the same line" {
17-
BeforeAll{
18+
BeforeAll {
1819
$def = @'
1920
function foo ($param1)
2021
{
@@ -35,14 +36,15 @@ function foo ($param1)
3536
}
3637

3738
Context "When an open brace must be on a new line" {
38-
BeforeAll{
39+
BeforeAll {
3940
$def = @'
4041
function foo ($param1) {
4142
4243
}
4344
'@
4445
$ruleConfiguration.'OnSameLine' = $false
45-
$ruleConfiguration.'NewLineAfter' = $true
46+
$ruleConfiguration.'NewLineAfter' = $true
47+
$ruleConfiguration.'IgnoreOneLineBlock' = $false
4648
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings
4749
$defShouldIgnore = @'
4850
Get-Process | % { "blah" }
@@ -61,15 +63,25 @@ Get-Process | % { "blah" }
6163
It "Should ignore violations for a command element" {
6264
$violationsShouldIgnore.Count | Should Be 0
6365
}
66+
67+
It "Should ignore violations for one line if statement" {
68+
$def = @'
69+
$x = if ($true) { "blah" } else { "blah blah" }
70+
'@
71+
$ruleConfiguration.'IgnoreOneLineBlock' = $true
72+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings
73+
$violations.Count | Should Be 0
74+
}
6475
}
6576

6677
Context "When a new line should follow an open brace" {
67-
BeforeAll{
78+
BeforeAll {
6879
$def = @'
6980
function foo { }
7081
'@
7182
$ruleConfiguration.'OnSameLine' = $true
7283
$ruleConfiguration.'NewLineAfter' = $true
84+
$ruleConfiguration.'IgnoreOneLineBlock' = $false
7385
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings
7486
}
7587

0 commit comments

Comments
 (0)