Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -35,20 +37,40 @@ 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
}
elseif ($null -ne $CommandElementAst.Pipeline) {
$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]
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -134,24 +197,45 @@ 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
$ExpressionToParameter = $CommandElement.Extent.Text + " is a null-valued parameter value."
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{
Expand All @@ -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
Expand All @@ -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
}
}
Expand Down Expand Up @@ -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]) {
Expand Down Expand Up @@ -669,4 +744,4 @@ function Measure-ParameterNameAndValue {
}
}

Export-ModuleMember -Function Measure-*
Export-ModuleMember -Function Measure-*
43 changes: 25 additions & 18 deletions tools/StaticAnalysis/ExampleAnalyzer/utils.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,21 @@ function Get-ExamplesDetailsFromMd {
$indexOfExamples = $fileContent.IndexOf($EXAMPLES_HEADING)
$indexOfParameters = $fileContent.IndexOf($PARAMETERS_HEADING)

$exampleNumber = 0
$exampleNumber = -1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you change the initial value as -1.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$examplesTitles[$exampleNumber] needs to start at index 0 and examples matching (autogenerated) or <!-- Skip.*--> needs $exampleNumber++, or the number of the titles will be confusing.

$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 "<!-- Skip.*-->"){
continue
}
$exampleTitle = ($examplesTitles[$exampleNumber].Value -split $SINGLE_EXAMPLE_HEADING_REGEX)[1].Trim()
$exampleCodes = @()
$exampleOutputs = @()
Expand Down Expand Up @@ -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 "<!-- Skip.*-->"){
$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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions tools/StaticAnalysis/Exceptions/Az.Accounts/ExampleIssues.csv
Original file line number Diff line number Diff line change
@@ -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."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this file needs to be checked in?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the file for suppressing. We discussed to add these items in exception csv instead of adding them in the source code.

"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."