diff --git a/RuleDocumentation/AvoidTrailingWhitespace.md b/RuleDocumentation/AvoidTrailingWhitespace.md new file mode 100644 index 000000000..8a5a1e189 --- /dev/null +++ b/RuleDocumentation/AvoidTrailingWhitespace.md @@ -0,0 +1,7 @@ +# AvoidTrailingWhitespace + +**Severity Level: Information** + +## Description + +Lines should not end with whitespace characters. This can cause problems with the line-continuation backtick, and also clutters up future commits to source control. diff --git a/Rules/AvoidTrailingWhitespace.cs b/Rules/AvoidTrailingWhitespace.cs new file mode 100644 index 000000000..f9fb86b68 --- /dev/null +++ b/Rules/AvoidTrailingWhitespace.cs @@ -0,0 +1,159 @@ +// 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; +using System.Text.RegularExpressions; +#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 for violation. + /// +#if !CORECLR + [Export(typeof(IScriptRule))] +#endif + public class AvoidTrailingWhitespace : IScriptRule + { + /// + /// Analyzes the given ast to find violations. + /// + /// 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 IEnumerable AnalyzeScript(Ast ast, string fileName) + { + if (ast == null) + { + throw new ArgumentNullException("ast"); + } + + var diagnosticRecords = new List(); + + string[] lines = Regex.Split(ast.Extent.Text, @"\r?\n"); + + for (int lineNumber = 0; lineNumber < lines.Length; lineNumber++) + { + var line = lines[lineNumber]; + + var match = Regex.Match(line, @"\s+$"); + if (match.Success) + { + var startLine = lineNumber + 1; + var endLine = startLine; + var startColumn = match.Index + 1; + var endColumn = startColumn + match.Length; + + var violationExtent = new ScriptExtent( + new ScriptPosition( + ast.Extent.File, + startLine, + startColumn, + line + ), + new ScriptPosition( + ast.Extent.File, + endLine, + endColumn, + line + )); + + var suggestedCorrections = new List(); + suggestedCorrections.Add(new CorrectionExtent( + violationExtent, + string.Empty, + ast.Extent.File + )); + + diagnosticRecords.Add( + new DiagnosticRecord( + String.Format(CultureInfo.CurrentCulture, Strings.AvoidTrailingWhitespaceError), + violationExtent, + GetName(), + GetDiagnosticSeverity(), + ast.Extent.File, + null, + suggestedCorrections + )); + } + } + + return diagnosticRecords; + } + + /// + /// Retrieves the common name of this rule. + /// + public string GetCommonName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.AvoidTrailingWhitespaceCommonName); + } + + /// + /// Retrieves the description of this rule. + /// + public string GetDescription() + { + return string.Format(CultureInfo.CurrentCulture, Strings.AvoidTrailingWhitespaceDescription); + } + + /// + /// Retrieves the name of this rule. + /// + public string GetName() + { + return string.Format( + CultureInfo.CurrentCulture, + Strings.NameSpaceFormat, + GetSourceName(), + Strings.AvoidTrailingWhitespaceName); + } + + /// + /// Retrieves the severity of the rule: error, warning or information. + /// + public RuleSeverity GetSeverity() + { + return RuleSeverity.Information; + } + + /// + /// Gets the severity of the returned diagnostic record: error, warning, or information. + /// + /// + public DiagnosticSeverity GetDiagnosticSeverity() + { + return DiagnosticSeverity.Information; + } + + /// + /// Retrieves the name of the module/assembly the rule is from. + /// + public string GetSourceName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.SourceName); + } + + /// + /// Retrieves the type of the rule, Builtin, Managed or Module. + /// + public SourceType GetSourceType() + { + return SourceType.Builtin; + } + } +} diff --git a/Rules/Strings.resx b/Rules/Strings.resx index 66884bff7..7148ba8ea 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -870,6 +870,18 @@ AvoidGlobalAliases + + AvoidTrailingWhitespace + + + Avoid trailing whitespace + + + Each line should have no trailing whitespace. + + + Line has trailing whitespace + PlaceOpenBrace diff --git a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 index 2ba99ecea..28980e462 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 = 51 + $expectedNumRules = 52 if ((Test-PSEditionCoreClr) -or (Test-PSVersionV3) -or (Test-PSVersionV4)) { # for PSv3 PSAvoidGlobalAliases is not shipped because @@ -159,7 +159,7 @@ Describe "TestSeverity" { It "filters rules based on multiple severity inputs"{ $rules = Get-ScriptAnalyzerRule -Severity Error,Information - $rules.Count | Should be 13 + $rules.Count | Should be 14 } It "takes lower case inputs" { diff --git a/Tests/Rules/AvoidDefaultValueForMandatoryParameterNoViolations.ps1 b/Tests/Rules/AvoidDefaultValueForMandatoryParameterNoViolations.ps1 index a42931454..0215f9252 100644 --- a/Tests/Rules/AvoidDefaultValueForMandatoryParameterNoViolations.ps1 +++ b/Tests/Rules/AvoidDefaultValueForMandatoryParameterNoViolations.ps1 @@ -2,11 +2,11 @@ { param( [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] + [ValidateNotNullOrEmpty()] [string] $Param1, [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] + [ValidateNotNullOrEmpty()] [string] $Param2=$null ) diff --git a/Tests/Rules/AvoidTrailingWhitespace.tests.ps1 b/Tests/Rules/AvoidTrailingWhitespace.tests.ps1 new file mode 100644 index 000000000..4d7126a49 --- /dev/null +++ b/Tests/Rules/AvoidTrailingWhitespace.tests.ps1 @@ -0,0 +1,35 @@ +$directory = Split-Path -Parent $MyInvocation.MyCommand.Path +$testRootDirectory = Split-Path -Parent $directory + +Import-Module PSScriptAnalyzer +Import-Module (Join-Path $testRootDirectory "PSScriptAnalyzerTestHelper.psm1") + +$ruleName = "PSAvoidTrailingWhitespace" + +$settings = @{ + IncludeRules = @($ruleName) +} + +Describe "AvoidTrailingWhitespace" { + $testCases = @( + @{ + Type = 'spaces' + Whitespace = ' ' + } + + @{ + Type = 'tabs' + Whitespace = "`t`t`t" + } + ) + + It 'Should find a violation when a line contains trailing ' -TestCases $testCases { + param ( + [string] $Whitespace + ) + + $def = "`$null = `$null$Whitespace" + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + Test-CorrectionExtentFromContent $def $violations 1 $Whitespace '' + } +} diff --git a/Tests/Rules/BadCmdlet.ps1 b/Tests/Rules/BadCmdlet.ps1 index 2958a0778..40f1a05b9 100644 --- a/Tests/Rules/BadCmdlet.ps1 +++ b/Tests/Rules/BadCmdlet.ps1 @@ -1,7 +1,7 @@ function Verb-Files { - [CmdletBinding(DefaultParameterSetName='Parameter Set 1', - SupportsShouldProcess=$true, + [CmdletBinding(DefaultParameterSetName='Parameter Set 1', + SupportsShouldProcess=$true, PositionalBinding=$false, HelpUri = 'http://www.microsoft.com/', ConfirmImpact='Medium')] @@ -12,17 +12,17 @@ Param ( # Param1 help description - [Parameter(Mandatory=$true, + [Parameter(Mandatory=$true, ValueFromPipeline=$true, - ValueFromPipelineByPropertyName=$true, - ValueFromRemainingArguments=$false, + ValueFromPipelineByPropertyName=$true, + ValueFromRemainingArguments=$false, Position=0, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [ValidateCount(0,5)] [ValidateSet("sun", "moon", "earth")] - [Alias("p1")] + [Alias("p1")] $Param1, # Param2 help description