From 5de8ab9436ce2427e7600993b1c837509f5df8df Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Wed, 12 Apr 2017 09:41:36 -0700 Subject: [PATCH 01/20] Add AlignAssignmentStatement rule skeleton --- RuleDocumentation/AlignAssignmentStatement.md | 17 +++ Rules/AlignAssignmentStatement.cs | 109 ++++++++++++++++++ Rules/ScriptAnalyzerBuiltinRules.csproj | 1 + Rules/Strings.resx | 12 ++ .../Rules/AlignAssignmentStatement.tests.ps1 | 9 ++ 5 files changed, 148 insertions(+) create mode 100644 RuleDocumentation/AlignAssignmentStatement.md create mode 100644 Rules/AlignAssignmentStatement.cs create mode 100644 Tests/Rules/AlignAssignmentStatement.tests.ps1 diff --git a/RuleDocumentation/AlignAssignmentStatement.md b/RuleDocumentation/AlignAssignmentStatement.md new file mode 100644 index 000000000..e7dd6995a --- /dev/null +++ b/RuleDocumentation/AlignAssignmentStatement.md @@ -0,0 +1,17 @@ +# AlignAssignmentStatement +**Severity Level: Warning** + +## Description + +## How to Fix + +## Example +### Wrong: +```PowerShell + +``` + +### Correct: +```PowerShell + +``` diff --git a/Rules/AlignAssignmentStatement.cs b/Rules/AlignAssignmentStatement.cs new file mode 100644 index 000000000..14ac0198a --- /dev/null +++ b/Rules/AlignAssignmentStatement.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Collections.Generic; +#if !CORECLR +using System.ComponentModel.Composition; +#endif +using System.Globalization; +using System.Linq; +using System.Management.Automation.Language; +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; + +namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules +{ + /// + /// A class to walk an AST to check if consecutive assignment statements are aligned. + /// + #if !CORECLR + [Export(typeof(IScriptRule))] +#endif + class AlignAssignmentStatement : ConfigurableRule + { + /// + /// Analyzes the given ast to find if consecutive assignment statements are aligned. + /// + /// AST to be analyzed. This should be non-null + /// Name of file that corresponds to the input AST. + /// A an enumerable type containing the violations + public override IEnumerable AnalyzeScript(Ast ast, string fileName) + { + if (ast == null) + { + throw new ArgumentNullException("ast"); + } + + // your code goes here + yield break; + } + + /// + /// Retrieves the common name of this rule. + /// + public override string GetCommonName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.AlignAssignmentStatementCommonName); + } + + /// + /// Retrieves the description of this rule. + /// + public override string GetDescription() + { + return string.Format(CultureInfo.CurrentCulture, Strings.AlignAssignmentStatementDescription); + } + + /// + /// Retrieves the name of this rule. + /// + public override string GetName() + { + return string.Format( + CultureInfo.CurrentCulture, + Strings.NameSpaceFormat, + GetSourceName(), + Strings.AlignAssignmentStatementName); + } + + /// + /// Retrieves the severity of the rule: error, warning or information. + /// + public override RuleSeverity GetSeverity() + { + return RuleSeverity.Warning; + } + + /// + /// Gets the severity of the returned diagnostic record: error, warning, or information. + /// + /// + public DiagnosticSeverity GetDiagnosticSeverity() + { + return DiagnosticSeverity.Warning; + } + + /// + /// Retrieves the name of the module/assembly the rule is from. + /// + public override string GetSourceName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.SourceName); + } + + /// + /// Retrieves the type of the rule, Builtin, Managed or Module. + /// + public override SourceType GetSourceType() + { + return SourceType.Builtin; + } + } +} diff --git a/Rules/ScriptAnalyzerBuiltinRules.csproj b/Rules/ScriptAnalyzerBuiltinRules.csproj index cd5c3da95..9fdc256ef 100644 --- a/Rules/ScriptAnalyzerBuiltinRules.csproj +++ b/Rules/ScriptAnalyzerBuiltinRules.csproj @@ -121,6 +121,7 @@ + diff --git a/Rules/Strings.resx b/Rules/Strings.resx index bb4f9abbc..6cea74a59 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -942,4 +942,16 @@ Use space after a semicolon. + + AlignAssignmentStatement + + + Align assignment statement + + + Line up assignment statements such that the assignment operator are aligned. + + + Assignment statements are not aligned + \ No newline at end of file diff --git a/Tests/Rules/AlignAssignmentStatement.tests.ps1 b/Tests/Rules/AlignAssignmentStatement.tests.ps1 new file mode 100644 index 000000000..2541f9af8 --- /dev/null +++ b/Tests/Rules/AlignAssignmentStatement.tests.ps1 @@ -0,0 +1,9 @@ +Import-Module PSScriptAnalyzer +$ruleName = "AlignAssignmentStatement" + +Describe "AlignAssignmentStatement" { + Context "" { + It "" { + } + } +} From 2e9940eba9bc24ff5c95968e7d0842715e25d24a Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Wed, 12 Apr 2017 09:45:41 -0700 Subject: [PATCH 02/20] Add configurable properties to AlignAssignmentStatement rule --- Rules/AlignAssignmentStatement.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Rules/AlignAssignmentStatement.cs b/Rules/AlignAssignmentStatement.cs index 14ac0198a..c5ecd32c8 100644 --- a/Rules/AlignAssignmentStatement.cs +++ b/Rules/AlignAssignmentStatement.cs @@ -28,6 +28,13 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules #endif class AlignAssignmentStatement : ConfigurableRule { + + [ConfigurableRuleProperty(defaultValue:true)] + public bool AlignInHashtable { get; set; } + + [ConfigurableRuleProperty(defaultValue:true)] + public bool AlignInDSCConfiguration { get; set; } + /// /// Analyzes the given ast to find if consecutive assignment statements are aligned. /// From 3f2e1046d15479d42b091b79eff52772ad75d458 Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Wed, 12 Apr 2017 10:15:00 -0700 Subject: [PATCH 03/20] Add initial tests for AlignAssignmentStatement rule --- .../Rules/AlignAssignmentStatement.tests.ps1 | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/Tests/Rules/AlignAssignmentStatement.tests.ps1 b/Tests/Rules/AlignAssignmentStatement.tests.ps1 index 2541f9af8..1198b3bb0 100644 --- a/Tests/Rules/AlignAssignmentStatement.tests.ps1 +++ b/Tests/Rules/AlignAssignmentStatement.tests.ps1 @@ -1,9 +1,41 @@ -Import-Module PSScriptAnalyzer -$ruleName = "AlignAssignmentStatement" +$directory = Split-Path -Parent $MyInvocation.MyCommand.Path +$testRootDirectory = Split-Path -Parent $directory + +Import-Module PSScriptAnalyzer +Import-Module (Join-Path $testRootDirectory "PSScriptAnalyzerTestHelper.psm1") + +$ruleConfiguration = @{ + Enable = $true + AlignInHashtable = $true + AlignInDSCConfiguration = $true +} + +$settings = @{ + IncludeRules = @("PSAlignAssignmentStatement") + Rules = @{ + PSAlignAssignmentStatement = $ruleConfiguration + } +} Describe "AlignAssignmentStatement" { - Context "" { - It "" { + Context "Hashtable" { + It "Should align assignment statements in a hashtable" { + $def = @' +$hashtable = @{ + property1 = "value" + anotherProperty = "another value" +} +'@ + +# Expected output should be the following +# $hashtable = @{ +# property1 = "value" +# anotherProperty = "another value" +# } + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $violations.Count | Should Be 1 + Test-CorrectionExtentFromContent $def $violations 1 '' ' ' } } } From dd0b9ea8421d3c1f570eb009b33927a8c9accaf9 Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Wed, 12 Apr 2017 10:27:39 -0700 Subject: [PATCH 04/20] WIP --- Rules/AlignAssignmentStatement.cs | 39 +++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/Rules/AlignAssignmentStatement.cs b/Rules/AlignAssignmentStatement.cs index c5ecd32c8..abdd14f07 100644 --- a/Rules/AlignAssignmentStatement.cs +++ b/Rules/AlignAssignmentStatement.cs @@ -29,11 +29,42 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules class AlignAssignmentStatement : ConfigurableRule { + private List>> violationFinders + = new List>>(); + [ConfigurableRuleProperty(defaultValue:true)] - public bool AlignInHashtable { get; set; } + public bool CheckHashtable { get; set; } [ConfigurableRuleProperty(defaultValue:true)] - public bool AlignInDSCConfiguration { get; set; } + public bool CheckDSCConfiguration { get; set; } + + public override void ConfigureRule(IDictionary paramValueMap) + { + base.ConfigureRule(paramValueMap); + if (CheckHashtable) + { + violationFinders.Add(FindHashtableViolations); + } + + if (CheckDSCConfiguration) + { + violationFinders.Add(FindDSCConfigurationViolations); + } + } + + private IEnumerable FindDSCConfigurationViolations(TokenOperations arg) + { + throw new NotImplementedException(); + } + + private IEnumerable FindHashtableViolations(TokenOperations tokenOps) + { + var hashtableAsts = tokenOps.Ast.FindAll(ast => ast is HashtableAst, true); + if (hashtableAsts == null) + { + yield break; + } + } /// /// Analyzes the given ast to find if consecutive assignment statements are aligned. @@ -47,6 +78,10 @@ public override IEnumerable AnalyzeScript(Ast ast, string file { throw new ArgumentNullException("ast"); } + // only handles one line assignments + // if the rule encounters assignment statements that are multi-line, the rule will ignore that block + + // your code goes here yield break; From 6b5e183818a45371bfdf2e82b622fb1dbd88312b Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Tue, 18 Apr 2017 18:58:46 -0700 Subject: [PATCH 05/20] Add method to get extents of equal tokens --- Rules/AlignAssignmentStatement.cs | 92 +++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/Rules/AlignAssignmentStatement.cs b/Rules/AlignAssignmentStatement.cs index abdd14f07..451cbd84b 100644 --- a/Rules/AlignAssignmentStatement.cs +++ b/Rules/AlignAssignmentStatement.cs @@ -64,6 +64,98 @@ private IEnumerable FindHashtableViolations(TokenOperations to { yield break; } + + // it is probably much easier have a hashtable writer that formats the hashtable and writes it + // but it my make handling comments hard. So we need to use this approach. + + // check if each key is on a separate line + // align only if each key=val pair is on a separate line + // if each pair on a separate line + // find all the assignment operators + // if all the assignment operators are aligned (check the column number of each alignment operator) + // skip + // else + // find the distance between the assignment operaters its left expression + // find the longest left expression + // make sure all the assignment operators are in the same column as that of the longest left hand. + // else + + var alignments = new List(); + foreach (var astItem in hashtableAsts) + { + var hashtableAst = (HashtableAst)astItem; + if (!HasKeysOnSeparateLines(hashtableAst)) + { + continue; + } + + var nodeTuples = GetExtents(tokenOps, hashtableAst); + if (nodeTuples == null) + { + continue; + } + } + } + + private static IList> GetExtents( + TokenOperations tokenOps, + HashtableAst hashtableAst) + { + var nodeTuples = new List>(); + foreach (var kvp in hashtableAst.KeyValuePairs) + { + var keyStartPos = kvp.Item1.Extent.StartScriptPosition; + var tokenNode = tokenOps.GetTokenNodes( + token => token.Extent.StartScriptPosition == keyStartPos).FirstOrDefault(); + if (tokenNode == null) + { + return null; + } + + var leftToken = tokenNode.Value; + var tempNode = tokenNode.Next; + while (tempNode != null + && tempNode.Value.Kind != TokenKind.EndOfInput + && tempNode.Value.Kind != TokenKind.Equals) + { + tempNode = tempNode.Next; + } + + if (tempNode == null || tempNode.Value.Kind == TokenKind.EndOfInput) + { + return null; + } + + var equalToken = tempNode.Value; + if (kvp.Item1.Extent.EndLineNumber != equalToken.Extent.StartLineNumber) + { + return null; + } + + nodeTuples.Add(new Tuple( + equalToken.Extent, + kvp.Item1.Extent)); + } + + return nodeTuples; + } + + private bool HasKeysOnSeparateLines(HashtableAst hashtableAst) + { + var lines = new HashSet(); + foreach (var kvp in hashtableAst.KeyValuePairs) + { + if (lines.Contains(kvp.Item1.Extent.StartLineNumber)) + { + return false; + } + else + { + lines.Add(kvp.Item1.Extent.StartLineNumber); + } + } + + return true; } /// From a522aaf8397ee583cdd77533fb83ad04239f2cba Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Thu, 20 Apr 2017 14:14:13 -0700 Subject: [PATCH 06/20] Only handle the case when equal sign follows lhs --- Rules/AlignAssignmentStatement.cs | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/Rules/AlignAssignmentStatement.cs b/Rules/AlignAssignmentStatement.cs index 451cbd84b..86951363e 100644 --- a/Rules/AlignAssignmentStatement.cs +++ b/Rules/AlignAssignmentStatement.cs @@ -105,35 +105,17 @@ private static IList> GetExtents( foreach (var kvp in hashtableAst.KeyValuePairs) { var keyStartPos = kvp.Item1.Extent.StartScriptPosition; - var tokenNode = tokenOps.GetTokenNodes( + var keyTokenNode = tokenOps.GetTokenNodes( token => token.Extent.StartScriptPosition == keyStartPos).FirstOrDefault(); - if (tokenNode == null) - { - return null; - } - - var leftToken = tokenNode.Value; - var tempNode = tokenNode.Next; - while (tempNode != null - && tempNode.Value.Kind != TokenKind.EndOfInput - && tempNode.Value.Kind != TokenKind.Equals) - { - tempNode = tempNode.Next; - } - - if (tempNode == null || tempNode.Value.Kind == TokenKind.EndOfInput) - { - return null; - } - - var equalToken = tempNode.Value; - if (kvp.Item1.Extent.EndLineNumber != equalToken.Extent.StartLineNumber) + if (keyTokenNode == null + || keyTokenNode.Next == null + || keyTokenNode.Next.Value.Kind != TokenKind.Equals) { return null; } nodeTuples.Add(new Tuple( - equalToken.Extent, + keyTokenNode.Next.Value.Extent, kvp.Item1.Extent)); } From e89d5cb7eef9ffc65d543adaba10aa1fafbd61ae Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Thu, 20 Apr 2017 15:36:59 -0700 Subject: [PATCH 07/20] Add method to get extent width in TokenOperations --- Engine/TokenOperations.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Engine/TokenOperations.cs b/Engine/TokenOperations.cs index 35d4b6828..e6040e324 100644 --- a/Engine/TokenOperations.cs +++ b/Engine/TokenOperations.cs @@ -272,6 +272,11 @@ public IEnumerable> GetOpenParensWithWhiteSpacesBefore() return GetTokenAndPrecedingWhitespace(TokenKind.LParen); } + public static int GetExtentWidth(IScriptExtent extent) + { + return extent.EndOffset - extent.StartOffset; + } + private bool OnSameLine(Token token1, Token token2) { return token1.Extent.StartLineNumber == token2.Extent.EndLineNumber; From 4ed08e63d9ba6dae5f5749cdfdfcbfa4dc6a9b65 Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Thu, 20 Apr 2017 15:37:34 -0700 Subject: [PATCH 08/20] Add logic to detect alignment violation in hashtable --- Rules/AlignAssignmentStatement.cs | 59 ++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/Rules/AlignAssignmentStatement.cs b/Rules/AlignAssignmentStatement.cs index 86951363e..bc89fd644 100644 --- a/Rules/AlignAssignmentStatement.cs +++ b/Rules/AlignAssignmentStatement.cs @@ -23,7 +23,7 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules /// /// A class to walk an AST to check if consecutive assignment statements are aligned. /// - #if !CORECLR +#if !CORECLR [Export(typeof(IScriptRule))] #endif class AlignAssignmentStatement : ConfigurableRule @@ -32,10 +32,10 @@ class AlignAssignmentStatement : ConfigurableRule private List>> violationFinders = new List>>(); - [ConfigurableRuleProperty(defaultValue:true)] + [ConfigurableRuleProperty(defaultValue: true)] public bool CheckHashtable { get; set; } - [ConfigurableRuleProperty(defaultValue:true)] + [ConfigurableRuleProperty(defaultValue: true)] public bool CheckDSCConfiguration { get; set; } public override void ConfigureRule(IDictionary paramValueMap) @@ -54,7 +54,7 @@ public override void ConfigureRule(IDictionary paramValueMap) private IEnumerable FindDSCConfigurationViolations(TokenOperations arg) { - throw new NotImplementedException(); + yield break; } private IEnumerable FindHashtableViolations(TokenOperations tokenOps) @@ -90,13 +90,42 @@ private IEnumerable FindHashtableViolations(TokenOperations to } var nodeTuples = GetExtents(tokenOps, hashtableAst); - if (nodeTuples == null) + if (nodeTuples == null + || !nodeTuples.All(t => t.Item1.StartLineNumber == t.Item2.EndLineNumber)) { continue; } + + var widestKeyExtent = nodeTuples + .Select(t => t.Item1) + .Aggregate((t1, tAggregate) => { + return TokenOperations.GetExtentWidth(tAggregate) > TokenOperations.GetExtentWidth(t1) + ? tAggregate + : t1; + }); + var expectedStartColumn = widestKeyExtent.EndColumnNumber + 1; + foreach (var extentTuple in nodeTuples) + { + if (extentTuple.Item2.StartColumnNumber != expectedStartColumn) + { + yield return new DiagnosticRecord( + GetError(), + extentTuple.Item2, + GetName(), + GetDiagnosticSeverity(), + extentTuple.Item1.File, + null, + null); + } + } } } + private string GetError() + { + return String.Format(CultureInfo.CurrentCulture, Strings.AlignAssignmentStatementError); + } + private static IList> GetExtents( TokenOperations tokenOps, HashtableAst hashtableAst) @@ -104,9 +133,9 @@ private static IList> GetExtents( var nodeTuples = new List>(); foreach (var kvp in hashtableAst.KeyValuePairs) { - var keyStartPos = kvp.Item1.Extent.StartScriptPosition; + var keyStartOffset = kvp.Item1.Extent.StartOffset; var keyTokenNode = tokenOps.GetTokenNodes( - token => token.Extent.StartScriptPosition == keyStartPos).FirstOrDefault(); + token => token.Extent.StartOffset == keyStartOffset).FirstOrDefault(); if (keyTokenNode == null || keyTokenNode.Next == null || keyTokenNode.Next.Value.Kind != TokenKind.Equals) @@ -115,8 +144,8 @@ private static IList> GetExtents( } nodeTuples.Add(new Tuple( - keyTokenNode.Next.Value.Extent, - kvp.Item1.Extent)); + kvp.Item1.Extent, + keyTokenNode.Next.Value.Extent)); } return nodeTuples; @@ -155,10 +184,14 @@ public override IEnumerable AnalyzeScript(Ast ast, string file // only handles one line assignments // if the rule encounters assignment statements that are multi-line, the rule will ignore that block - - - // your code goes here - yield break; + var tokenOps = new TokenOperations(Helper.Instance.Tokens, ast); + foreach (var violationFinder in violationFinders) + { + foreach (var diagnosticRecord in violationFinder(tokenOps)) + { + yield return diagnosticRecord; + } + } } /// From 58b16a1c9bd1f0acd3596f4823334fbc465f648d Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Thu, 20 Apr 2017 18:36:59 -0700 Subject: [PATCH 09/20] Fix option names in AlignAssignmentStatement rule test --- Tests/Rules/AlignAssignmentStatement.tests.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/Rules/AlignAssignmentStatement.tests.ps1 b/Tests/Rules/AlignAssignmentStatement.tests.ps1 index 1198b3bb0..265e6d142 100644 --- a/Tests/Rules/AlignAssignmentStatement.tests.ps1 +++ b/Tests/Rules/AlignAssignmentStatement.tests.ps1 @@ -6,8 +6,8 @@ Import-Module (Join-Path $testRootDirectory "PSScriptAnalyzerTestHelper.psm1") $ruleConfiguration = @{ Enable = $true - AlignInHashtable = $true - AlignInDSCConfiguration = $true + CheckHashtable = $true + CheckDSCConfiguration = $true } $settings = @{ @@ -35,7 +35,7 @@ $hashtable = @{ $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings $violations.Count | Should Be 1 - Test-CorrectionExtentFromContent $def $violations 1 '' ' ' + Test-CorrectionExtentFromContent $def $violations 1 ' ' ' ' } } } From 18574049acd9239d1d4447986b40aab9f37f62f6 Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Thu, 20 Apr 2017 18:37:38 -0700 Subject: [PATCH 10/20] Add corrections for hashtable violation --- Rules/AlignAssignmentStatement.cs | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/Rules/AlignAssignmentStatement.cs b/Rules/AlignAssignmentStatement.cs index bc89fd644..60c61d40d 100644 --- a/Rules/AlignAssignmentStatement.cs +++ b/Rules/AlignAssignmentStatement.cs @@ -16,7 +16,6 @@ using System.Globalization; using System.Linq; using System.Management.Automation.Language; -using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules { @@ -28,6 +27,7 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules #endif class AlignAssignmentStatement : ConfigurableRule { + private readonly char whitespaceChar = ' '; private List>> violationFinders = new List>>(); @@ -91,6 +91,7 @@ private IEnumerable FindHashtableViolations(TokenOperations to var nodeTuples = GetExtents(tokenOps, hashtableAst); if (nodeTuples == null + || nodeTuples.Count == 0 || !nodeTuples.All(t => t.Item1.StartLineNumber == t.Item2.EndLineNumber)) { continue; @@ -103,10 +104,10 @@ private IEnumerable FindHashtableViolations(TokenOperations to ? tAggregate : t1; }); - var expectedStartColumn = widestKeyExtent.EndColumnNumber + 1; + var expectedStartColumnNumber = widestKeyExtent.EndColumnNumber + 1; foreach (var extentTuple in nodeTuples) { - if (extentTuple.Item2.StartColumnNumber != expectedStartColumn) + if (extentTuple.Item2.StartColumnNumber != expectedStartColumnNumber) { yield return new DiagnosticRecord( GetError(), @@ -115,12 +116,28 @@ private IEnumerable FindHashtableViolations(TokenOperations to GetDiagnosticSeverity(), extentTuple.Item1.File, null, - null); + GetHashtableCorrections(extentTuple, expectedStartColumnNumber).ToList()); } } } } + private IEnumerable GetHashtableCorrections( + Tuple extentTuple, + int expectedStartColumnNumber) + { + var equalExtent = extentTuple.Item2; + var lhsExtent = extentTuple.Item1; + var columnDiff = expectedStartColumnNumber - equalExtent.StartColumnNumber; + yield return new CorrectionExtent( + lhsExtent.EndLineNumber, + equalExtent.StartLineNumber, + lhsExtent.EndColumnNumber, + equalExtent.StartColumnNumber, + new String(whitespaceChar, Math.Max(0, columnDiff) + 1), + GetError()); + } + private string GetError() { return String.Format(CultureInfo.CurrentCulture, Strings.AlignAssignmentStatementError); From c0e35455882252e0a66341bddb71451da481467b Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Thu, 20 Apr 2017 18:39:47 -0700 Subject: [PATCH 11/20] Add AlignAssignmentRule to code formatting settings --- Engine/Settings/CodeFormatting.psd1 | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Engine/Settings/CodeFormatting.psd1 b/Engine/Settings/CodeFormatting.psd1 index 89dfd787c..ba5d3a1aa 100644 --- a/Engine/Settings/CodeFormatting.psd1 +++ b/Engine/Settings/CodeFormatting.psd1 @@ -2,8 +2,9 @@ IncludeRules = @( 'PSPlaceOpenBrace', 'PSPlaceCloseBrace', + 'PSUseConsistentWhitespace', 'PSUseConsistentIndentation', - 'PSUseConsistentWhitespace' + 'PSAlignAssignmentStatement' ) Rules = @{ @@ -30,5 +31,10 @@ CheckSeparator = $true } + PSAlignAssignmentStatement = @{ + Enable = $true + CheckHashtable = $true + CheckDSCConfiguration = $true + } } } From 7d4c5411379f0e3c289c2ad318be72aae7cbf879 Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Thu, 20 Apr 2017 19:06:53 -0700 Subject: [PATCH 12/20] Update alignment correction logic --- Rules/AlignAssignmentStatement.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Rules/AlignAssignmentStatement.cs b/Rules/AlignAssignmentStatement.cs index 60c61d40d..c81d581f7 100644 --- a/Rules/AlignAssignmentStatement.cs +++ b/Rules/AlignAssignmentStatement.cs @@ -16,6 +16,7 @@ using System.Globalization; using System.Linq; using System.Management.Automation.Language; +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules { @@ -134,7 +135,7 @@ private IEnumerable GetHashtableCorrections( equalExtent.StartLineNumber, lhsExtent.EndColumnNumber, equalExtent.StartColumnNumber, - new String(whitespaceChar, Math.Max(0, columnDiff) + 1), + new String(whitespaceChar, expectedStartColumnNumber - lhsExtent.EndColumnNumber), GetError()); } From 053f141a5b335ec764defcc15149f130dde62ca2 Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Thu, 20 Apr 2017 19:07:11 -0700 Subject: [PATCH 13/20] Add test to verify correction for negative whitespace --- .../Rules/AlignAssignmentStatement.tests.ps1 | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/Tests/Rules/AlignAssignmentStatement.tests.ps1 b/Tests/Rules/AlignAssignmentStatement.tests.ps1 index 265e6d142..46fe87ad8 100644 --- a/Tests/Rules/AlignAssignmentStatement.tests.ps1 +++ b/Tests/Rules/AlignAssignmentStatement.tests.ps1 @@ -19,7 +19,7 @@ $settings = @{ Describe "AlignAssignmentStatement" { Context "Hashtable" { - It "Should align assignment statements in a hashtable" { + It "Should align assignment statements in a hashtable when need to add whitespace" { $def = @' $hashtable = @{ property1 = "value" @@ -27,7 +27,7 @@ $hashtable = @{ } '@ -# Expected output should be the following +# Expected output after correction should be the following # $hashtable = @{ # property1 = "value" # anotherProperty = "another value" @@ -37,5 +37,24 @@ $hashtable = @{ $violations.Count | Should Be 1 Test-CorrectionExtentFromContent $def $violations 1 ' ' ' ' } - } + + It "Should align assignment statements in a hashtable when need to remove whitespace" { + $def = @' +$hashtable = @{ + property1 = "value" + anotherProperty = "another value" +} +'@ + +# Expected output should be the following +# $hashtable = @{ +# property1 = "value" +# anotherProperty = "another value" +# } + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $violations.Count | Should Be 1 + Test-CorrectionExtentFromContent $def $violations 1 ' ' ' ' + } +} } From 613f06ecbf31eedaa6cddbf081aeddf92a5ea8d3 Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Fri, 21 Apr 2017 12:01:18 -0700 Subject: [PATCH 14/20] Remove CheckDSCConfiguration switch We remove this switch because the propery value pairs in a DSC configuration are in fact key value pairs of a hashtable. Since, we already have a switch for checking hashtable, CheckDSCConfiguration switch is redundant. --- Rules/AlignAssignmentStatement.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Rules/AlignAssignmentStatement.cs b/Rules/AlignAssignmentStatement.cs index c81d581f7..2fd81c8a4 100644 --- a/Rules/AlignAssignmentStatement.cs +++ b/Rules/AlignAssignmentStatement.cs @@ -36,9 +36,6 @@ private List>> violationFind [ConfigurableRuleProperty(defaultValue: true)] public bool CheckHashtable { get; set; } - [ConfigurableRuleProperty(defaultValue: true)] - public bool CheckDSCConfiguration { get; set; } - public override void ConfigureRule(IDictionary paramValueMap) { base.ConfigureRule(paramValueMap); @@ -46,16 +43,6 @@ public override void ConfigureRule(IDictionary paramValueMap) { violationFinders.Add(FindHashtableViolations); } - - if (CheckDSCConfiguration) - { - violationFinders.Add(FindDSCConfigurationViolations); - } - } - - private IEnumerable FindDSCConfigurationViolations(TokenOperations arg) - { - yield break; } private IEnumerable FindHashtableViolations(TokenOperations tokenOps) From 1c0bacda142d9ca1082c3ffb09a7a9742b582d8d Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Fri, 21 Apr 2017 13:52:19 -0700 Subject: [PATCH 15/20] Add code documentation and re-arrange elements --- Rules/AlignAssignmentStatement.cs | 199 ++++++++++++++++-------------- 1 file changed, 104 insertions(+), 95 deletions(-) diff --git a/Rules/AlignAssignmentStatement.cs b/Rules/AlignAssignmentStatement.cs index 2fd81c8a4..9c5747f59 100644 --- a/Rules/AlignAssignmentStatement.cs +++ b/Rules/AlignAssignmentStatement.cs @@ -28,14 +28,25 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules #endif class AlignAssignmentStatement : ConfigurableRule { + // We keep this switch even though the rule has only one switch (this) as of now, because we want + // to let the rule be expandable in the future to allow formatting assignments even + // in variable assignments. But for now we will stick to only one option. + /// + /// Check if key value pairs in a hashtable are aligned or not. + /// + /// + [ConfigurableRuleProperty(defaultValue: true)] + public bool CheckHashtable { get; set; } + private readonly char whitespaceChar = ' '; private List>> violationFinders = new List>>(); - [ConfigurableRuleProperty(defaultValue: true)] - public bool CheckHashtable { get; set; } - + /// + /// Sets the configurable properties of this rule. + /// + /// A dictionary that maps parameter name to it value. Must be non-null public override void ConfigureRule(IDictionary paramValueMap) { base.ConfigureRule(paramValueMap); @@ -45,6 +56,92 @@ public override void ConfigureRule(IDictionary paramValueMap) } } + + /// + /// Analyzes the given ast to find if consecutive assignment statements are aligned. + /// + /// AST to be analyzed. This should be non-null + /// Name of file that corresponds to the input AST. + /// A an enumerable type containing the violations + public override IEnumerable AnalyzeScript(Ast ast, string fileName) + { + if (ast == null) + { + throw new ArgumentNullException("ast"); + } + // only handles one line assignments + // if the rule encounters assignment statements that are multi-line, the rule will ignore that block + + var tokenOps = new TokenOperations(Helper.Instance.Tokens, ast); + foreach (var violationFinder in violationFinders) + { + foreach (var diagnosticRecord in violationFinder(tokenOps)) + { + yield return diagnosticRecord; + } + } + } + + /// + /// Retrieves the common name of this rule. + /// + public override string GetCommonName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.AlignAssignmentStatementCommonName); + } + + /// + /// Retrieves the description of this rule. + /// + public override string GetDescription() + { + return string.Format(CultureInfo.CurrentCulture, Strings.AlignAssignmentStatementDescription); + } + + /// + /// Retrieves the name of this rule. + /// + public override string GetName() + { + return string.Format( + CultureInfo.CurrentCulture, + Strings.NameSpaceFormat, + GetSourceName(), + Strings.AlignAssignmentStatementName); + } + + /// + /// Retrieves the severity of the rule: error, warning or information. + /// + public override RuleSeverity GetSeverity() + { + return RuleSeverity.Warning; + } + + /// + /// Gets the severity of the returned diagnostic record: error, warning, or information. + /// + /// + public DiagnosticSeverity GetDiagnosticSeverity() + { + return DiagnosticSeverity.Warning; + } + + /// + /// Retrieves the name of the module/assembly the rule is from. + /// + public override string GetSourceName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.SourceName); + } + + /// + /// Retrieves the type of the rule, Builtin, Managed or Module. + /// + public override SourceType GetSourceType() + { + return SourceType.Builtin; + } private IEnumerable FindHashtableViolations(TokenOperations tokenOps) { var hashtableAsts = tokenOps.Ast.FindAll(ast => ast is HashtableAst, true); @@ -54,19 +151,17 @@ private IEnumerable FindHashtableViolations(TokenOperations to } // it is probably much easier have a hashtable writer that formats the hashtable and writes it - // but it my make handling comments hard. So we need to use this approach. + // but it makes handling comments hard. So we need to use this approach. - // check if each key is on a separate line - // align only if each key=val pair is on a separate line - // if each pair on a separate line + // This is how the algorithm actually works: + // if each key value pair are on a separate line // find all the assignment operators // if all the assignment operators are aligned (check the column number of each alignment operator) // skip // else - // find the distance between the assignment operaters its left expression + // find the distance between the assignment operators and their corresponding LHS // find the longest left expression // make sure all the assignment operators are in the same column as that of the longest left hand. - // else var alignments = new List(); foreach (var astItem in hashtableAsts) @@ -173,91 +268,5 @@ private bool HasKeysOnSeparateLines(HashtableAst hashtableAst) return true; } - - /// - /// Analyzes the given ast to find if consecutive assignment statements are aligned. - /// - /// AST to be analyzed. This should be non-null - /// Name of file that corresponds to the input AST. - /// A an enumerable type containing the violations - public override IEnumerable AnalyzeScript(Ast ast, string fileName) - { - if (ast == null) - { - throw new ArgumentNullException("ast"); - } - // only handles one line assignments - // if the rule encounters assignment statements that are multi-line, the rule will ignore that block - - var tokenOps = new TokenOperations(Helper.Instance.Tokens, ast); - foreach (var violationFinder in violationFinders) - { - foreach (var diagnosticRecord in violationFinder(tokenOps)) - { - yield return diagnosticRecord; - } - } - } - - /// - /// Retrieves the common name of this rule. - /// - public override string GetCommonName() - { - return string.Format(CultureInfo.CurrentCulture, Strings.AlignAssignmentStatementCommonName); - } - - /// - /// Retrieves the description of this rule. - /// - public override string GetDescription() - { - return string.Format(CultureInfo.CurrentCulture, Strings.AlignAssignmentStatementDescription); - } - - /// - /// Retrieves the name of this rule. - /// - public override string GetName() - { - return string.Format( - CultureInfo.CurrentCulture, - Strings.NameSpaceFormat, - GetSourceName(), - Strings.AlignAssignmentStatementName); - } - - /// - /// Retrieves the severity of the rule: error, warning or information. - /// - public override RuleSeverity GetSeverity() - { - return RuleSeverity.Warning; - } - - /// - /// Gets the severity of the returned diagnostic record: error, warning, or information. - /// - /// - public DiagnosticSeverity GetDiagnosticSeverity() - { - return DiagnosticSeverity.Warning; - } - - /// - /// Retrieves the name of the module/assembly the rule is from. - /// - public override string GetSourceName() - { - return string.Format(CultureInfo.CurrentCulture, Strings.SourceName); - } - - /// - /// Retrieves the type of the rule, Builtin, Managed or Module. - /// - public override SourceType GetSourceType() - { - return SourceType.Builtin; - } } } From 94c62040f54c5de6604d42ee5293efbd894d26a1 Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Fri, 21 Apr 2017 13:53:57 -0700 Subject: [PATCH 16/20] Rename local variable in FindHashtableViolations --- Rules/AlignAssignmentStatement.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Rules/AlignAssignmentStatement.cs b/Rules/AlignAssignmentStatement.cs index 9c5747f59..20b92cc82 100644 --- a/Rules/AlignAssignmentStatement.cs +++ b/Rules/AlignAssignmentStatement.cs @@ -172,15 +172,15 @@ private IEnumerable FindHashtableViolations(TokenOperations to continue; } - var nodeTuples = GetExtents(tokenOps, hashtableAst); - if (nodeTuples == null - || nodeTuples.Count == 0 - || !nodeTuples.All(t => t.Item1.StartLineNumber == t.Item2.EndLineNumber)) + var extentTuples = GetExtents(tokenOps, hashtableAst); + if (extentTuples == null + || extentTuples.Count == 0 + || !extentTuples.All(t => t.Item1.StartLineNumber == t.Item2.EndLineNumber)) { continue; } - var widestKeyExtent = nodeTuples + var widestKeyExtent = extentTuples .Select(t => t.Item1) .Aggregate((t1, tAggregate) => { return TokenOperations.GetExtentWidth(tAggregate) > TokenOperations.GetExtentWidth(t1) @@ -188,7 +188,7 @@ private IEnumerable FindHashtableViolations(TokenOperations to : t1; }); var expectedStartColumnNumber = widestKeyExtent.EndColumnNumber + 1; - foreach (var extentTuple in nodeTuples) + foreach (var extentTuple in extentTuples) { if (extentTuple.Item2.StartColumnNumber != expectedStartColumnNumber) { From 1b7511267eaf5218f6a1e7e3143faf6e37d84214 Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Fri, 21 Apr 2017 14:29:36 -0700 Subject: [PATCH 17/20] Add more tests to AlignAssignmentStatement rule --- .../Rules/AlignAssignmentStatement.tests.ps1 | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/Tests/Rules/AlignAssignmentStatement.tests.ps1 b/Tests/Rules/AlignAssignmentStatement.tests.ps1 index 46fe87ad8..207b5f088 100644 --- a/Tests/Rules/AlignAssignmentStatement.tests.ps1 +++ b/Tests/Rules/AlignAssignmentStatement.tests.ps1 @@ -7,7 +7,6 @@ Import-Module (Join-Path $testRootDirectory "PSScriptAnalyzerTestHelper.psm1") $ruleConfiguration = @{ Enable = $true CheckHashtable = $true - CheckDSCConfiguration = $true } $settings = @{ @@ -18,8 +17,8 @@ $settings = @{ } Describe "AlignAssignmentStatement" { - Context "Hashtable" { - It "Should align assignment statements in a hashtable when need to add whitespace" { + Context "When assignment statements are in hashtable" { + It "Should find violation when assignment statements are not aligned (whitespace needs to be added)" { $def = @' $hashtable = @{ property1 = "value" @@ -38,7 +37,7 @@ $hashtable = @{ Test-CorrectionExtentFromContent $def $violations 1 ' ' ' ' } - It "Should align assignment statements in a hashtable when need to remove whitespace" { + It "Should find violation when assignment statements are not aligned (whitespace needs to be removed)" { $def = @' $hashtable = @{ property1 = "value" @@ -56,5 +55,37 @@ $hashtable = @{ $violations.Count | Should Be 1 Test-CorrectionExtentFromContent $def $violations 1 ' ' ' ' } + + It "Should ignore if a hashtable is empty" { + $def = @' +$x = @{ } +'@ + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Get-Count | Should Be 0 + + } + } + + Context "When assignment statements are in DSC Configuration" { + It "Should find violations when assignment statements are not aligned" { + $def = @' +Configuration MyDscConfiguration { + + param( + [string[]]$ComputerName="localhost" + ) + Node $ComputerName { + WindowsFeature MyFeatureInstance { + Ensure = "Present" + Name = "RSAT" + } + WindowsFeature My2ndFeatureInstance { + Ensure = "Present" + Name = "Bitlocker" + } + } +} +'@ + Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings | Get-Count | Should Be 2 + } } } From 40e21f3123d95ca4ce73683a2ba7812c2e086438 Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Fri, 21 Apr 2017 14:50:54 -0700 Subject: [PATCH 18/20] Add documentation for AlignAssignmentStatement rule --- RuleDocumentation/AlignAssignmentStatement.md | 43 ++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/RuleDocumentation/AlignAssignmentStatement.md b/RuleDocumentation/AlignAssignmentStatement.md index e7dd6995a..1462bd035 100644 --- a/RuleDocumentation/AlignAssignmentStatement.md +++ b/RuleDocumentation/AlignAssignmentStatement.md @@ -1,17 +1,48 @@ # AlignAssignmentStatement + **Severity Level: Warning** ## Description -## How to Fix +Consecutive assignment statements are more readable if they are aligned. By aligned, we imply that the `equal` sign for all the assignment statements should be in the same column. -## Example -### Wrong: -```PowerShell +The rule looks for key (property) value pairs in a hashtable (DSC configuration) to check if they are aligned or not. Consider the following example in which the key value pairs are not aligned. +```powershell +$hashtable = @{ + property1 = "value" + anotherProperty = "another value" +} ``` -### Correct: -```PowerShell +Alignment in this case would look like the following. + +```powershell +$hashtable = @{ + property1 = "value" + anotherProperty = "another value" +} +``` + +The rule will ignore hashtables in which the assignment statements are on the same line. For example, the rule will ignore `$h = {a = 1; b = 2}`. + +## Configuration +```powershell + Rules = @{ + PSAlignAssignmentStatement = @{ + Enable = $true + CheckHashtable = $true + } + } ``` + +### Parameters + +#### Enable: bool (Default value is `$false`) + +Enable or disable the rule during ScriptAnalyzer invocation. + +#### CheckHashtable: bool (Default value is `$false`) + +Enforce alignment of assignment statements in a hashtable and in a DSC Configuration. There is only one switch for hasthable and DSC configuration because the property value pairs in a DSC configuration are parsed as key value pairs of a hashtable. From 90e7aa41c15e0119ad35bb1842e4b657de06ad39 Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Fri, 21 Apr 2017 17:18:51 -0700 Subject: [PATCH 19/20] Update rule count in test --- Tests/Engine/GetScriptAnalyzerRule.tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 index a81bf3634..40adc257d 100644 --- a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 +++ b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 @@ -61,7 +61,7 @@ Describe "Test Name parameters" { It "get Rules with no parameters supplied" { $defaultRules = Get-ScriptAnalyzerRule - $expectedNumRules = 49 + $expectedNumRules = 50 if ((Test-PSEditionCoreClr) -or (Test-PSVersionV3) -or (Test-PSVersionV4)) { # for PSv3 PSAvoidGlobalAliases is not shipped because @@ -70,7 +70,7 @@ Describe "Test Name parameters" { # for PowerShell Core, PSUseSingularNouns is not # shipped because it uses APIs that are not present # in dotnet core. - $expectedNumRules = 48 + $expectedNumRules = 49 } $defaultRules.Count | Should be $expectedNumRules } From ab2e03116390b831ff163063e1c15d0863017cd9 Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Fri, 21 Apr 2017 17:27:21 -0700 Subject: [PATCH 20/20] Remove unused variable FindHashtableViolation method --- Rules/AlignAssignmentStatement.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Rules/AlignAssignmentStatement.cs b/Rules/AlignAssignmentStatement.cs index 20b92cc82..531cb9654 100644 --- a/Rules/AlignAssignmentStatement.cs +++ b/Rules/AlignAssignmentStatement.cs @@ -156,14 +156,13 @@ private IEnumerable FindHashtableViolations(TokenOperations to // This is how the algorithm actually works: // if each key value pair are on a separate line // find all the assignment operators - // if all the assignment operators are aligned (check the column number of each alignment operator) + // if all the assignment operators are aligned (check the column number of each assignment operator) // skip // else // find the distance between the assignment operators and their corresponding LHS // find the longest left expression // make sure all the assignment operators are in the same column as that of the longest left hand. - var alignments = new List(); foreach (var astItem in hashtableAsts) { var hashtableAst = (HashtableAst)astItem;