diff --git a/tools/StaticAnalysis/ExampleAnalyzer/AnalyzeRules/ParameterNameAndValue.psm1 b/tools/StaticAnalysis/ExampleAnalyzer/AnalyzeRules/ParameterNameAndValue.psm1 index fa01add92330..5b6d189a2e4d 100644 --- a/tools/StaticAnalysis/ExampleAnalyzer/AnalyzeRules/ParameterNameAndValue.psm1 +++ b/tools/StaticAnalysis/ExampleAnalyzer/AnalyzeRules/ParameterNameAndValue.psm1 @@ -15,6 +15,8 @@ enum RuleNames { Mismatched_Parameter_Value_Type } +$global:UtilityOutputTypePair = @{"ConvertTo-Json" = [string]; "ConvertFrom-Json" = [hashtable]} + <# .SYNOPSIS Gets the actual name of the parameter, not alias. @@ -35,12 +37,25 @@ function Get-ParameterNameNotAlias { Gets the final actual value from ast. #> function Get-FinalVariableValue { - param([System.Management.Automation.Language.Ast]$CommandElementAst) - + param([System.Management.Automation.Language.Ast]$CommandElementAst, + [System.Management.Automation.Language.VariableExpressionAst]$VariableExpressionAst = $null) while ($true) { if ($null -ne $CommandElementAst.Expression) { $CommandElementAst = $CommandElementAst.Expression } + elseif ($null -ne $CommandElementAst.Left) { + if($CommandElementAst.Left -eq $VariableExpressionAst){ + if($CommandElementAst.Right -eq $VariableExpressionAst){ + $CommandElementAst = $null + } + else{ + $CommandElementAst = $CommandElementAst.Right + } + } + else{ + $CommandElementAst = $CommandElementAst.Left + } + } elseif ($null -ne $CommandElementAst.Target) { $CommandElementAst = $CommandElementAst.Target } @@ -48,7 +63,14 @@ function Get-FinalVariableValue { $CommandElementAst = $CommandElementAst.Pipeline } elseif ($null -ne $CommandElementAst.PipelineElements) { - $CommandElementAst = $CommandElementAst.PipelineElements[-1] + $LastElement = $CommandElementAst.PipelineElements[-1].Extent.Text + # If the LastElement contains "where" or "sort", then the type isnot changed. + if($LastElement -match "where" -or $LastElement -match "sort"){ + $CommandElementAst = $CommandElementAst.PipelineElements[0] + } + else{ + $CommandElementAst = $CommandElementAst.PipelineElements[-1] + } } elseif($null -ne $CommandElementAst.Elements){ $CommandElementAst = $CommandElementAst.Elements[0] @@ -98,12 +120,23 @@ function Get-RecoveredValueType{ } } else{ + if($Items[$j].Value -eq "new"){ + return $Type + } $Member = $Type.GetMembers() | Where-Object {$_.Name -eq $Items[$j]} - if($Member -is [array]){ - $Member = $Member[0] + if($null -eq $Member -and $null -ne $Type.ImplementedInterfaces){ + for($i = 0; $i -lt $Type.ImplementedInterfaces.Length; $i++){ + $Member = $Type.ImplementedInterfaces[$i].GetMembers() | Where-Object {$_.Name -eq $Items[$j]} + if($null -ne $Member){ + break + } + } } if($null -eq $Member){ - return $null + return $null + } + if($Member -is [array]){ + $Member = $Member[0] } if($null -ne $Member.PropertyType){ $Type = $Member.PropertyType @@ -119,6 +152,36 @@ function Get-RecoveredValueType{ return $Type } +<# + .SYNOPSIS + Measure whether the actual type matches the expected type. +#> +function Measure-IsTypeMatched{ + param ( + [System.Reflection.TypeInfo]$ExpectedType, + [System.Reflection.TypeInfo]$ActualType + ) + if($ActualType.IsArray) { + $ActualType = $ActualType.GetElementType() + } + if($ActualType.IsGenericType){ + $ActualType = $ActualType.GetGenericArguments()[0] + } + $Converter = [System.ComponentModel.TypeDescriptor]::GetConverter($ExpectedType) + if ($ActualType -eq $ExpectedType -or + $ActualType.GetInterfaces().Contains($ExpectedType) -or + $ExpectedType.GetInterfaces().Contains($ActualType) -or + $ActualType.IsSubclassOf($ExpectedType) -or + $Converter.CanConvertFrom($ActualType)) { + return $true + } + return $false +} + +<# + .SYNOPSIS + Gets the expression's actual value and type, if the parameter is assigned with a value. +#> function Get-AssignedParameterExpression { param ( [System.Management.Automation.CommandInfo]$GetCommand, @@ -134,7 +197,7 @@ function Get-AssignedParameterExpression { break } # Get the actual value - $CommandElement_Copy = Get-FinalVariableValue $global:AssignmentLeftAndRight.($CommandElement_Copy.Extent.Text) + $CommandElement_Copy = Get-FinalVariableValue $global:AssignmentLeftAndRight.($CommandElement_Copy.Extent.Text) $CommandElement_Copy if ($null -eq $CommandElement_Copy) { # Variable is not assigned with a value. # Unassigned_Variable @@ -142,16 +205,37 @@ function Get-AssignedParameterExpression { return $ExpressionToParameter } } + if($CommandElement_Copy.Extent.Text -match "foreach" -or $CommandElement_Copy.Extent.Text -match "select"){ + Write-Debug "The CommandElement contains 'foreach' or 'select'. This situation can not be handled now." + return $null + } + $ExpectedType = $GetCommand.Parameters.$ParameterNameNotAlias.ParameterType + if($CommandElement_Copy -is [System.Management.Automation.Language.HashtableAst]){ + # If ExpectedType is ValueType, then it cannot be created by Hashtable. + if($ExpectedType.IsValueType){ + # Mismatched_Parameter_Value_Type + $ExpressionToParameter = "$($CommandElement.Extent.Text)-#-$ExpectedType, created by hashtable but is value type." + return $ExpressionToParameter + } + return $null + } + while ($ExpectedType.IsArray) { + $ExpectedType = $ExpectedType.GetElementType() + } + if($ExpectedType.IsGenericType){ + $ExpectedType = $ExpectedType.GetGenericArguments()[0] + } if ($CommandElement_Copy -is [System.Management.Automation.Language.CommandAst]) { # Value is an command # If the value is created by "New-Object", then get the type behind "New-Object". if($CommandElement_Copy.CommandElements[0].Extent.Text -eq "New-Object"){ if($CommandElement_Copy.CommandElements[1].Extent.Text -eq "-TypeName"){ - $OutputType = $CommandElement_Copy.CommandElements[2].Extent.Text -as [Type] + $TypeName = $CommandElement_Copy.CommandElements[2].Extent.Text -replace "`"" } else{ - $OutputType = $CommandElement_Copy.CommandElements[1].Extent.Text -as [Type] + $TypeName = $CommandElement_Copy.CommandElements[1].Extent.Text } + $OutputType = $TypeName -as [Type] $OutputTypes = @() + $OutputType } else{ @@ -162,10 +246,16 @@ function Get-AssignedParameterExpression { return $null } $OutputTypes = @() - $j = 0 - while($GetElementCommand.OutputType[$j]){ - $OutputTypes += $GetElementCommand.OutputType[$j].Type - $j++ + if($global:UtilityOutputTypePair.ContainsKey($GetElementCommand.Name)){ + $OutputType = $global:UtilityOutputTypePair.($GetElementCommand.Name) + $OutputTypes += $OutputType + } + else{ + $j = 0 + while($GetElementCommand.OutputType[$j]){ + $OutputTypes += $GetElementCommand.OutputType[$j].Type + $j++ + } } } $flag = $true @@ -174,63 +264,43 @@ function Get-AssignedParameterExpression { $ReturnType = $OutputTypes[$j] $j++ $ActualType = Get-RecoveredValueType $CommandElement $ReturnType - $ExpectedType = $GetCommand.Parameters.$ParameterNameNotAlias.ParameterType if($null -eq $ActualType){ Continue } - if ($ExpectedType.IsArray) { - $ExpectedType = $ExpectedType.GetElementType() - } - if($ActualType.IsArray) { - $ActualType = $ActualType.GetElementType() - } - if($ActualType.IsGenericType){ - $ActualType = $ActualType.GetGenericArguments()[0] - } - if($ExpectedType.IsGenericType){ - $ExpectedType = $ExpectedType.GetGenericArguments()[0] - } - if ($ActualType -eq $ExpectedType -or $ActualType -is $ExpectedType -or - $ActualType.GetInterfaces().Contains($ExpectedType) -or $ExpectedType.GetInterfaces().Contains($ActualType)) { + if(Measure-IsTypeMatched $ExpectedType $ActualType){ $flag = $false + break } } if($flag){ # Mismatched_Parameter_Value_Type - $ExpressionToParameter = "$($CommandElement.Extent.Text)-#-$ExpectedType" + $ExpressionToParameter = "$($CommandElement.Extent.Text)-#-$ExpectedType. Now the type is $ActualType.(Command)" return $ExpressionToParameter } - } - elseif($CommandElement_Copy -is [System.Management.Automation.Language.HashtableAst]){ - # If ExpectedType is ValueType, then it cannot be created by Hashtable. - if($ExpectedType.IsValueType){ + elseif($CommandElement_Copy -is [System.Management.Automation.Language.TypeExpressionAst] -or + $CommandElement_Copy -is [System.Management.Automation.Language.TypeConstraintAst]){ + $ReturnType = $CommandElement_Copy.TypeName.ToString() -as [Type] + $ActualType = Get-RecoveredValueType $CommandElement $ReturnType + if (!(Measure-IsTypeMatched $ExpectedType $ActualType)) { # Mismatched_Parameter_Value_Type - $ExpressionToParameter = "$($CommandElement.Extent.Text)-#-$ExpectedType" + $ExpressionToParameter = "$($CommandElement.Extent.Text)-#-$ExpectedType. Now the type is $ActualType.(Type)" return $ExpressionToParameter } } elseif($CommandElement_Copy -is [System.Management.Automation.Language.ExpressionAst]) { - # Value is a constant expression - $ExpectedType = $GetCommand.Parameters.$ParameterNameNotAlias.ParameterType + # Value is a constant expression $ConvertedObject = $CommandElement_Copy.Extent.text -as $ExpectedType - $StaticType = $CommandElement_Copy.StaticType - if($ExpectedType.IsGenericType){ - $ExpectedType = $ExpectedType.GetGenericArguments()[0] - } - if($StaticType.IsGenericType){ - $StaticType = $StaticType.GetGenericArguments()[0] - } - if ($ExpectedType.IsArray){ - $ExpectedType = $ExpectedType.GetElementType() - } - if($StaticType.IsArray){ - $StaticType = $StaticType.GetElementType() + if($null -eq $ConvertedObject){ + if($null -ne (Get-Variable | Where-Object {$_.Name -eq $CommandElement_Copy.VariablePath})){ + $value = (Get-Variable | Where-Object {$_.Name -eq $CommandElement_Copy.VariablePath}).Value + } + $ConvertedObject = $value -as $ExpectedType } - if ($StaticType -ne $ExpectedType -and $null -eq $ConvertedObject -and - !$StaticType.GetInterfaces().Contains($ExpectedType) -and !$ExpectedType.GetInterfaces().Contains($StaticType)) { + $StaticType = $CommandElement_Copy.StaticType + if (!(Measure-IsTypeMatched $ExpectedType $StaticType) -and $null -eq $ConvertedObject) { # Mismatched_Parameter_Value_Type - $ExpressionToParameter = "$($CommandElement.Extent.Text)-#-$ExpectedType" + $ExpressionToParameter = "$($CommandElement.Extent.Text)-#-$ExpectedType. Now the type is $StaticType.(Static)" return $ExpressionToParameter } } @@ -272,7 +342,12 @@ function Measure-ParameterNameAndValue { if ($Ast -is [System.Management.Automation.Language.AssignmentStatementAst]) { [System.Management.Automation.Language.AssignmentStatementAst]$AssignmentStatementAst = $Ast - $global:AssignmentLeftAndRight.($AssignmentStatementAst.Left.Extent.Text) = $AssignmentStatementAst.Right + if($AssignmentStatementAst.Left -is [System.Management.Automation.Language.ConvertExpressionAst]){ + $global:AssignmentLeftAndRight.($AssignmentStatementAst.Left.Child.Extent.Text) = $AssignmentStatementAst.Left.Type + } + elseif($AssignmentStatementAst.Left -is [System.Management.Automation.Language.VariableExpressionAst]){ + $global:AssignmentLeftAndRight.($AssignmentStatementAst.Left.Extent.Text) = $AssignmentStatementAst.Right + } } if ($Ast -is [System.Management.Automation.Language.CommandElementAst] -and $Ast.Parent -is [System.Management.Automation.Language.CommandAst]) { @@ -669,4 +744,4 @@ function Measure-ParameterNameAndValue { } } -Export-ModuleMember -Function Measure-* \ No newline at end of file +Export-ModuleMember -Function Measure-* diff --git a/tools/StaticAnalysis/ExampleAnalyzer/utils.ps1 b/tools/StaticAnalysis/ExampleAnalyzer/utils.ps1 index f8397bda47bb..9973344b66a0 100644 --- a/tools/StaticAnalysis/ExampleAnalyzer/utils.ps1 +++ b/tools/StaticAnalysis/ExampleAnalyzer/utils.ps1 @@ -75,16 +75,21 @@ function Get-ExamplesDetailsFromMd { $indexOfExamples = $fileContent.IndexOf($EXAMPLES_HEADING) $indexOfParameters = $fileContent.IndexOf($PARAMETERS_HEADING) - $exampleNumber = 0 + $exampleNumber = -1 $examplesProperties = @() $examplesContent = $fileContent.Substring($indexOfExamples, $indexOfParameters - $indexOfExamples) $examplesTitles = ($examplesContent | Select-String -Pattern $SINGLE_EXAMPLE_TITLE_HEADING_REGEX -AllMatches).Matches $examplesContentWithoutTitle = $examplesContent -split $SINGLE_EXAMPLE_TITLE_HEADING_REGEX | Select-Object -Skip 1 foreach ($exampleContent in $examplesContentWithoutTitle) { + $exampleNumber++ # Skip the autogenerated example if($exampleContent -match "\(autogenerated\)"){ continue } + # Skip the example whose output can not be splitted from code + if($exampleContent -match ""){ + continue + } $exampleTitle = ($examplesTitles[$exampleNumber].Value -split $SINGLE_EXAMPLE_HEADING_REGEX)[1].Trim() $exampleCodes = @() $exampleOutputs = @() @@ -323,26 +328,28 @@ function Measure-SectionMissingAndOutputScript { $examplesDetails = Get-ExamplesDetailsFromMd $MarkdownPath # If no examples if ($examplesDetails.Count -eq 0) { - $missingExampleTitle++ - $missingExampleCode++ - $missingExampleOutput++ - $missingExampleDescription++ - $result = [AnalysisOutput]@{ - Module = $Module - Cmdlet = $Cmdlet - Example = "" - Description = "Example is missing." - RuleName = "MissingExample" - Severity = $missingSeverity - Extent = "$Module\help\$Cmdlet.md" - ProblemID = 5042 - Remediation = "Add Example. Remove any placeholders." + if($fileContent -notmatch "\(autogenerated\)" -and $fileContent -notmatch ""){ + $missingExampleTitle++ + $missingExampleCode++ + $missingExampleOutput++ + $missingExampleDescription++ + $result = [AnalysisOutput]@{ + Module = $Module + Cmdlet = $Cmdlet + Example = "" + Description = "Example is missing." + RuleName = "MissingExample" + Severity = $missingSeverity + Extent = "$Module\help\$Cmdlet.md" + ProblemID = 5042 + Remediation = "Add Example. Remove any placeholders." + } + $results += $result } - $results += $result } else { foreach ($exampleDetails in $examplesDetails) { - $exampleNumber++ + $exampleNumber = $exampleDetails.Num $_missingExampleTitle = ($exampleDetails.Title | Select-String -Pattern "{{[A-Za-z ]*}}").Count $_missingExampleCode = ($exampleDetails.Codes | Select-String -Pattern "{{[A-Za-z ]*}}").Count $_missingExampleOutput = ($exampleDetails.Outputs | Select-String -Pattern "{{[A-Za-z ]*}}").Count @@ -435,7 +442,7 @@ function Measure-SectionMissingAndOutputScript { RuleName = "NeedDeleting" Severity = $missingSeverity Extent = "$Module\help\$Cmdlet.md" - ProblemID = 5051 + ProblemID = 5052 Remediation = "Delete the prompt of example." } $results += $result diff --git a/tools/StaticAnalysis/Exceptions/Az.Accounts/ExampleIssues.csv b/tools/StaticAnalysis/Exceptions/Az.Accounts/ExampleIssues.csv new file mode 100644 index 000000000000..67c001ce7aca --- /dev/null +++ b/tools/StaticAnalysis/Exceptions/Az.Accounts/ExampleIssues.csv @@ -0,0 +1,8 @@ +"Module","Cmdlet","Example","RuleName","ProblemID","Severity","Description","Extent","Remediation" +"Accounts","Disconnect-AzAccount","2","Unbinded_Expression","5014","2","Get-AzContext 'Work' is not explicitly assigned to a parameter.","'Work'","Assign 'Work' explicitly to the parameter." +"Accounts","Remove-AzContext","1","Invalid_Parameter_Name","5011","2","Remove-AzContext -Name is not a valid parameter name.","-Name","Check validity of the parameter Name." +"Accounts","Rename-AzContext","1","Invalid_Parameter_Name","5011","2","Rename-AzContext -SourceName is not a valid parameter name.","-SourceName","Check validity of the parameter SourceName." +"Accounts","Rename-AzContext","1","Invalid_Parameter_Name","5011","2","Rename-AzContext -TargetName is not a valid parameter name.","-TargetName","Check validity of the parameter TargetName." +"Accounts","Rename-AzContext","2","Unbinded_Expression","5014","2","Rename-AzContext 'My context' is not explicitly assigned to a parameter.","'My context'","Assign 'My context' explicitly to the parameter." +"Accounts","Rename-AzContext","2","Unbinded_Expression","5014","2","Rename-AzContext 'Work' is not explicitly assigned to a parameter.","'Work'","Assign 'Work' explicitly to the parameter." +"Accounts","Select-AzContext","1","Unbinded_Expression","5014","2","Select-AzContext 'Work' is not explicitly assigned to a parameter.","'Work'","Assign 'Work' explicitly to the parameter." \ No newline at end of file