diff --git a/.azure-pipelines/util/analyze-steps.yml b/.azure-pipelines/util/analyze-steps.yml index d4fa720120cb..0241121d5af0 100644 --- a/.azure-pipelines/util/analyze-steps.yml +++ b/.azure-pipelines/util/analyze-steps.yml @@ -29,9 +29,9 @@ steps: packageType: sdk version: 3.1.x -- pwsh: 'Install-Module platyPS -Force -Confirm:$false -Scope CurrentUser' - displayName: 'Install platyPS' - +- pwsh: 'Install-Module "platyPS", "PSScriptAnalyzer" -Force -Confirm:$false -Scope CurrentUser' + displayName: 'Install PowerShell Dependencies' + - task: DotNetCoreCLI@2 displayName: 'Generate Help' inputs: diff --git a/.ci-config.json b/.ci-config.json index c3ea25f1fd8d..49b5fe98c401 100644 --- a/.ci-config.json +++ b/.ci-config.json @@ -87,7 +87,7 @@ "src/{ModuleName}/**/*.md$" ], "phases": [ - "build:module", + "build:dependent-module", "help:module" ] }, diff --git a/build.proj b/build.proj index c83d798e4f4a..f1fbd8e9d44f 100644 --- a/build.proj +++ b/build.proj @@ -118,7 +118,7 @@ - + @@ -263,7 +263,12 @@ - + + + + + + diff --git a/documentation/Debugging-StaticAnalysis-Errors.md b/documentation/Debugging-StaticAnalysis-Errors.md index 3f35f5dfd69c..2974529049a5 100644 --- a/documentation/Debugging-StaticAnalysis-Errors.md +++ b/documentation/Debugging-StaticAnalysis-Errors.md @@ -8,6 +8,7 @@ Our StaticAnalysis tools help us ensure our modules follow PowerShell guidelines - [Breaking Changes](#breaking-changes) - [Signature Issues](#signature-issues) - [Help Issues](#help-issues) + - [Example Issues](#example-issues) ## How to know if you have a StaticAnalysis Error If your build is failing, click on the Jenkins job inside the PR (marked as "Default" within checks). Then check the Console Output within the Jenkins job. If you have this error, then you have failed StaticAnalysis: @@ -34,21 +35,32 @@ If you make a change that could cause a breaking change, it will be listed in `B _Note_: Sometimes the error listed in the .csv file can be a false positive (for example, if you change a parameter attribute to span all parameter sets rather than individual parameter sets). Please read the error thoroughly and examine the relevant code before deciding that an error is a false positive, and contact the Azure PowerShell team if you have questions. If you are releasing a preview module, are releasing during a breaking change release, or have determined that the error is a false positive, please follow these instructions to suppress the errors: -- Download the `BreakingChangeIssues.csv` file from the Jenkins build +- Download the `BreakingChangeIssues.csv` file from the CI pipeline artifacts - Open the file using a text editor (such as VS Code) and copy each of the errors you'd like to suppress - Paste each of these errors into the `BreakingChangeIssues.csv` file found in their respective [module folder](../tools/StaticAnalysis/Exceptions) (_e.g._, if a breaking change is being suppressed for Compute, then you would paste the corresponding line(s) in the `tools/StaticAnalysis/Exceptions/Az.Compute/BreakingChangeIssues.csv` file) using the same text editor -- Push the changes to the .csv file and ensure the errors no longer show up in the `BreakingChangeIssues.csv` file output from the Jenkins build. +- Push the changes to the .csv file and ensure the errors no longer show up in the `BreakingChangeIssues.csv` file output from the CI pipeline artifacts. We take breaking changes very seriously, so please be mindful about the violations that you suppress in our repo. ### Signature Issues Signature issues occur when your cmdlets do not follow PowerShell standards. Please check the [_Cmdlet Best Practices_](https://github.com/Azure/azure-powershell/blob/main/documentation/development-docs/design-guidelines/cmdlet-best-practices.md) and the [_Parameter Best Practices_](https://github.com/Azure/azure-powershell/blob/main/documentation/development-docs/design-guidelines/parameter-best-practices.md) documents to ensure you are following PowerShell guidelines. Issues with severity 0 or 1 must be addressed, while issues with severity 2 are advisory. If you have an issue with severity 0 or 1 that has been approved by the Azure PowerShell team, you can suppress them following these steps: -- Download the `SignatureIssues.csv` file from the Jenkins build +- Download the `SignatureIssues.csv` file from the CI pipeline artifacts - Open the file using a text editor (such as VS Code) and copy each of the errors you'd like to suppress - Paste each of these errors into the `SignatureIssues.csv` file found in their respective [module folder](../tools/StaticAnalysis/Exceptions) (_e.g.,_ if a signature issue is being suppressed for Sql, then you would paste the corresponding line(s) in the `tools/StaticAnalysis/Exceptions/Az.Sql/SignatureIssues.csv` file) using the same text editor -- Copy each of the errors you would like to suppress directly from the SignatureIssues.csv file output in the Jenkins build -- Push the changes to the .csv file and ensure the errors no longer show up in the `SignatureIssues.csv` file output from the Jenkins build. +- Copy each of the errors you would like to suppress directly from the SignatureIssues.csv file output in the CI pipeline artifacts +- Push the changes to the .csv file and ensure the errors no longer show up in the `SignatureIssues.csv` file output from the CI pipeline artifacts. ### Help Issues Most help issues that cause StaticAnalysis to fail occur when help has not been added for a particular cmdlet. If you have not generated help for your new cmdlets, please follow the instructions [here](https://github.com/Azure/azure-powershell/blob/main/documentation/development-docs/help-generation.md). If this is not the issue, follow the steps listed under "Remediation" for each violation listed in HelpIssues.csv. + +### Example Issues +Example issues occur when your changed markdown files in the `help` folder (_e.g.,_ `src/Accounts/Accounts/help`) violate PowerShell language best practices. Please follow the suggestion displayed in "Remediation" entry for each violation listed in `ExampleIssues.csv`. If you have an issue with severity 0 or 1 that has been approved by the Azure PowerShell team, you can suppress them following these steps: + +- Download the `ExampleIssues.csv` file from the CI pipeline artifacts +- Open the file using a text editor (such as VS Code) and copy each of the errors you'd like to suppress +- Paste each of these errors into the `ExampleIssues.csv` file found in their respective [module folder](../tools/StaticAnalysis/Exceptions) (_e.g.,_ if an example issue is being suppressed for Accounts, then you would paste the corresponding line(s) in the `tools/StaticAnalysis/Exceptions/Az.Accounts/ExampleIssue.csv` file) using the same text editor +- Copy each of the errors you would like to suppress directly from the ExampleIssues.csv file output in the CI pipeline artifacts +- Push the changes to the .csv file and ensure the errors no longer show up in the `ExampleIssues.csv` file output from the CI pipeline artifacts. + +To better standardize the writing of documents, please also check the warning issues with severity 2 by downloading the `ExampleIssues.csv` file. \ No newline at end of file diff --git a/documentation/tooling/static-analysis.md b/documentation/tooling/static-analysis.md index 8a0b508c065d..155318c65027 100644 --- a/documentation/tooling/static-analysis.md +++ b/documentation/tooling/static-analysis.md @@ -85,6 +85,8 @@ The dependency analyzer can be found in the [`DependencyAnalyzer`](https://githu - The implementation of the `IReportRecord` interface; defines what a missing assembly exception looks like when it's reported in the `MissingAssembly.csv` file that is found in the build artifacts of a CI run, as well as how to compare a new record to a record found in the existing `MissingAssembly.csv` file used for exception suppressions - `SharedAssemblyConflict` - The implementation of the `IReportRecord` interface; defines what a shared conflict exception looks like when it's reported in the `SharedAssemblyConflict.csv` file that is found in the build artifacts of a CI run, as well as how to compare a new record to a record found in the existing `SharedAssemblyConflict.csv` file used for exception suppressions +- `ExampleIssue` + - The implementation of the `IReportRecord` interface; defines what an example issue exception looks like when it's reported in the `ExampleIssues.csv` file that is found in the build artifacts of a CI run, as well as how to compare a new record to a record found in the existing `ExampleIssues.csv` file used for exception suppressions #### Help Analyzer diff --git a/tools/BuildPackagesTask/Microsoft.Azure.Build.Tasks/FilesChangedTask.cs b/tools/BuildPackagesTask/Microsoft.Azure.Build.Tasks/FilesChangedTask.cs index c487919e763e..dba5979bef3e 100644 --- a/tools/BuildPackagesTask/Microsoft.Azure.Build.Tasks/FilesChangedTask.cs +++ b/tools/BuildPackagesTask/Microsoft.Azure.Build.Tasks/FilesChangedTask.cs @@ -49,6 +49,11 @@ public class FilesChangedTask : Task /// public string TargetModule { get; set; } + /// + /// Gets or set the OutputFile, store FilesChanged.txt in 'artifacts' folder + /// + public string OutputFile { get; set; } + /// /// Gets or sets the files changed produced by the task. /// @@ -148,10 +153,10 @@ public override bool Execute() return true; } - // This method will record the changed files into FilesChanged.txt under root folder for other task to consum. + // This method will record the changed files into a text file at `OutputFile` for other task to consum. private void SerializeChangedFilesToFile(string[] FilesChanged) { - File.WriteAllLines("FilesChanged.txt", FilesChanged); + File.WriteAllLines(OutputFile, FilesChanged); } } } diff --git a/tools/PrepareAutorestModule.ps1 b/tools/PrepareAutorestModule.ps1 index 6d39253d5d3d..ec0ab66e1991 100644 --- a/tools/PrepareAutorestModule.ps1 +++ b/tools/PrepareAutorestModule.ps1 @@ -17,7 +17,7 @@ param( ) -$ChangedFiles = Get-Content -Path "$PSScriptRoot\..\FilesChanged.txt" +$ChangedFiles = Get-Content -Path "$PSScriptRoot\..\artifacts\FilesChanged.txt" $ALL_MODULE = "ALL_MODULE" diff --git a/tools/PrepareForSecurityCheck.ps1 b/tools/PrepareForSecurityCheck.ps1 index f4c5111a5211..a0b48705c549 100644 --- a/tools/PrepareForSecurityCheck.ps1 +++ b/tools/PrepareForSecurityCheck.ps1 @@ -17,7 +17,7 @@ param( ) -$ChangedFiles = Get-Content -Path "$PSScriptRoot\..\FilesChanged.txt" +$ChangedFiles = Get-Content -Path "$PSScriptRoot\..\artifacts\FilesChanged.txt" $SecurityTmpFolder = "$PSScriptRoot\..\SecurityTmp" New-Item -ItemType Directory -Force -Path $SecurityTmpFolder diff --git a/tools/StaticAnalysis/ExampleAnalyzer/AnalyzeRules/CommandName.psm1 b/tools/StaticAnalysis/ExampleAnalyzer/AnalyzeRules/CommandName.psm1 new file mode 100644 index 000000000000..a2b6748d73e2 --- /dev/null +++ b/tools/StaticAnalysis/ExampleAnalyzer/AnalyzeRules/CommandName.psm1 @@ -0,0 +1,143 @@ +<# + .SYNOPSIS + Custom rule for command name. + .NOTES + File: CommandName.psm1 +#> + +enum RuleNames { + Invalid_Cmdlet + Is_Alias + Capitalization_Conventions_Violated +} + +<# + .SYNOPSIS + Returns invaild, alias or unrecognized cmdlets. +#> +function Measure-CommandName { + [CmdletBinding()] + [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.ScriptBlockAst] + $ScriptBlockAst + ) + begin{ + $modulePath = "$PSScriptRoot\..\..\..\..\artifacts\Debug\Az.*\Az.*.psd1" + Get-Item $modulePath | Import-Module -Global + } + process { + $Results = @() + $global:CommandParameterPair = @() + $global:Ast = $null + + try { + [ScriptBlock]$Predicate = { + param([System.Management.Automation.Language.Ast]$Ast) + $global:Ast = $Ast + + #Find all command in .ps1 + if ($Ast -is [System.Management.Automation.Language.CommandAst]) { + [System.Management.Automation.Language.CommandAst]$CommandAst = $Ast + # Get wrapper function name by command element + $funcAst = $CommandAst + while($funcAst -isnot [System.Management.Automation.Language.FunctionDefinitionAst] -and $null -ne $funcAst.Parent.Parent.Parent){ + $funcAst = $funcAst.Parent + } + $ModuleCmdletExNum = $funcAst.name + + if ($CommandAst.InvocationOperator -eq "Unknown") { + $CommandName = $CommandAst.CommandElements[0].Extent.Text + $GetCommand = Get-Command $CommandName -ErrorAction SilentlyContinue + if ($null -eq $GetCommand) { + # CommandName is not valid. + $global:CommandParameterPair += @{ + CommandName = $CommandName + ParameterName = "" + ModuleCmdletExNum = $ModuleCmdletExNum + } + return $true + } + else { + if ($GetCommand.CommandType -eq "Alias") { + # CommandName is an alias. + $global:CommandParameterPair += @{ + CommandName = $CommandName + ParameterName = "" + ModuleCmdletExNum = $ModuleCmdletExNum + } + return $true + } + if ($CommandName -cnotmatch "^([A-Z][a-z]+)+-([A-Z][a-z0-9]*)+$") { + # CommandName doesn't follow the Capitalization Conventions. + $global:CommandParameterPair += @{ + CommandName = $CommandName + ParameterName = "" + ModuleCmdletExNum = $ModuleCmdletExNum + } + return $true + } + } + } + } + + return $false + } + + # Find all false scriptblock + [System.Management.Automation.Language.Ast[]]$Asts = $ScriptBlockAst.FindAll($Predicate, $false) + for ($i = 0; $i -lt $Asts.Count; $i++) { + if ($global:CommandParameterPair[$i].ParameterName -eq "") { + $Message = "$($CommandParameterPair[$i].CommandName) is not a valid command name." + $RuleName = [RuleNames]::Invalid_Cmdlet + $RuleSuppressionID = "5000" + $Remediation = "Check the spell of $($CommandParameterPair[$i].CommandName)." + $Severity = "Error" + } + if ($global:CommandParameterPair[$i].ParameterName -eq "") { + $Message = "$($CommandParameterPair[$i].CommandName) is an alias of `"$((Get-Alias $CommandParameterPair[$i].CommandName)[0].ResolvedCommandName)`"." + $RuleName = [RuleNames]::Is_Alias + $RuleSuppressionID = "5100" + $Remediation = "Use formal name `"$((Get-Alias $CommandParameterPair[$i].CommandName)[0].ResolvedCommandName)`" of the alias `"$($CommandParameterPair[$i].CommandName)`"." + $Severity = "Warning" + } + if ($global:CommandParameterPair[$i].ParameterName -eq "") { + $Message = "$($CommandParameterPair[$i].CommandName) doesn't follow the Capitalization Conventions." + $RuleName = [RuleNames]::Capitalization_Conventions_Violated + $RuleSuppressionID = "5101" + $name = $($CommandParameterPair[$i].CommandName) + $textInfo = (Get-Culture).TextInfo + $CorrectName = $textInfo.ToTitleCase(($name -split "-")[0]) + $CorrectName += "-Az" + $CorrectName += $textInfo.ToTitleCase(($name -split "Az")[1]) + $Remediation = "Check the Capitalization Conventions. Suggest format: $CorrectName" + $Severity = "Warning" + } + $ModuleCmdletExNum = $($CommandParameterPair[$i].ModuleCmdletExNum) + $Result = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ + Message = "$ModuleCmdletExNum-@$Message@$Remediation"; + Extent = $Asts[$i].Extent; + RuleName = $RuleName; + Severity = $Severity + RuleSuppressionID = $RuleSuppressionID + } + $Results += $Result + } + return $Results + } + catch { + $Result = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ + Message = $_.Exception.Message; + Extent = $global:Ast.Extent; + RuleName = $PSCmdlet.MyInvocation.InvocationName; + Severity = "Error" + } + $Results += $Result + return $Results + } + } +} + +Export-ModuleMember -Function Measure-* diff --git a/tools/StaticAnalysis/ExampleAnalyzer/AnalyzeRules/ParameterNameAndValue.psm1 b/tools/StaticAnalysis/ExampleAnalyzer/AnalyzeRules/ParameterNameAndValue.psm1 new file mode 100644 index 000000000000..b1ec5546f6bf --- /dev/null +++ b/tools/StaticAnalysis/ExampleAnalyzer/AnalyzeRules/ParameterNameAndValue.psm1 @@ -0,0 +1,534 @@ +<# + .SYNOPSIS + Custom rule for parameter name and value. + .NOTES + File: ParameterNameAndValue.psm1 +#> + +enum RuleNames { + Unknown_Parameter_Set + Invalid_Parameter_Name + Duplicate_Parameter_Name + Unassigned_Parameter + Unassigned_Variable + Unbinded_Parameter_Name + Mismatched_Parameter_Value_Type +} + +<# + .SYNOPSIS + Gets the actual value from ast. +#> +function Get-ActualVariableValue { + param([System.Management.Automation.Language.Ast]$CommandElementAst) + + while ($true) { + if ($null -ne $CommandElementAst.Expression) { + $CommandElementAst = $CommandElementAst.Expression + } + 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] + } + else { + break + } + } + return $CommandElementAst +} + +<# + .SYNOPSIS + Detects parameter and expression error. +#> +function Measure-ParameterNameAndValue { + [CmdletBinding()] + [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.ScriptBlockAst] + $ScriptBlockAst + ) + begin{ + $modulePath = "$PSScriptRoot\..\..\..\..\artifacts\Debug\Az.*\Az.*.psd1" + Get-Item $modulePath | Import-Module -Global + } + process { + $Results = @() + $global:CommandParameterPair = @() + $global:Ast = $null + $global:AssignmentLeftAndRight = @{} + $global:ParameterSet = $null + $global:AppearedParameters = @() + $global:AppearedExpressions = @() + $global:ParameterExpressionPair = @() + $global:SkipNextCommandElementAst = $false + + try { + [ScriptBlock]$Predicate = { + param([System.Management.Automation.Language.Ast]$Ast) + $global:Ast = $Ast + + if ($Ast -is [System.Management.Automation.Language.AssignmentStatementAst]) { + [System.Management.Automation.Language.AssignmentStatementAst]$AssignmentStatementAst = $Ast + if ($global:AssignmentLeftAndRight.ContainsKey($AssignmentStatementAst.Left.Extent.Text)) { + $global:AssignmentLeftAndRight.($AssignmentStatementAst.Left.Extent.Text) = $AssignmentStatementAst.Right + } + else { + $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]) { + [System.Management.Automation.Language.CommandElementAst]$CommandElementAst = $Ast + $funcAst = $CommandElementAst + while($funcAst -isnot [System.Management.Automation.Language.FunctionDefinitionAst] -and $null -ne $funcAst.Parent.Parent.Parent){ + $funcAst = $funcAst.Parent + } + $ModuleCmdletExNum = $funcAst.name + + if ($global:SkipNextCommandElementAst) { + $global:SkipNextCommandElementAst = $false + return $false + } + + $CommandAst = $CommandElementAst.Parent + $CommandName = $CommandAst.CommandElements[0].Extent.Text + $GetCommand = Get-Command $CommandName -ErrorAction SilentlyContinue + if ($null -eq $GetCommand) { + return $false + } + + # Get command from alias + if ($GetCommand.CommandType -eq "Alias") { + $CommandNameNotAlias = $GetCommand.ResolvedCommandName + $GetCommand = Get-Command $CommandNameNotAlias + } + + if ($CommandElementAst -is [System.Management.Automation.Language.ExpressionAst] -and + $CommandAst.CommandElements.Extent.Text.IndexOf($CommandElementAst.Extent.Text) -eq 0) { + # This CommandElement is the first CommandElement of the command. + $global:AppearedParameters = @() + $global:AppearedExpressions = @() + + # $AllParameters is the set of the command required to have + # Sort ParameterSets, move ParameterSets that have position parameters to the front. + $ParameterSets = @() + + ($GetCommand.ParameterSets | Sort-Object {($_.Parameters.Position | Where-Object {$_ -ge 0}).Count} -Descending) + foreach ($ParameterSet in $ParameterSets) { + # ParameterSets.Count is 0 when CommandName is an alias. + $AllParameterNamesInASet_Flag = $true + $Parameters = $ParameterSet.Parameters.Name + $ParameterSet.Parameters.Aliases + $AllParameters = $GetCommand.Parameters.Values.Name + $GetCommand.Parameters.Values.Aliases + foreach ($CommandElement in $CommandAst.CommandElements) { + if ($CommandElement -is [System.Management.Automation.Language.CommandParameterAst]) { + $ParameterName = ([System.Management.Automation.Language.CommandParameterAst]$CommandElement).ParameterName + if ($ParameterName -in $AllParameters -and $ParameterName -notin $Parameters) { + # Exclude ParameterNames that are not in AllParameters. They will be reported later. + $AllParameterNamesInASet_Flag = $false + break + } + } + } + if ($AllParameterNamesInASet_Flag) { + break + } + } + if ($AllParameterNamesInASet_Flag -eq $false) { + # Not all parameters in a same ParameterSet. + # Unknown_Parameter_Set + $global:ParameterSet = $null + $global:CommandParameterPair += @{ + CommandName = $CommandAst.Extent.Text + ParameterName = "" + ExpressionToParameter = "" + ModuleCmdletExNum = $ModuleCmdletExNum + } + return $true + } + else { + # Create ParameterExpressionPair + $global:ParameterSet = $ParameterSet + $global:ParameterExpressionPair = @() + for ($i = 1; $i -lt $CommandAst.CommandElements.Count;) { + $CommandElement = $CommandAst.CommandElements[$i] + $NextCommandElement = $CommandAst.CommandElements[$i + 1] + + if ($CommandElement -is [System.Management.Automation.Language.CommandParameterAst]) { + $CommandParameterElement = [System.Management.Automation.Language.CommandParameterAst]$CommandElement + $ParameterName = $CommandParameterElement.ParameterName + $ParameterNameNotAlias = $GetCommand.Parameters.Values.Name | Where-Object { + $ParameterName -in $GetCommand.Parameters.$_.Name -or + $ParameterName -in $GetCommand.Parameters.$_.Aliases + } + if ($null -eq $ParameterNameNotAlias) { + # ParameterName is not in AllParameters. + # will report later. + $global:ParameterExpressionPair += @{ + ParameterName = $ParameterName + ExpressionToParameter = "" + } + if ($null -eq $NextCommandElement -or $NextCommandElement -is [System.Management.Automation.Language.CommandParameterAst]) { + $i += 1 + } + else { + $i += 2 + } + continue + } + if ($GetCommand.Parameters.$ParameterNameNotAlias.SwitchParameter -eq $true) { + # SwitchParameter + $global:ParameterExpressionPair += @{ + ParameterName = $ParameterNameNotAlias + ExpressionToParameter = "" + } + $i += 1 + } + else { + # not a SwitchParameter + if ($null -eq $NextCommandElement -or $NextCommandElement -is [System.Management.Automation.Language.CommandParameterAst]) { + # NonSwitchParameter + Parameter + # Parameter must be assigned with a value. + # will report later. + $global:ParameterExpressionPair += @{ + ParameterName = $ParameterNameNotAlias + ExpressionToParameter = "" + } + $i += 1 + } + else { + # NonSwitchParameter + Expression + $global:ParameterExpressionPair += @{ + ParameterName = $ParameterNameNotAlias + ExpressionToParameter = $NextCommandElement + } + $i += 2 + } + } + } + elseif ($CommandElement -is [System.Management.Automation.Language.ExpressionAst]) { + $CommandExpressionElement = [System.Management.Automation.Language.ExpressionAst]$CommandElement + $PositionMaximum = ($global:ParameterSet.Parameters.Position | Measure-Object -Maximum).Maximum + for ($Position = 0; $Position -le $PositionMaximum; $Position++) { + $ImplicitParameterName = ($global:ParameterSet.Parameters | Where-Object {$_.Position -eq $Position}).Name + if ($null -ne $ImplicitParameterName -and $ImplicitParameterName -notin $global:ParameterExpressionPair.ParameterName) { + $global:ParameterExpressionPair += @{ + ParameterName = $ImplicitParameterName + ExpressionToParameter = $CommandExpressionElement + } + $i += 1 + break + } + } + if ($Position -gt $PositionMaximum) { + # This expression doesn't belong to any parameters. + # will report later. + $global:ParameterExpressionPair += @{ + ParameterName = "" + ExpressionToParameter = $CommandExpressionElement + } + $i += 1 + } + } + } + } + } + + if ($null -eq $global:ParameterSet) { + # Skip commands that can't determine their ParameterSets. + return $false + } + + if ($CommandElementAst -is [System.Management.Automation.Language.CommandParameterAst]) { + # This CommandElement is CommandParameter. + $index = $CommandAst.CommandElements.Extent.Text.IndexOf($CommandElementAst.Extent.Text) + $NextCommandElement = $CommandAst.CommandElements[$index + 1] + $ParameterName = ([System.Management.Automation.Language.CommandParameterAst]$CommandElementAst).ParameterName + $ParameterNameNotAlias = $GetCommand.Parameters.Values.Name | Where-Object { + $ParameterName -in $GetCommand.Parameters.$_.Name -or + $ParameterName -in $GetCommand.Parameters.$_.Aliases + } + if ($null -eq $ParameterNameNotAlias) { + # ParameterName is not in AllParameters. + # Invalid_Parameter_Name + if ($NextCommandElement -is [System.Management.Automation.Language.ExpressionAst]) { + $global:SkipNextCommandElementAst = $true + } + $global:CommandParameterPair += @{ + CommandName = $CommandName + ParameterName = $ParameterName + ExpressionToParameter = "" + ModuleCmdletExNum = $ModuleCmdletExNum + } + return $true + } + else { + # ParameterName is correct. + if ($ParameterNameNotAlias -in $global:AppearedParameters) { + # This parameter appeared more than once. + # Duplicate_Parameter_Name + $global:CommandParameterPair += @{ + CommandName = $CommandName + ParameterName = $ParameterNameNotAlias + ExpressionToParameter = "<2>" + ModuleCmdletExNum = $ModuleCmdletExNum + } + return $true + } + else { + $global:AppearedParameters += $ParameterNameNotAlias + } + + if ($GetCommand.Parameters.$ParameterNameNotAlias.SwitchParameter -eq $false) { + # Parameter is not a SwitchParameter. + if ($null -eq $NextCommandElement -or $NextCommandElement -is [System.Management.Automation.Language.CommandParameterAst]) { + # Parameter is not assigned with a value. + # Unassigned_Parameter + $global:CommandParameterPair += @{ + CommandName = $CommandName + ParameterName = $ParameterName + ExpressionToParameter = $null + ModuleCmdletExNum = $ModuleCmdletExNum + } + return $true + } + else { + # Parameter is assigned with a value. + $global:SkipNextCommandElementAst = $true + $NextCommandElement_Copy = Get-ActualVariableValue $NextCommandElement + + while ($NextCommandElement_Copy -is [System.Management.Automation.Language.VariableExpressionAst]) { + # Get the actual value + $NextCommandElement_Copy = Get-ActualVariableValue $global:AssignmentLeftAndRight.($NextCommandElement_Copy.Extent.Text) + if ($null -eq $NextCommandElement_Copy) { + # Variable is not assigned with a value. + # Unassigned_Variable + $global:CommandParameterPair += @{ + CommandName = $CommandName + ParameterName = "-$ParameterName" + ExpressionToParameter = $NextCommandElement.Extent.Text + " is a null-valued parameter value." + ModuleCmdletExNum = $ModuleCmdletExNum + } + return $true + } + } + if ($NextCommandElement_Copy -is [System.Management.Automation.Language.CommandAst]) { + # Value is an command + $GetNextElementCommand = Get-Command $NextCommandElement_Copy.CommandElements[0].Extent.Text -ErrorAction SilentlyContinue + if ($null -eq $GetNextElementCommand) { + # CommandName is not valid. + # will be reported in next CommandAst + return $false + } + $ReturnType = $GetNextElementCommand.OutputType[0].Type + if ($null -eq $ReturnType) + { + $ReturnType = [Object] + } + $ExpectedType = $GetCommand.Parameters.$ParameterNameNotAlias.ParameterType + if ($ReturnType -ne $ExpectedType -and $ReturnType -isnot $ExpectedType -and + !$ReturnType.GetInterfaces().Contains($ExpectedType) -and !$ReturnType.GetInterfaces().Contains($ExpectedType.GetElementType())) { + # Mismatched_Parameter_Value_Type + $global:CommandParameterPair += @{ + CommandName = $CommandName + ParameterName = "-$ParameterName" + ExpressionToParameter = $NextCommandElement.Extent.Text + ModuleCmdletExNum = $ModuleCmdletExNum + } + return $true + } + } + else { + # Value is a constant expression + $ExpectedType = $GetCommand.Parameters.$ParameterNameNotAlias.ParameterType + $ConvertedObject = $NextCommandElement_Copy.Extent.Text -as $ExpectedType + if ($NextCommandElement_Copy.StaticType -ne $ExpectedType -and $null -eq $ConvertedObject) { + # Mismatched_Parameter_Value_Type + $global:CommandParameterPair += @{ + CommandName = $CommandName + ParameterName = "-$ParameterName" + ExpressionToParameter = $NextCommandElement.Extent.Text + ModuleCmdletExNum = $ModuleCmdletExNum + } + return $true + } + } + } + } + } + } + + if ($CommandElementAst -is [System.Management.Automation.Language.ExpressionAst] -and + $CommandAst.CommandElements.Extent.Text.IndexOf($CommandElementAst.Extent.Text) -ne 0) { + # This CommandElement is an expression with implicit parameter and is not the first CommandElement. + # When there are same parameter values: + $index = ($global:AppearedExpressions | Where-Object {$_.Extent.Text -eq $CommandElementAst.Extent.Text}).Count + $PairWithThisExpression = $global:ParameterExpressionPair | Where-Object {$_.ExpressionToParameter.Extent.Text -eq $CommandElementAst.Extent.Text} + if ((@() + $PairWithThisExpression).Count -eq 1) { + $ImplicitParameterName = $PairWithThisExpression.ParameterName + } + else { + $ImplicitParameterName = $PairWithThisExpression[$index].ParameterName + } + $global:AppearedExpressions += $CommandElementAst + + if ($ImplicitParameterName -eq "") { + # This expression doesn't belong to any parameters. + # Unbinded_Parameter_Name + $global:CommandParameterPair += @{ + CommandName = $CommandName + ParameterName = $ImplicitParameterName + ExpressionToParameter = $CommandElementAst.Extent.Text + ModuleCmdletExNum = $ModuleCmdletExNum + } + return $true + } + + $CommandElementAst_Copy = Get-ActualVariableValue $CommandElementAst + while ($CommandElementAst_Copy -is [System.Management.Automation.Language.VariableExpressionAst]) { + # get the actual value + $CommandElementAst_Copy = Get-ActualVariableValue $global:AssignmentLeftAndRight.($CommandElementAst_Copy.Extent.Text) + if ($null -eq $CommandElementAst_Copy) { + # Variable is not assigned with a value. + # Unassigned_Variable + $global:CommandParameterPair += @{ + CommandName = $CommandName + ParameterName = "[-$ImplicitParameterName]" + ExpressionToParameter = $CommandElementAst.Extent.Text + " is a null-valued parameter value." + ModuleCmdletExNum = $ModuleCmdletExNum + } + return $true + } + } + if ($CommandElementAst_Copy -is [System.Management.Automation.Language.CommandAst]) { + # Value is an command + $GetElementCommand = Get-Command $CommandElementAst_Copy.CommandElements[0].Extent.Text -ErrorAction SilentlyContinue + if ($null -eq $GetElementCommand) { + # CommandName is not valid. + # will be reported in next CommandAst + return $false + } + $ReturnType = $GetElementCommand.OutputType[0].Type + if ($null -eq $ReturnType) + { + $ReturnType = [Object] + } + $ExpectedType = $GetCommand.Parameters.$ImplicitParameterName.ParameterType + if ($ReturnType -ne $ExpectedType -and $ReturnType -isnot $ExpectedType -and + !$ReturnType.GetInterfaces().Contains($ExpectedType) -and !$ReturnType.GetInterfaces().Contains($ExpectedType.GetElementType())) { + # Mismatched_Parameter_Value_Type + $global:CommandParameterPair += @{ + CommandName = $CommandName + ParameterName = "[-$ImplicitParameterName]" + ExpressionToParameter = $CommandElementAst.Extent.Text + ModuleCmdletExNum = $ModuleCmdletExNum + } + return $true + } + } + else { + # Value is a constant expression + $ExpectedType = $GetCommand.Parameters.$ImplicitParameterName.ParameterType + $ConvertedObject = $CommandElementAst_Copy.Extent.Text -as $ExpectedType + if ($CommandElementAst_Copy.StaticType -ne $ExpectedType -and $null -eq $ConvertedObject) { + # Mismatched_Parameter_Value_Type + $global:CommandParameterPair += @{ + CommandName = $CommandName + ParameterName = "[-$ImplicitParameterName]" + ExpressionToParameter = $CommandElementAst.Extent.Text + ModuleCmdletExNum = $ModuleCmdletExNum + } + return $true + } + } + } + } + + return $false + } + + [System.Management.Automation.Language.Ast[]]$Asts = $ScriptBlockAst.FindAll($Predicate, $false) + for ($i = 0; $i -lt $Asts.Count; $i++) { + if ($global:CommandParameterPair[$i].ParameterName -eq "" -and $global:CommandParameterPair[$i].ExpressionToParameter -eq "") { + $Message = "$($CommandParameterPair[$i].ModuleCmdletExNum)-@$($CommandParameterPair[$i].CommandName) has a parameter not in the same ParameterSet as others." + $RuleName = [RuleNames]::Unknown_Parameter_Set + $Severity = "Error" + $RuleSuppressionID = "5010" + $Remediation = "Make sure the parameters are from the same parameter set." + } + elseif ($global:CommandParameterPair[$i].ExpressionToParameter -eq "") { + $Message = "$($CommandParameterPair[$i].ModuleCmdletExNum)-@$($CommandParameterPair[$i].CommandName) -$($CommandParameterPair[$i].ParameterName) is not a valid parameter name." + $RuleName = [RuleNames]::Invalid_Parameter_Name + $Severity = "Error" + $RuleSuppressionID = "5011" + $Remediation = "Check validity of the parameter $($CommandParameterPair[$i].ParameterName)." + } + elseif ($global:CommandParameterPair[$i].ExpressionToParameter -eq "<2>") { + $Message = "$($CommandParameterPair[$i].ModuleCmdletExNum)-@$($CommandParameterPair[$i].CommandName) -$($CommandParameterPair[$i].ParameterName) appeared more than once." + $RuleName = [RuleNames]::Duplicate_Parameter_Name + $Severity = "Error" + $RuleSuppressionID = "5012" + $Remediation = "Remove redundant parameter $($CommandParameterPair[$i].ParameterName)." + } + elseif ($null -eq $global:CommandParameterPair[$i].ExpressionToParameter) { + $Message = "$($CommandParameterPair[$i].ModuleCmdletExNum)-@$($CommandParameterPair[$i].CommandName) -$($CommandParameterPair[$i].ParameterName) must be assigned with a value." + $RuleName = [RuleNames]::Unassigned_Parameter + $Severity = "Error" + $RuleSuppressionID = "5013" + $Remediation = "Assign value for the parameter $($CommandParameterPair[$i].ParameterName)." + } + elseif ($global:CommandParameterPair[$i].ExpressionToParameter.EndsWith(" is a null-valued parameter value.")) { + $Message = "$($CommandParameterPair[$i].ModuleCmdletExNum)-@$($CommandParameterPair[$i].CommandName) $($CommandParameterPair[$i].ParameterName) $($CommandParameterPair[$i].ExpressionToParameter)" + $RuleName = [RuleNames]::Unassigned_Variable + $Severity = "Warning" + $RuleSuppressionID = "5110" + $variable = $CommandParameterPair[$i].ExpressionToParameter -replace " is a null-valued parameter value." + $Remediation = "Assign value for $variable." + } + elseif ($global:CommandParameterPair[$i].ParameterName -eq "") { + $Message = "$($CommandParameterPair[$i].ModuleCmdletExNum)-@$($CommandParameterPair[$i].CommandName) $($CommandParameterPair[$i].ExpressionToParameter) is not explicitly assigned to a parameter." + $RuleName = [RuleNames]::Unbinded_Parameter_Name + $Severity = "Error" + $RuleSuppressionID = "5014" + $Remediation = "Assign $($CommandParameterPair[$i].ExpressionToParameter) explicitly to the parameter." + } + else { + $Message = "$($CommandParameterPair[$i].ModuleCmdletExNum)-@$($CommandParameterPair[$i].CommandName) $($CommandParameterPair[$i].ParameterName) $($CommandParameterPair[$i].ExpressionToParameter) is not an expected parameter value type." + $RuleName = [RuleNames]::Mismatched_Parameter_Value_Type + $Severity = "Warning" + $RuleSuppressionID = "5111" + $Remediation = "Use correct parameter value type." + } + $Result = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ + Message = "$Message@$Remediation"; + Extent = $Asts[$i].Extent; + RuleName = $RuleName; + Severity = $Severity + RuleSuppressionID = $RuleSuppressionID + } + $Results += $Result + } + return $Results + } + catch { + $Result = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]@{ + Message = $_.Exception.Message; + Extent = $global:Ast.Extent; + RuleName = $PSCmdlet.MyInvocation.InvocationName; + Severity = "Error" + } + $Results += $Result + return $Results + } + } +} + +Export-ModuleMember -Function Measure-* diff --git a/tools/StaticAnalysis/ExampleAnalyzer/ExampleIssue.cs b/tools/StaticAnalysis/ExampleAnalyzer/ExampleIssue.cs new file mode 100644 index 000000000000..60375f54a5f9 --- /dev/null +++ b/tools/StaticAnalysis/ExampleAnalyzer/ExampleIssue.cs @@ -0,0 +1,79 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Text.RegularExpressions; +using Tools.Common.Issues; +using System.Collections.Generic; + +namespace StaticAnalysis.ExampleAnalyzer +{ + public class ExampleIssue : IReportRecord + { + public string Module { get; set; } + public string Cmdlet { get; set; } + public int Example { get; set; } + public string RuleName { get; set; } + public string Extent { get; set; } + public int ProblemId { get; set; } + public int Severity { get; set; } + public string Description { get; set; } + public string Remediation { get; set; } + public string PrintHeaders() + { + return "\"Module\",\"Cmdlet\",\"Example\",\"RuleName\",\"ProblemId\",\"Severity\",\"Description\",\"Extent\",\"Remediation\""; + } + + public string FormatRecord() + { + return string.Format("\"{0}\",\"{1}\",\"{2}\",\"{3}\",\"{4}\",\"{5}\",\"{6}\",\"{7}\",\"{8}\"", + Module, Cmdlet, Example, RuleName, ProblemId, Severity, Description, Extent, Remediation); + } + + public bool Match(IReportRecord other) + { + var result = false; + var record = other as ExampleIssue; + if (record != null) + { + result = (record.Module == Module)&& + (record.Cmdlet == Cmdlet)&& + (record.Example == Example)&& + (record.ProblemId == ProblemId)&& + (record.Description == Description); + } + return result; + } + + public IReportRecord Parse(string line) + { + var matcher = "\"([^\"]*)\",\"([^\"]*)\",\"([^\"]*)\",\"([^\"]*)\",\"([^\"]*)\",\"([^\"]*)\",\"([^\"]*)\",\"([^\"]*)\",\"([^\"]*)\""; + var match = Regex.Match(line, matcher); + if (!match.Success || match.Groups.Count < 10) + { + throw new InvalidOperationException(string.Format("Could not parse '{0}' as ExampleIssue record", line)); + } + Module = match.Groups[1].Value; + Cmdlet = match.Groups[2].Value; + Example = int.Parse(match.Groups[3].Value); + RuleName = match.Groups[4].Value; + ProblemId = int.Parse(match.Groups[5].Value); + Severity = int.Parse(match.Groups[6].Value); + Description = match.Groups[7].Value; + Extent = match.Groups[8].Value; + Remediation = match.Groups[9].Value; + return this; + } + } +} \ No newline at end of file diff --git a/tools/StaticAnalysis/ExampleAnalyzer/Measure-MarkdownOrScript.ps1 b/tools/StaticAnalysis/ExampleAnalyzer/Measure-MarkdownOrScript.ps1 new file mode 100644 index 000000000000..c3938a59d5fc --- /dev/null +++ b/tools/StaticAnalysis/ExampleAnalyzer/Measure-MarkdownOrScript.ps1 @@ -0,0 +1,100 @@ +<# + .SYNOPSIS + The script to find examples in ".md" and analyze the examples by custom rules. + .NOTES + File Name: Measure-MarkdownOrScript.ps1 +#> + +#Requires -Modules PSScriptAnalyzer + +[CmdletBinding(DefaultParameterSetName = "Markdown")] +param ( + [Parameter(Mandatory, HelpMessage = "Markdown searching paths. Empty for current path. Supports wildcard.", ParameterSetName = "Markdown")] + [AllowEmptyString()] + [string[]]$MarkdownPaths, + [Parameter(Mandatory, HelpMessage = "PowerShell scripts searching paths. Empty for current path. Supports wildcard.", ParameterSetName = "Script")] + [AllowEmptyString()] + [string[]]$ScriptPaths, + [Parameter(HelpMessage = "PSScriptAnalyzer custom rules paths. Empty for current path. Supports wildcard.")] + [string[]]$RulePaths, + [switch]$Recurse, + [switch]$IncludeDefaultRules, + [string]$OutputFolder = "$PSScriptRoot\..\..\..\artifacts\StaticAnalysisResults\ExampleAnalysis", + [Parameter(ParameterSetName = "Markdown")] + [switch]$AnalyzeScriptsInFile, + [Parameter(ParameterSetName = "Markdown")] + [switch]$OutputScriptsInFile, + [switch]$OutputResultsByModule, + [switch]$CleanScripts +) + +. $PSScriptRoot\utils.ps1 + +if ($PSCmdlet.ParameterSetName -eq "Markdown") { + $scaleTable = @() + $missingTable = @() + $deletePromptAndSeparateOutputTable = @() +} +$analysisResultsTable = @() + +# Clean caches, remove files in "output" folder +if ($OutputScriptsInFile.IsPresent) { + Remove-Item $OutputFolder\TempScript.ps1 -ErrorAction SilentlyContinue + Remove-Item $OutputFolder\*.csv -Recurse -ErrorAction SilentlyContinue + Remove-Item $PSScriptRoot\..\..\..\artifacts\StaticAnalysisResults\ExampleIssues.csv -ErrorAction SilentlyContinue + Remove-Item $OutputFolder -ErrorAction SilentlyContinue +} + + +# Find examples in "help\*.md", output ".ps1" +if ($PSCmdlet.ParameterSetName -eq "Markdown") { + $null = New-Item -ItemType Directory -Path $OutputFolder -ErrorAction SilentlyContinue + $null = New-Item -ItemType File $OutputFolder\TempScript.ps1 + $MarkdownPath = Get-Content $MarkdownPaths + foreach($_ in Get-ChildItem $MarkdownPath){ + # Filter the .md of overview in "\help\" + if ((Get-Item -Path $_.FullName).Directory.Name -eq "help" -and $_.FullName -cmatch ".*\.md" -and $_.BaseName -cmatch "^([A-Z][a-z]+)+-([A-Z][a-z0-9]*)+$") { + Write-Output "Searching in file $($_.FullName) ..." + $module = (Get-Item -Path $_.FullName).Directory.Parent.Name + $cmdlet = $_.BaseName + $result = Measure-SectionMissingAndOutputScript $module $cmdlet $_.FullName ` + -OutputScriptsInFile:$OutputScriptsInFile.IsPresent ` + -OutputFolder $OutputFolder + $scaleTable += $result.Scale + $missingTable += $result.Missing + $deletePromptAndSeparateOutputTable += $result.DeletePromptAndSeparateOutput + $analysisResultsTable += $result.Errors + } + } + if ($AnalyzeScriptsInFile.IsPresent) { + $ScriptPaths = "$OutputFolder\TempScript.ps1" + } + # Summarize searching results + if($scaleTable){ + $scaleTable | Where-Object {$_ -ne $null} | Export-Csv "$OutputFolder\Scale.csv" -NoTypeInformation + } + if($missingTable){ + $missingTable | Where-Object {$_ -ne $null} | Export-Csv "$OutputFolder\Missing.csv" -NoTypeInformation + } + if($deletePromptAndSeparateOutputTable){ + $deletePromptAndSeparateOutputTable | Where-Object {$_ -ne $null} | Export-Csv "$OutputFolder\DeletingSeparating.csv" -NoTypeInformation + } +} + + +# Analyze scripts +if ($PSCmdlet.ParameterSetName -eq "Script" -or $AnalyzeScriptsInFile.IsPresent) { + # Read and analyze ".ps1" in \ScriptsByExample + Write-Output "Analyzing file ..." + $analysisResultsTable += Get-ScriptAnalyzerResult (Get-Item -Path $ScriptPaths) $RulePaths -IncludeDefaultRules:$IncludeDefaultRules.IsPresent -ErrorAction SilentlyContinue + + # Summarize analysis results, output in Result.csv + if($analysisResultsTable){ + $analysisResultsTable| Where-Object {$_ -ne $null} | Export-Csv "$PSScriptRoot\..\..\..\artifacts\StaticAnalysisResults\ExampleIssues.csv" -NoTypeInformation + } +} + +# Clean caches +if ($CleanScripts.IsPresent) { + Remove-Item $ScriptPaths -Exclude *.csv -Recurse -ErrorAction Continue +} diff --git a/tools/StaticAnalysis/ExampleAnalyzer/utils.ps1 b/tools/StaticAnalysis/ExampleAnalyzer/utils.ps1 new file mode 100644 index 000000000000..3357a74e1afc --- /dev/null +++ b/tools/StaticAnalysis/ExampleAnalyzer/utils.ps1 @@ -0,0 +1,578 @@ +<# + .SYNOPSIS + Tools for Measure-MarkdownOrScript.ps1. + .NOTES + File Name: utils.ps1 + Class: Scale + Missing + DeletePromptAndSeparateOutput + AnalysisOutput + Functions: Get-ExamplesDetailsFromMd + ExceptionRecord + Measure-SectionMissingAndOutputScript + Get-ScriptAnalyzerResult +#> + +$SYNOPSIS_HEADING = "## SYNOPSIS" +$SYNTAX_HEADING = "## SYNTAX" +$DESCRIPTION_HEADING = "## DESCRIPTION" +$EXAMPLES_HEADING = "## EXAMPLES" +$PARAMETERS_HEADING = "## PARAMETERS" +$SINGLE_EXAMPLE_HEADING_REGEX = "\n###\s*" +$SINGLE_EXAMPLE_TITLE_HEADING_REGEX = "$SINGLE_EXAMPLE_HEADING_REGEX[^\n]?(Example)?\s*[0-9]*:?\s*({{)?.*(}})?" +$CODE_BLOCK_REGEX = "``````(powershell)?\s*\n(.*\n)*?\s*``````" +$OUTPUT_BLOCK_REGEX = "``````output\s*\n(.*\n)*?\s*``````" + +class Scale { + [string]$Module + [string]$Cmdlet + [int]$Examples +} + +class Missing { + [string]$Module + [string]$Cmdlet + [int]$MissingSynopsis + [int]$MissingDescription + [int]$MissingExampleTitle + [int]$MissingExampleCode + [int]$MissingExampleOutput + [int]$MissingExampleDescription +} + +class DeletePromptAndSeparateOutput { + [string]$Module + [string]$Cmdlet + [int]$NeedDeleting + [int]$NeedSplitting +} + +class AnalysisOutput{ + [string]$Module + [string]$Cmdlet + [int]$Example + [string]$RuleName + [int]$ProblemID + [int]$Severity + [string]$Description + [string]$Extent + [String]$Remediation +} + +<# + .SYNOPSIS + Get examples details from ".md". + .DESCRIPTION + Splits title, code, output, description according to regular expression. +#> +function Get-ExamplesDetailsFromMd { + param ( + [string]$MarkdownPath + ) + + $fileContent = Get-Content $MarkdownPath -Raw + $indexOfExamples = $fileContent.IndexOf($EXAMPLES_HEADING) + $indexOfParameters = $fileContent.IndexOf($PARAMETERS_HEADING) + + $exampleNumber = 0 + $examplesProperties = @() + $examplesContent = $fileContent.Substring($indexOfExamples, $indexOfParameters - $indexOfExamples) + $examplesTitles = ($examplesContent | Select-String -Pattern $SINGLE_EXAMPLE_TITLE_HEADING_REGEX -AllMatches).Matches + + for($exampleNumber = 0; $exampleNumber -le $examplesTitles.length - 1; $exampleNumber++){ + if($exampleNumber -ne $examplesTitles.length - 1){ + $exampleContent = ($examplesContent -split $examplesTitles[$exampleNumber].Value)[1] + $exampleContent = ($exampleContent -split $examplesTitles[$exampleNumber + 1].Value)[0] + } + else{ + $exampleContent = ($examplesContent -split $examplesTitles[$exampleNumber].Value)[1] + } + # Skip the autogenerated example + if($exampleContent -match "\(autogenerated\)"){ + continue + } + $exampleTitle = ($examplesTitles[$exampleNumber].Value -split $SINGLE_EXAMPLE_HEADING_REGEX)[1].Trim() + $exampleCodes = @() + $exampleOutputs = @() + $exampleDescriptions = @() + + $exampleCodeBlocks = ($exampleContent | Select-String -Pattern $CODE_BLOCK_REGEX -AllMatches).Matches + $exampleOutputBlocks = ($exampleContent | Select-String -Pattern $OUTPUT_BLOCK_REGEX -AllMatches).Matches + if ($exampleCodeBlocks.Count -eq 0) { + $description = $exampleContent.Trim() + if ($description -ne "") { + $exampleDescriptions += $description + } + } + else { + # From the start to the start of the first codeblock is example description. + $description = $exampleContent.SubString(0, $exampleCodeBlocks[0].Index).Trim() + if ($description -ne "") { + $exampleDescriptions += $description + } + + # If there is no ```output``` split codelines and outputlines + if ($exampleOutputBlocks.Count -eq 0) { + foreach ($exampleCodeBlock in $exampleCodeBlocks) { + $codeRegex = "\n(([A-Za-z \t])*(PS|[A-Za-z]:)(\w|[\\/\[\].\- ])*(>|>)+( PS)*)*[ \t]*((([A-Za-z]\w+-[A-Za-z]\w+\b(.ps1)?(?!(-| +\w)))|(" + + "(@?\((?>\((?)|[^\(\)]+|\)(?<-pair>))*(?(pair)(?!))\) *[|.-] *\w)|" + + "(\[(?>\[(?)|[^\[\]]+|\](?<-pair>))*(?(pair)(?!))\]\$)|" + + "(@{(?>{(?)|[^{}]+|}(?<-pair>))*(?(pair)(?!))})|" + + "('(?>'(?)|[^']+|'(?<-pair>))*(?(pair)(?!))' *[|.-] *\w)|" + + "((?(?)|[\s\S]|(?))*(?(pair)(?!))(?@()\[\]{},.+*/|\\&!?%#]*[``|][ \t]*(\n|\r\n)?)*([\w-~``'`"$= \t:;<>@()\[\]{},.+*/|\\&!?%#]*(?=\n|\r\n|#)))" + $exampleCodeLines = ($exampleCodeBlock.Value | Select-String -Pattern $codeRegex -CaseSensitive -AllMatches).Matches + if ($exampleCodeLines.Count -eq 0) { + $exampleCodes = @() + $exampleOutputs = @() + } + else { + for ($i = 0; $i -lt $exampleCodeLines.Count; $i++) { + # If a codeline contains " :", it's not a codeline but an output line of "Format-List". + if ($exampleCodeLines[$i].Value -notmatch " : *\w") { + # If a codeline ends with "`", "\r", or "\n", it should end at the last "`". + $lastCharacter = $exampleCodeLines[$i].Value.Substring($exampleCodeLines[$i].Value.Length - 1, 1) + if ($lastCharacter -eq "``" -or $lastCharacter -eq "`r" -or $lastCharacter -eq "`n") { + $exampleCodes += $exampleCodeLines[$i].Value.Substring(0, $exampleCodeLines[$i].Value.LastIndexOf("``")).Trim() + } + else { + $exampleCodes += $exampleCodeLines[$i].Value.Trim() + } + + # Content before the first codeline, between codelines, and after the last codeline is output. + if ($i -eq 0) { + $startIndex = $exampleCodeBlock.Value.IndexOf("`n") + $output = $exampleCodeBlock.Value.Substring($startIndex, $exampleCodeLines[$i].Index - $startIndex).Trim() + if ($output -ne "") { + $exampleOutputs += $output + } + } + $startIndex = $exampleCodeLines[$i].Index + $exampleCodeLines[$i].Length + if ($i -lt $exampleCodeLines.Count - 1) { + $nextStartIndex = $exampleCodeLines[$i + 1].Index + } + else { + $nextStartIndex = $exampleCodeBlock.Value.LastIndexOf("`n") + } + # If an output line starts with "-", it's an incomplete codeline, but it should still be added to output. + $output = $exampleCodeBlock.Value.Substring($startIndex, $nextStartIndex - $startIndex).Trim() + if ($output -match "^-+\w") { + $exampleOutputs += $output + } + elseif ($output -ne "") { + $exampleOutputs += $output + } + } + } + } + } + } + # If there is ```output``` + else { + # Extract code from the first "\n" to the last "\n" + foreach ($exampleCodeBlock in $exampleCodeBlocks) { + $code = $exampleCodeBlock.Value.Substring($exampleCodeBlock.Value.IndexOf("`n"), $exampleCodeBlock.Value.LastIndexOf("`n") - $exampleCodeBlock.Value.IndexOf("`n")).Trim() + if ($code -ne "") { + $exampleCodes += $code + } + } + # Extract output from the first "\n" to the last "\n" + foreach ($exampleOutputBlock in $exampleOutputBlocks) { + $output = $exampleOutputBlock.Value.Substring($exampleOutputBlock.Value.IndexOf("`n"), $exampleOutputBlock.Value.LastIndexOf("`n") - $exampleOutputBlock.Value.IndexOf("`n")).Trim() + if ($output -ne "") { + $exampleOutputs += $output + } + } + } + + # From the end of the last codeblock to the end is example description. + if($null -ne $exampleOutputBlocks){ + $description = $exampleContent.SubString($exampleOutputBlocks[-1].Index + $exampleOutputBlocks[-1].Length).Trim() + } + else{ + + $description = $exampleContent.SubString($exampleCodeBlocks[-1].Index + $exampleCodeBlocks[-1].Length).Trim() + } + if ($description -ne "") { + $exampleDescriptions += $description + } + } + + $examplesProperties += [PSCustomObject]@{ + Num = $exampleNumber + 1 + Title = $exampleTitle + Codes = $exampleCodes + CodeBlocks = $exampleCodeBlocks + Outputs = $exampleOutputs + OutputBlocks = $exampleOutputBlocks + Description = ([string]$exampleDescriptions).Trim() + } + } + + return $examplesProperties +} +<# + .SYNOPSIS + Except the suppressed records +#> +function ExceptionRecord{ + param( + [AnalysisOutput[]]$records + ) + $exceptionPaths = "$PSScriptRoot\..\..\..\tools\StaticAnalysis\Exceptions" + $results = @() + foreach($record in $records){ + $needAdd = $true + $exceptionPath = Join-Path -Path $exceptionPaths -ChildPath "Az.$($record.Module)" -AdditionalChildPath "ExampleIssues.csv" + if(Test-Path -Path $exceptionPath){ + $exceptionContents = Import-Csv -Path $exceptionPath + foreach($exceptionContent in $exceptionContents) { + if($exceptionContent.Module -eq $record.Module -and $exceptionContent.Cmdlet -eq $record.Cmdlet -and $exceptionContent.Example -eq $record.Example -and $exceptionContent.Description -eq $record.Description){ + $needAdd = $false + break + } + } + } + if($needAdd){ + $results += $record + } + } + return $results +} + +<# + .SYNOPSIS + Tests whether the script is integral, outputs examples in ".md" to "TempScript.ps1" + and records the Scale, Missing, DeletePromptAndSeparateOutput class. +#> +function Measure-SectionMissingAndOutputScript { + param ( + [string]$Module, + [string]$Cmdlet, + [string]$MarkdownPath, + [switch]$OutputScriptsInFile, + [string]$OutputFolder + ) + $results = @() + $missingSeverity = 2 + + $fileContent = Get-Content $MarkdownPath -Raw + + $indexOfSynopsis = $fileContent.IndexOf($SYNOPSIS_HEADING) + $indexOfSyntax = $fileContent.IndexOf($SYNTAX_HEADING) + $indexOfDescription = $fileContent.IndexOf($DESCRIPTION_HEADING) + $indexOfExamples = $fileContent.IndexOf($EXAMPLES_HEADING) + + $exampleNumber = 0 + $missingSynopsis = 0 + $missingDescription = 0 + $missingExampleTitle = 0 + $missingExampleCode = 0 + $missingExampleOutput = 0 + $missingExampleDescription = 0 + $needDeleting = 0 + $needSplitting = 0 + + # If Synopsis section exists + if ($indexOfSynopsis -ne -1) { + $synopsisContent = $fileContent.Substring($indexOfSynopsis + $SYNOPSIS_HEADING.Length, $indexOfSyntax - ($indexOfSynopsis + $SYNOPSIS_HEADING.Length)) + if ($synopsisContent.Trim() -eq "") { + $missingSynopsis = 1 + } + else { + $missingSynopsis = ($synopsisContent | Select-String -Pattern "{{[A-Za-z ]*}}").Count + } + } + else { + $missingSynopsis = 1 + } + if($missingSynopsis -ne 0){ + $result = [AnalysisOutput]@{ + Module = $Module + Cmdlet = $Cmdlet + Example = "" + Description = "Synopsis is missing." + RuleName = "MissingSynopsis" + Severity = $missingSeverity + Extent = "$Module\help\$Cmdlet.md" + ProblemID = 5040 + Remediation = "Add Synopsis. Remove any placeholders." + } + $results += $result + } + + # If Description section exists + if ($indexOfDescription -ne -1) { + $descriptionContent = $fileContent.Substring($indexOfDescription + $DESCRIPTION_HEADING.Length, $indexOfExamples - ($indexOfDescription + $DESCRIPTION_HEADING.Length)) + if ($descriptionContent.Trim() -eq "") { + $missingDescription = 1 + } + else { + $missingDescription = ($descriptionContent | Select-String -Pattern "{{[A-Za-z ]*}}").Count + } + } + else { + $missingDescription = 1 + } + if($missingDescription -ne 0){ + $result = [AnalysisOutput]@{ + Module = $Module + Cmdlet = $Cmdlet + Example = "" + Description = "Description is missing." + RuleName = "MissingDescription" + Severity = $missingSeverity + Extent = "$Module\help\$Cmdlet.md" + ProblemID = 5041 + Remediation = "Add Description. Remove any placeholders." + } + $results += $result + } + + $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." + } + $results += $result + } + else { + foreach ($exampleDetails in $examplesDetails) { + $exampleNumber++ + $_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 + $_missingExampleDescription = ($exampleDetails.Description | Select-String -Pattern "{{[A-Za-z ]*}}").Count + $_needDeleting = ($exampleDetails.CodeBlocks | Select-String -Pattern "\n([A-Za-z \t\\:>])*(PS|[A-Za-z]:)(\w|[\\/\[\].\- ])*(>|>)+( PS)*[ \t]*" -CaseSensitive).Count + + ($exampleDetails.CodeBlocks | Select-String -Pattern "(?<=[A-Za-z]\w+-[A-Za-z]\w+)\.ps1" -CaseSensitive).Count + switch ($exampleDetails) { + {$exampleDetails.Title -eq "" -or $_missingExampleTitle -ne 0} { + $missingExampleTitle ++ + $result = [AnalysisOutput]@{ + Module = $Module + Cmdlet = $Cmdlet + Example = $exampleDetails.Num + Description = "Title of the example is missing." + RuleName = "MissingExampleTitle" + Severity = $missingSeverity + Extent = "$Module\help\$Cmdlet.md" + ProblemID = 5043 + Remediation = "Add title for the example. Remove any placeholders." + } + $results += $result + } + {$exampleDetails.Codes.Count -eq 0 -or $_missingExampleCode -ne 0} { + $missingExampleCode++ + $result = [AnalysisOutput]@{ + Module = $Module + Cmdlet = $Cmdlet + Example = $exampleDetails.Num + Description = "Code of the example is missing." + RuleName = "MissingExampleCode" + Severity = $missingSeverity + Extent = "$Module\help\$Cmdlet.md" + ProblemID = 5044 + Remediation = "Add code for the example. Remove any placeholders." + } + $results += $result + } + {($exampleDetails.OutputBlocks.Count -ne 0 -and $exampleDetails.Outputs.Count -eq 0) -or $_missingExampleOutput -ne 0} { + $missingExampleOutput++ + $result = [AnalysisOutput]@{ + Module = $Module + Cmdlet = $Cmdlet + Example = $exampleDetails.Num + Description = "Output of the example is missing." + RuleName = "MissingExampleOutput" + Severity = $missingSeverity + Extent = "$Module\help\$Cmdlet.md" + ProblemID = 5045 + Remediation = "Add output for the example. Remove any placeholders." + } + $results += $result + } + {$exampleDetails.OutputBlocks.Count -eq 0 -and $exampleDetails.Outputs.Count -ne 0} { + $needSplitting++ + $result = [AnalysisOutput]@{ + Module = $Module + Cmdlet = $Cmdlet + Example = $exampleDetails.Num + Description = "The output need to be split from example." + RuleName = "NeedSplitting" + Severity = $missingSeverity + Extent = "$Module\help\$Cmdlet.md" + ProblemID = 5051 + Remediation = "Split output from example." + } + $results += $result + } + {$exampleDetails.Description -eq "" -or $_missingExampleDescription -ne 0} { + $missingExampleDescription++ + $result = [AnalysisOutput]@{ + Module = $Module + Cmdlet = $Cmdlet + Example = $exampleDetails.Num + Description = "Description of the example is missing." + RuleName = "MissingExampleDescription" + Severity = $missingSeverity + Extent = "$Module\help\$Cmdlet.md" + ProblemID = 5046 + Remediation = "Add description for the example. Remove any placeholders." + } + $results += $result + } + {$_needDeleting -ne 0}{ + $needDeleting++ + $result = [AnalysisOutput]@{ + Module = $Module + Cmdlet = $Cmdlet + Example = $exampleDetails.Num + Description = "The prompt of example need to be deleted." + RuleName = "NeedDeleting" + Severity = $missingSeverity + Extent = "$Module\help\$Cmdlet.md" + ProblemID = 5051 + Remediation = "Delete the prompt of example." + } + $results += $result + } + } + + # Delete prompts + $exampleCodes = $exampleDetails.Codes + for ($i = $exampleCodes.Count - 1; $i -ge 0; $i--) { + $newCode = $exampleDetails.Codes[$i] -replace "([A-Za-z \t\\:>])*(PS|[A-Za-z]:)(\w|[\\/\[\].\- ])*(>|>)+( PS)*[ \t]*", "" + $newCode = $newCode -replace "(?<=[A-Za-z]\w+-[A-Za-z]\w+)\.ps1", "" + $exampleCodes[$i] = $newCode + } + + # Output example codes to "TempScript.ps1" + if ($OutputScriptsInFile.IsPresent) { + $cmdletExamplesScriptPath = "$OutputFolder\TempScript.ps1" + $functionHead = "function $Module-$Cmdlet-$exampleNumber{" + Add-Content -Path (Get-Item $cmdletExamplesScriptPath).FullName -Value $functionHead + $exampleCodes = $exampleCodes -join "`n" + Add-Content -Path (Get-Item $cmdletExamplesScriptPath).FullName -Value $exampleCodes + $functionTail = "}`n" + Add-Content -Path (Get-Item $cmdletExamplesScriptPath).FullName -Value $functionTail + } + } + } + + # ScaleTable + $examples = $examplesDetails.Count + $scale = [Scale]@{ + Module = $module + Cmdlet = $cmdlet + Examples = $examples + } + + # MissingTable + if ($missingSynopsis -ne 0 -or $missingDescription -ne 0 -or $missingExampleTitle -ne 0 -or $missingExampleCode -ne 0 -or $missingExampleOutput -ne 0 -or $missingExampleDescription -ne 0) { + $missing = [Missing]@{ + Module = $module + Cmdlet = $cmdlet + MissingSynopsis = $missingSynopsis + MissingDescription = $missingDescription + MissingExampleTitle = $missingExampleTitle + MissingExampleCode = $missingExampleCode + MissingExampleOutput = $missingExampleOutput + MissingExampleDescription = $missingExampleDescription + } + } + + # DeletePromptAndSeparateOutputTable + if ($needDeleting -ne 0 -or $needSplitting -ne 0) { + $deletePromptAndSeparateOutput = [DeletePromptAndSeparateOutput]@{ + Module = $module + Cmdlet = $cmdlet + NeedDeleting = $needDeleting + NeedSplitting = $needSplitting + } + } + + # Except the suppressed records + $results = ExceptionRecord $results + + return @{ + Scale = $scale + Missing = $missing + DeletePromptAndSeparateOutput = $deletePromptAndSeparateOutput + Errors = $results + } +} + +<# + .SYNOPSIS + Invoke PSScriptAnalyzer with custom rules, return the error set. +#> +function Get-ScriptAnalyzerResult { + param ( + [string]$ScriptPath, + [Parameter(Mandatory, HelpMessage = "PSScriptAnalyzer custom rules path. Supports wildcard.")] + [string[]]$RulePath, + [switch]$IncludeDefaultRules + ) + + # Validate script file exists. + if (!(Test-Path $ScriptPath -PathType Leaf)) { + throw "Cannot find cached script file '$ScriptPath'." + } + + # Invoke PSScriptAnalyzer : input scriptblock, output error set in $result with property: RuleName, Message, Extent + if ($null -eq $RulePath) { + $analysisResults = Invoke-ScriptAnalyzer -Path $ScriptPath -IncludeDefaultRules:$IncludeDefaultRules.IsPresent + } + else { + $analysisResults = Invoke-ScriptAnalyzer -Path $ScriptPath -CustomRulePath $RulePath -IncludeDefaultRules:$IncludeDefaultRules.IsPresent + } + $results = @() + foreach($analysisResult in $analysisResults){ + if($analysisResult.Severity -eq "Error"){ + $Severity = 2 + } + elseif($analysisResult.Severity -eq "Warning"){ + $Severity = 3 + } + if($analysisResult.RuleSuppressionID -ge 5000 -and $analysisResult.RuleSuppressionID -le 5199){ + $result = [AnalysisOutput]@{ + Module = ($analysisResult.Message -split "-")[0] + Cmdlet = ($analysisResult.Message -split "-")[1] + "-" + ($analysisResult.Message -split "-")[2] + Example = ($analysisResult.Message -split "-")[3] + RuleName = $analysisResult.RuleName + Description = ($analysisResult.Message -split "@")[1] -replace "`"","`'" + Severity = $Severity + Extent = $analysisResult.Extent -replace "`"","`'" + ProblemID = $analysisResult.RuleSuppressionID + Remediation = ($analysisResult.Message -split "@")[2] -replace "`"","`'" + } + } + else{ + $result = [AnalysisOutput]@{ + RuleName = $analysisResult.RuleName + Description = $analysisResult.Message + Severity = $Severity + Extent = $analysisResult.Extent + Remediation = "Unexpected Error! Please contact the Azure Powershell Team." + } + } + $results += $result + } + #Except the suppressed records + $results = ExceptionRecord $results + + return $results +} \ No newline at end of file diff --git a/tools/StaticAnalysis/IssueChecker/IssueChecker.cs b/tools/StaticAnalysis/IssueChecker/IssueChecker.cs index c8a853947948..70cb8f0a0f2f 100644 --- a/tools/StaticAnalysis/IssueChecker/IssueChecker.cs +++ b/tools/StaticAnalysis/IssueChecker/IssueChecker.cs @@ -25,6 +25,7 @@ using StaticAnalysis.BreakingChangeAnalyzer; using StaticAnalysis.DependencyAnalyzer; using StaticAnalysis.SignatureVerifier; +using StaticAnalysis.ExampleAnalyzer; namespace StaticAnalysis.IssueChecker { @@ -39,6 +40,7 @@ public class IssueChecker : IStaticAnalyzer ("MissingAssemblies.csv", typeof(MissingAssembly).FullName), ("ExtraAssemblies.csv", typeof(ExtraAssembly).FullName), ("SignatureIssues.csv", typeof(SignatureIssue).FullName), + ("ExampleIssues.csv", typeof(ExampleIssue).FullName), }; public AnalysisLogger Logger { get; set; } diff --git a/tools/StaticAnalysis/ProblemIDs.cs b/tools/StaticAnalysis/ProblemIDs.cs index 007741e62f51..3b4c4000d2b8 100644 --- a/tools/StaticAnalysis/ProblemIDs.cs +++ b/tools/StaticAnalysis/ProblemIDs.cs @@ -74,4 +74,28 @@ public static class BreakingChangeProblemId public const int ChangedGenericTypeArgument = 3040; public const int DifferentGenericTypeArgumentSize = 3050; } + + //ExampleProblemId is also defined in tools\StaticAnalysis\ExampleAnalyzer, the range is 5000-5199 + public static class ExampleProblemId + { + public const int Invalid_Cmdlet = 5000; + public const int Unknown_Parameter_Set = 5010; + public const int Invalid_Parameter_Name = 5011; + public const int Duplicate_Parameter_Name = 5012; + public const int Unassigned_Parameter = 5013; + public const int Unbinded_Parameter_Name = 5014; + public const int MissingSynopsis = 5040; + public const int MissingDescription = 5041; + public const int MissingExample = 5042; + public const int MissingExampleTitle = 5043; + public const int MissingExampleCode = 5044; + public const int MissingExampleOutput = 5045; + public const int MissingExampleDescription = 5046; + public const int NeedDeleting = 5050; + public const int NeedSplitting = 5051; + public const int Is_Alias = 5100; + public const int Capitalization_Conventions_Violated = 5101; + public const int Unassigned_Variable = 5110; + public const int Mismatched_Parameter_Value_Type = 5111; + } } diff --git a/tools/StaticAnalysis/Program.cs b/tools/StaticAnalysis/Program.cs index 65a04aad9203..39cf05a5f268 100644 --- a/tools/StaticAnalysis/Program.cs +++ b/tools/StaticAnalysis/Program.cs @@ -38,7 +38,8 @@ public class Program "ExtraAssemblies.csv", "HelpIssues.csv", "MissingAssemblies.csv", - "SignatureIssues.csv" + "SignatureIssues.csv", + "ExampleIssues.csv" }; private static string ExceptionsDirectory { get; set; } diff --git a/tools/StaticAnalysis/ReportRecordFactory.cs b/tools/StaticAnalysis/ReportRecordFactory.cs index fa05a8db38cb..53618559bdcd 100644 --- a/tools/StaticAnalysis/ReportRecordFactory.cs +++ b/tools/StaticAnalysis/ReportRecordFactory.cs @@ -17,6 +17,7 @@ using StaticAnalysis.DependencyAnalyzer; using StaticAnalysis.HelpAnalyzer; using StaticAnalysis.SignatureVerifier; +using StaticAnalysis.ExampleAnalyzer; using System; using System.Collections.Generic; @@ -58,6 +59,10 @@ public static IReportRecord Create(string type) { return new SignatureIssue(); } + if (type.Equals(typeof(ExampleIssue).FullName)) + { + return new ExampleIssue(); + } return null; } diff --git a/tools/VersionController/Program.cs b/tools/VersionController/Program.cs index 7bf0b90f682b..c44f76c9f58a 100644 --- a/tools/VersionController/Program.cs +++ b/tools/VersionController/Program.cs @@ -41,7 +41,8 @@ public class Program "ExtraAssemblies.csv", "HelpIssues.csv", "MissingAssemblies.csv", - "SignatureIssues.csv" + "SignatureIssues.csv", + "ExampleIssues.csv" }; public static void Main(string[] args)