diff --git a/Engine/Generic/CorrectionExtent.cs b/Engine/Generic/CorrectionExtent.cs index 0974bea74..8455f1f41 100644 --- a/Engine/Generic/CorrectionExtent.cs +++ b/Engine/Generic/CorrectionExtent.cs @@ -73,16 +73,16 @@ public string Description private int endColumnNumber; private string text; private string description; - + public CorrectionExtent( - int startLineNumber, - int endLineNumber, - int startColumnNumber, - int endColumnNumber, - string text, - string file) + int startLineNumber, + int endLineNumber, + int startColumnNumber, + int endColumnNumber, + string text, + string file) : this( - startLineNumber, + startLineNumber, endLineNumber, startColumnNumber, endColumnNumber, @@ -93,11 +93,11 @@ public CorrectionExtent( } public CorrectionExtent( - int startLineNumber, - int endLineNumber, - int startColumnNumber, - int endColumnNumber, - string text, + int startLineNumber, + int endLineNumber, + int startColumnNumber, + int endColumnNumber, + string text, string file, string description) { @@ -115,9 +115,8 @@ public CorrectionExtent( private void ThrowIfInvalidArguments() { - ThrowIfNull(file, "filename"); ThrowIfNull(text, "text"); - ThrowIfDecreasing(startLineNumber, endLineNumber, "start line number cannot be less than end line number"); + ThrowIfDecreasing(startLineNumber, endLineNumber, "start line number cannot be less than end line number"); if (startLineNumber == endLineNumber) { ThrowIfDecreasing(StartColumnNumber, endColumnNumber, "start column number cannot be less than end column number for a one line extent"); diff --git a/Engine/Helper.cs b/Engine/Helper.cs index c8b26c274..ae21638e6 100644 --- a/Engine/Helper.cs +++ b/Engine/Helper.cs @@ -342,6 +342,52 @@ public static bool IsMissingManifestMemberException(ErrorRecord errorRecord) && string.Equals("MissingMemberException", errorRecord.CategoryInfo.Reason, StringComparison.OrdinalIgnoreCase); } + public IEnumerable GetStringsFromExpressionAst(ExpressionAst exprAst) + { + if (exprAst == null) + { + throw new ArgumentNullException("exprAst"); + } + + var result = new List(); + if (exprAst is StringConstantExpressionAst) + { + result.Add((exprAst as StringConstantExpressionAst).Value); + } + // Array of the form "v-n", "v-n1" + else if (exprAst is ArrayLiteralAst) + { + result.AddRange(Helper.Instance.GetStringsFromArrayLiteral(exprAst as ArrayLiteralAst)); + } + // Array of the form @("v-n", "v-n1") + else if (exprAst is ArrayExpressionAst) + { + ArrayExpressionAst arrExAst = exprAst as ArrayExpressionAst; + if (arrExAst.SubExpression != null && arrExAst.SubExpression.Statements != null) + { + foreach (StatementAst stAst in arrExAst.SubExpression.Statements) + { + if (stAst is PipelineAst) + { + PipelineAst pipeAst = stAst as PipelineAst; + if (pipeAst.PipelineElements != null) + { + foreach (CommandBaseAst cmdBaseAst in pipeAst.PipelineElements) + { + if (cmdBaseAst is CommandExpressionAst) + { + result.AddRange(Helper.Instance.GetStringsFromArrayLiteral((cmdBaseAst as CommandExpressionAst).Expression as ArrayLiteralAst)); + } + } + } + } + } + } + } + + return result; + } + /// /// Get the list of exported function by analyzing the ast /// @@ -433,41 +479,7 @@ public HashSet GetExportedFunction(Ast ast) if (exprAst != null) { - // One string so just add this to the list - if (exprAst is StringConstantExpressionAst) - { - exportedFunctions.Add((exprAst as StringConstantExpressionAst).Value); - } - // Array of the form "v-n", "v-n1" - else if (exprAst is ArrayLiteralAst) - { - exportedFunctions.UnionWith(Helper.Instance.GetStringsFromArrayLiteral(exprAst as ArrayLiteralAst)); - } - // Array of the form @("v-n", "v-n1") - else if (exprAst is ArrayExpressionAst) - { - ArrayExpressionAst arrExAst = exprAst as ArrayExpressionAst; - if (arrExAst.SubExpression != null && arrExAst.SubExpression.Statements != null) - { - foreach (StatementAst stAst in arrExAst.SubExpression.Statements) - { - if (stAst is PipelineAst) - { - PipelineAst pipeAst = stAst as PipelineAst; - if (pipeAst.PipelineElements != null) - { - foreach (CommandBaseAst cmdBaseAst in pipeAst.PipelineElements) - { - if (cmdBaseAst is CommandExpressionAst) - { - exportedFunctions.UnionWith(Helper.Instance.GetStringsFromArrayLiteral((cmdBaseAst as CommandExpressionAst).Expression as ArrayLiteralAst)); - } - } - } - } - } - } - } + exportedFunctions.UnionWith(Helper.Instance.GetStringsFromExpressionAst(exprAst)); } i += 1; diff --git a/RuleDocumentation/README.md b/RuleDocumentation/README.md index 493f35eb7..f219f2a93 100644 --- a/RuleDocumentation/README.md +++ b/RuleDocumentation/README.md @@ -38,6 +38,7 @@ |[UseDeclaredVarsMoreThanAssignments](./UseDeclaredVarsMoreThanAssignments.md) | Warning| |[UseIdenticalMandatoryParametersDSC](./UseIdenticalMandatoryParametersDSC.md) | Error | |[UseIdenticalParametersDSC](./UseIdenticalParametersDSC.md) | Error | +|[UseLiteralInitializerForHashtable](./UseLiteralInitializerForHashtable.md) | Warning | |[UseOutputTypeCorrectly](./UseOutputTypeCorrectly.md) | Information| |[UsePSCredentialType](./UsePSCredentialType.md) | Warning| |[UseShouldProcessCorrectly](./UseShouldProcessCorrectly.md) | Warning| diff --git a/RuleDocumentation/UseLiteralInitializerForHashtable.md b/RuleDocumentation/UseLiteralInitializerForHashtable.md new file mode 100644 index 000000000..571dd1f71 --- /dev/null +++ b/RuleDocumentation/UseLiteralInitializerForHashtable.md @@ -0,0 +1,24 @@ +# UseLiteralInitializerForHashtable +**Severity Level: Warning** + +## Description +Creating a hashtable by either `[hashtable]::new()` or `New-Object -TypeName hashtable` will create a hashtable wherein the keys are looked-up in a case-sensitive manner, unless an `IEqualityComparer` object is passed as an argument. However, PowerShell is case-insensitive in nature and it is best to create hashtables with case-insensitive key look-up. This rule is intended to warn the author of the case-sensitive nature of the hashtable if he/she creates a hashtable using the `new` member or the `New-Object` cmdlet. + +## How to Fix +Use the full cmdlet name and not an alias. + +## Example +### Wrong: +``` PowerShell +$hashtable = [hashtable]::new() +``` + +### Wrong: +``` PowerShell +$hashtable = New-Object -TypeName hashtable +``` + +### Correct: +``` PowerShell +$hashtable = @{} +``` diff --git a/Rules/ScriptAnalyzerBuiltinRules.csproj b/Rules/ScriptAnalyzerBuiltinRules.csproj index dba1427af..504f683e5 100644 --- a/Rules/ScriptAnalyzerBuiltinRules.csproj +++ b/Rules/ScriptAnalyzerBuiltinRules.csproj @@ -95,6 +95,7 @@ Strings.resx + diff --git a/Rules/Strings.Designer.cs b/Rules/Strings.Designer.cs index ffab47fe3..9987fb8b8 100644 --- a/Rules/Strings.Designer.cs +++ b/Rules/Strings.Designer.cs @@ -1824,6 +1824,42 @@ internal static string UseIdenticalParametersDSCName { } } + /// + /// Looks up a localized string similar to Create hashtables with literal initializers. + /// + internal static string UseLiteralInitilializerForHashtableCommonName { + get { + return ResourceManager.GetString("UseLiteralInitilializerForHashtableCommonName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use literal initializer, \@\{\}, for creating a hashtable as they are case-insensitive by default. + /// + internal static string UseLiteralInitilializerForHashtableDescription { + get { + return ResourceManager.GetString("UseLiteralInitilializerForHashtableDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Create hashtables with literal initliazers. + /// + internal static string UseLiteralInitilializerForHashtableError { + get { + return ResourceManager.GetString("UseLiteralInitilializerForHashtableError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to UseLiteralInitializerForHashtable. + /// + internal static string UseLiteralInitilializerForHashtableName { + get { + return ResourceManager.GetString("UseLiteralInitilializerForHashtableName", resourceCulture); + } + } + /// /// Looks up a localized string similar to Use OutputType Correctly. /// diff --git a/Rules/Strings.resx b/Rules/Strings.resx index c4ed5c240..345f5367e 100644 --- a/Rules/Strings.resx +++ b/Rules/Strings.resx @@ -1,17 +1,17 @@  - @@ -822,4 +822,16 @@ Replace {0} with {1} - + + Create hashtables with literal initializers + + + Use literal initializer, @{{}}, for creating a hashtable as they are case-insensitive by default + + + Create hashtables with literal initliazers + + + UseLiteralInitializerForHashtable + + \ No newline at end of file diff --git a/Rules/UseLiteralInitializerForHashtable.cs b/Rules/UseLiteralInitializerForHashtable.cs new file mode 100644 index 000000000..b978f0b92 --- /dev/null +++ b/Rules/UseLiteralInitializerForHashtable.cs @@ -0,0 +1,423 @@ +// 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.Collections.ObjectModel; +using System.Collections.Specialized; +#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 hashtable is not initialized using [hashtable]::new or new-object hashtable + /// + #if !CORECLR + [Export(typeof(IScriptRule))] +#endif + class UseLiteralInitializerForHashtable : AstVisitor, IScriptRule + { + private List diagnosticRecords; + private HashSet presetTypeNameSet; + private string fileName; + + public UseLiteralInitializerForHashtable() + { + var presetTypeNames = new string[] + { + "system.collections.hashtable", + "collections.hashtable", + "hashtable" + }; + presetTypeNameSet = new HashSet(presetTypeNames, StringComparer.OrdinalIgnoreCase); + diagnosticRecords = new List(); + } + + /// + /// Analyzes the given ast to find if a hashtable is initialized using [hashtable]::new or New-Object Hashtable + /// + /// 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"); + } + + this.fileName = fileName; + diagnosticRecords.Clear(); + ast.Visit(this); + return diagnosticRecords; + } + + /// + /// Retrieves the common name of this rule. + /// + public string GetCommonName() + { + return string.Format(CultureInfo.CurrentCulture, Strings.UseLiteralInitilializerForHashtableCommonName); + } + + /// + /// Retrieves the description of this rule. + /// + public string GetDescription() + { + return string.Format(CultureInfo.CurrentCulture, Strings.UseLiteralInitilializerForHashtableDescription); + } + + /// + /// Retrieves the name of this rule. + /// + public string GetName() + { + return string.Format( + CultureInfo.CurrentCulture, + Strings.NameSpaceFormat, + GetSourceName(), + Strings.UseLiteralInitilializerForHashtableName); + } + + /// + /// Retrieves the severity of the rule: error, warning or information. + /// + public 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 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; + } + + /// + /// Visits command ast to check for "new-object" command + /// + public override AstVisitAction VisitCommand(CommandAst commandAst) + { + if (commandAst == null + || commandAst.CommandElements.Count < 2) + { + return AstVisitAction.SkipChildren; + } + + var commandName = commandAst.GetCommandName(); + if (commandName == null + || !commandName.Equals("new-object", StringComparison.OrdinalIgnoreCase)) + { + return AstVisitAction.Continue; + } + + AnalyzeNewObjectCommand(commandAst); + return AstVisitAction.Continue; + } + + /// + /// Checks if a hashtable is created using [hashtable]::new() + /// + public override AstVisitAction VisitInvokeMemberExpression(InvokeMemberExpressionAst methodCallAst) + { + if (methodCallAst == null) + { + return AstVisitAction.SkipChildren; + } + + var typeExprAst = methodCallAst.Expression as TypeExpressionAst; + if (typeExprAst == null + || !presetTypeNameSet.Contains(typeExprAst.TypeName.FullName)) + { + return AstVisitAction.Continue; + } + + var memberStringConstantExprAst = methodCallAst.Member as StringConstantExpressionAst; + if (memberStringConstantExprAst == null + || !memberStringConstantExprAst.Value.Equals("new", StringComparison.OrdinalIgnoreCase)) + { + return AstVisitAction.Continue; + } + + // no arguments provided to new OR one of the argument ends with ignorecase + // (heuristics find to something like [system.stringcomparer]::ordinalignorecase) + if (methodCallAst.Arguments == null + || !HasIgnoreCaseComparerArg(methodCallAst.Arguments)) + { + var dr = new DiagnosticRecord( + Strings.UseLiteralInitilializerForHashtableDescription, + methodCallAst.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName, + ruleId: null, + suggestedCorrections: GetSuggestedCorrections(methodCallAst, this.fileName)); + diagnosticRecords.Add(dr); + } + + return AstVisitAction.Continue; + } + + /// + /// Analyzes command ast to check for new-object command and parse its arguments + /// + private void AnalyzeNewObjectCommand(CommandAst commandAst) + { + string typeName; + List argumentList; + GetParametersFromCommandAst(commandAst, out typeName, out argumentList); + if (typeName == null + || !presetTypeNameSet.Contains(typeName)) + { + return; + } + + + if (argumentList != null + && HasIgnoreCaseComparerArg(argumentList)) + { + return; + } + + var dr = new DiagnosticRecord( + Strings.UseLiteralInitilializerForHashtableDescription, + commandAst.Extent, + GetName(), + GetDiagnosticSeverity(), + fileName, + ruleId: null, + suggestedCorrections: GetSuggestedCorrections(commandAst, this.fileName)); + diagnosticRecords.Add(dr); + } + + /// + /// Interpret the named and unnamed arguments and assign them their corresponding parameters + /// + /// PSv4 onwards there exists System.Management.Automation.Language.StaticParameterBinder.BindCommand to + /// achieve identical objective. But since we support PSSA on PSv3 too we need this implementation. + /// + /// An non-null instance of CommandAst. Expects it be commandast of "new-object" command + /// Returns the TypeName argument + /// Returns the ArgumentList argument + /// This should read the command in all the following form + /// new-object hashtable + /// new-object -Typename hashtable + /// new-object hashtable -ArgumentList comparer + /// new-object -Typename hashtable -ArgumentList blah1,blah2 + /// new-object -ArgumentList blah1,blah2 -typename hashtable + private void GetParametersFromCommandAst(CommandAst commandAst, out string typeName, out List argumentList) + { + argumentList = null; + typeName = null; + var namedArguments = new OrderedDictionary(StringComparer.OrdinalIgnoreCase); + namedArguments.Add("typename", null); + namedArguments.Add("argumentlist", null); + var positinalElems = GetNamedArguments(commandAst.CommandElements, ref namedArguments); + GetPositionalArguments(new ReadOnlyCollection(positinalElems), ref namedArguments); + if (namedArguments["typename"] == null) + { + return; + } + + var typenameAst = namedArguments["typename"] as StringConstantExpressionAst; + if (typenameAst == null) + { + return; + } + + typeName = typenameAst.Value; + var argumentListAst = namedArguments["argumentlist"] as ExpressionAst; + if (argumentListAst == null) + { + return; + } + + argumentList = new List(Helper.Instance.GetStringsFromExpressionAst(argumentListAst)); + } + + /// + /// Returns the first index whose value is null + /// + /// An ordered dictionary. It must not be null + /// Returns the first index whose value is null else returns -1 + private int GetFirstEmptyIndex(OrderedDictionary namedArguments) + { + for (int k = 0; k < namedArguments.Count; k++) + { + if (namedArguments[k] == null) + { + return k; + } + } + + return -1; + } + + /// + /// Assigns the unnamed arguments to their corresponding parameters + /// + /// A collection of arguments that need to be assigned + /// An ordered dictionary of parameters in their corresponding positions + private void GetPositionalArguments(ReadOnlyCollection unnamedArguments, ref OrderedDictionary namedArguments) + { + for (int k = 0; k < unnamedArguments.Count; k++) + { + int firstEmptyIndex = GetFirstEmptyIndex(namedArguments); + if (firstEmptyIndex == -1) + { + return; + } + + var elem = unnamedArguments[k]; + namedArguments[firstEmptyIndex] = elem as ExpressionAst; + } + } + + /// + /// Gets the named arguments from a list of command elements + /// + /// A list of command elements, typically a property of CommandAst instance + /// An ordered dictionary of parameters in their corresponding positions + /// Returns a list of unnamed arguments that remain after taking into account named parameters + private List GetNamedArguments(ReadOnlyCollection commandElements, ref OrderedDictionary namedArguments) + { + bool paramFound = false; + string paramName = null; + var remainingCommandElements = new List(); + for (int k = 1; k < commandElements.Count; k++) + { + if (paramFound) + { + paramFound = false; + var argAst = commandElements[k] as ExpressionAst; + if (argAst != null) + { + namedArguments[paramName] = argAst; + continue; + } + } + + var paramAst = commandElements[k] as CommandParameterAst; + if (paramAst != null) + { + foreach (var key in namedArguments.Keys) + { + var keyStr = key as string; + if (keyStr.Equals(paramAst.ParameterName, StringComparison.OrdinalIgnoreCase)) + { + paramFound = true; + paramName = paramAst.ParameterName; + break; + } + } + } + else + { + remainingCommandElements.Add(commandElements[k]); + } + } + + return remainingCommandElements; + } + + /// + /// Checks if any argument in the given collection ends with "ignorecase" + /// + /// A collection of argument asts. Neither this nor any elements within it should be null + private bool HasIgnoreCaseComparerArg(ReadOnlyCollection arguments) + { + var argumentsAsStrings = new List(); + foreach (var arg in arguments) + { + var memberExprAst = arg as MemberExpressionAst; + argumentsAsStrings.Add(null); + if (memberExprAst == null) + { + continue; + } + + var strConstExprAst = memberExprAst.Member as StringConstantExpressionAst; + if (strConstExprAst == null) + { + continue; + } + argumentsAsStrings[argumentsAsStrings.Count - 1] = strConstExprAst.Value; + } + return HasIgnoreCaseComparerArg(argumentsAsStrings); + } + + /// + /// Checks if any argument in the given collection ends with "ignorecase" + /// + /// An enumerable of type string. Elements can be null but the collection must be non-null . + /// + private bool HasIgnoreCaseComparerArg(IEnumerable arguments) + { + + foreach (var arg in arguments) + { + if (arg == null) + { + continue; + } + if (arg.EndsWith("ignorecase", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + + /// + /// Suggested corrections to replace the violations + /// + /// Ast representing the violation. This should not be null. + /// Name of file containing the violation + private List GetSuggestedCorrections(Ast violation, string filename) + { + var correctionExtents = new List(); + correctionExtents.Add(new CorrectionExtent( + violation.Extent.StartLineNumber, + violation.Extent.EndLineNumber, + violation.Extent.StartColumnNumber, + violation.Extent.EndColumnNumber, + "@{}", + filename, + GetDescription())); + return correctionExtents; + } + } +} diff --git a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 index d1108ce4b..d06ad73f0 100644 --- a/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 +++ b/Tests/Engine/GetScriptAnalyzerRule.tests.ps1 @@ -16,7 +16,7 @@ Describe "Test available parameters" { It "has a RuleName parameter" { $params.ContainsKey("Name") | Should Be $true } - + It "accepts string" { $params["Name"].ParameterType.FullName | Should Be "System.String[]" } @@ -61,10 +61,10 @@ Describe "Test Name parameters" { It "get Rules with no parameters supplied" { $defaultRules = Get-ScriptAnalyzerRule - $expectedNumRules = 41 + $expectedNumRules = 42 if ((Test-PSEditionCoreClr)) { - $expectedNumRules = 40 + $expectedNumRules = 41 } $defaultRules.Count | Should be $expectedNumRules } @@ -117,7 +117,7 @@ Describe "Test RuleExtension" { It "with Name of a built-in rules" { $ruleExtension = Get-ScriptAnalyzerRule -CustomizedRulePath $directory\CommunityAnalyzerRules\CommunityAnalyzerRules.psm1 -Name $singularNouns - $ruleExtension.Count | Should Be 0 + $ruleExtension.Count | Should Be 0 } It "with Names of built-in, DSC and non-built-in rules" { @@ -137,7 +137,7 @@ Describe "Test RuleExtension" { } catch { - $Error[0].FullyQualifiedErrorId | should match "PathNotFound,Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands.GetScriptAnalyzerRuleCommand" + $Error[0].FullyQualifiedErrorId | should match "PathNotFound,Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands.GetScriptAnalyzerRuleCommand" } } diff --git a/Tests/Rules/UseLiteralInitializerForHashtable.tests.ps1 b/Tests/Rules/UseLiteralInitializerForHashtable.tests.ps1 new file mode 100644 index 000000000..97227068e --- /dev/null +++ b/Tests/Rules/UseLiteralInitializerForHashtable.tests.ps1 @@ -0,0 +1,67 @@ +Import-Module PSScriptAnalyzer +$ruleName = "PSUseLiteralInitializerForHashtable" + +Describe "UseLiteralInitlializerForHashtable" { + Context "When new-object hashtable is used to create a hashtable" { + It "has violation" { + $violationScriptDef = @' + $htable1 = new-object hashtable + $htable2 = new-object system.collections.hashtable + $htable3 = new-object -Typename hashtable -ArgumentList 10 + $htable4 = new-object collections.hashtable +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $violationScriptDef -IncludeRule $ruleName + $violations.Count | Should Be 4 + } + + It "does not detect violation if arguments given to new-object contain ignore case string comparer" { + $violationScriptDef = @' + $htable1 = new-object hashtable [system.stringcomparer]::ordinalignorecase + $htable2 = new-object -Typename hashtable -ArgumentList [system.stringcomparer]::ordinalignorecase + $htable3 = new-object hashtable -ArgumentList [system.stringcomparer]::ordinalignorecase + $htable4 = new-object -Typename hashtable [system.stringcomparer]::ordinalignorecase +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $violationScriptDef -IncludeRule $ruleName + $violations.Count | Should Be 0 + } + + It "suggests correction" { + $violationScriptDef = @' + $htable1 = new-object hashtable +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $violationScriptDef -IncludeRule $ruleName + $violations[0].SuggestedCorrections[0].Text | Should Be '@{}' + } + } + + Context "When [hashtable]::new is used to create a hashtable" { + It "has violation" { + $violationScriptDef = @' + $htable1 = [hashtable]::new() + $htable2 = [system.collections.hashtable]::new() + $htable3 = [hashtable]::new(10) + $htable4 = [collections.hashtable]::new() +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $violationScriptDef -IncludeRule $ruleName + $violations.Count | Should Be 4 + } + + It "does not detect violation if arguments given to [hashtable]::new contain ignore case string comparer" { + $violationScriptDef = @' + $htable1 = [hashtable]::new(10, [system.stringcomparer]::ordinalignorecase) + $htable2 = [hashtable]::new(10, [system.stringcomparer]::ordinalignorecase) +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $violationScriptDef -IncludeRule $ruleName + $violations.Count | Should Be 0 + } + + It "suggests correction" { + $violationScriptDef = @' + $htable1 = [hashtable]::new() +'@ + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $violationScriptDef -IncludeRule $ruleName + $violations[0].SuggestedCorrections[0].Text | Should Be '@{}' + } + + } +} \ No newline at end of file