diff --git a/.azure-pipelines-ci/ci.yaml b/.azure-pipelines-ci/ci.yaml deleted file mode 100644 index 7cf89937a..000000000 --- a/.azure-pipelines-ci/ci.yaml +++ /dev/null @@ -1,53 +0,0 @@ -variables: - # Avoid expensive initialization of dotnet cli, see: https://donovanbrown.com/post/Stop-wasting-time-during-NET-Core-builds - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 - -stages: - - stage: Build - jobs: - - job: 'Full_Build' - pool: - vmImage: windows-latest - steps: - - pwsh: | - Import-Module .\tools\appveyor.psm1 - Invoke-AppveyorInstall -SkipPesterInstallation - ./build.ps1 -Configuration 'Release' -All - ./PSCompatibilityCollector/build.ps1 -Configuration 'Release' - displayName: 'Full Build' - - pwsh: | - Write-Host "##vso[artifact.upload containerfolder=out;artifactname=out;]${env:Build_SourcesDirectory}/out" - - stage: Test - jobs: - - job: - strategy: - matrix: - Ubuntu_16_04: - vmImage: ubuntu-16.04 - Ubuntu_18_04: - vmImage: ubuntu-18.04 - macOS_10_14_Mojave: - vmImage: macOS-10.14 - macOS_10_15_Catalina: - vmImage: macOS-10.15 - Windows_Server2016_PowerShell_Core: - vmImage: vs2017-win2016 - Windows_Server2019_PowerShell_Core: - vmImage: windows-2019 - pool: - vmImage: $[ variables['vmImage'] ] - steps: - - template: templates/test-pwsh.yaml - - job: - strategy: - matrix: - Windows_Server2016_PowerShell_5_1: - vmImage: vs2017-win2016 - pwsh: false - Windows_Server2019_PowerShell_5_1: - vmImage: windows-2019 - pwsh: false - pool: - vmImage: $[ variables['vmImage'] ] - steps: - - template: templates/test-powershell.yaml diff --git a/.azure-pipelines-ci/templates/test-powershell.yaml b/.azure-pipelines-ci/templates/test-powershell.yaml deleted file mode 100644 index e832f04e4..000000000 --- a/.azure-pipelines-ci/templates/test-powershell.yaml +++ /dev/null @@ -1,19 +0,0 @@ -steps: -- task: DownloadPipelineArtifact@2 - displayName: 'Download Pipeline Artifact: out Folder' - inputs: - artifactName: out - targetPath: '$(Build.SourcesDirectory)/out' -- task: PowerShell@2 - displayName: 'Test' - inputs: - targetType: inline - pwsh: false - script: | - Import-Module .\tools\appveyor.psm1 - Invoke-AppveyorTest -CheckoutPath $env:BUILD_SOURCESDIRECTORY -- task: PublishTestResults@2 - inputs: - testRunner: NUnit - testResultsFiles: 'TestResults.xml' - condition: succeededOrFailed() diff --git a/.azure-pipelines-ci/templates/test-pwsh.yaml b/.azure-pipelines-ci/templates/test-pwsh.yaml deleted file mode 100644 index 2661b2157..000000000 --- a/.azure-pipelines-ci/templates/test-pwsh.yaml +++ /dev/null @@ -1,19 +0,0 @@ -steps: -- task: DownloadPipelineArtifact@2 - displayName: 'Download Pipeline Artifact: out Folder' - inputs: - artifactName: out - targetPath: '$(Build.SourcesDirectory)/out' -- task: PowerShell@2 - displayName: 'Test' - inputs: - targetType: inline - pwsh: true - script: | - Import-Module .\tools\appveyor.psm1 - Invoke-AppveyorTest -CheckoutPath $env:BUILD_SOURCESDIRECTORY -- task: PublishTestResults@2 - inputs: - testRunner: NUnit - testResultsFiles: 'TestResults.xml' - condition: succeededOrFailed() diff --git a/.config/tsaoptions.json b/.config/tsaoptions.json new file mode 100644 index 000000000..a0887d850 --- /dev/null +++ b/.config/tsaoptions.json @@ -0,0 +1,10 @@ +{ + "instanceUrl": "https://msazure.visualstudio.com/", + "projectName": "One", + "areaPath": "One\\MGMT\\Compute\\Powershell\\Powershell\\PowerShell Core", + "notificationAliases": [ + "andschwa@microsoft.com", + "slee@microsoft.com" + ], + "codebaseName": "PSSA_202403" +} diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 0b85b17b1..cc17c531c 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,31 +1,6 @@ -#------------------------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. -#------------------------------------------------------------------------------------------------------------- +# Licensed under the MIT License. -FROM mcr.microsoft.com/dotnet/core/sdk:3.1 - -# This Dockerfile adds a non-root user with sudo access. Use the "remoteUser" -# property in devcontainer.json to use it. On Linux, the container user's GID/UIDs -# will be updated to match your local UID/GID (when using the dockerFile property). -# See https://aka.ms/vscode-remote/containers/non-root-user for details. - -# Avoid warnings by switching to noninteractive -ENV DEBIAN_FRONTEND=noninteractive - -# Configure apt and install packages -RUN apt-get update \ - && apt-get -y install --no-install-recommends apt-utils dialog 2>&1 \ - # - # Verify git, process tools, lsb-release (common in install instructions for CLIs) installed - && apt-get -y install git iproute2 procps apt-transport-https gnupg2 curl lsb-release \ - # - # Clean up - && apt-get autoremove -y \ - && apt-get clean -y \ - && rm -rf /var/lib/apt/lists/* +FROM mcr.microsoft.com/dotnet/sdk:8.0 -RUN pwsh -c Install-Module platyPS,Pester -Force - -# Switch back to dialog for any ad-hoc use of apt-get -ENV DEBIAN_FRONTEND=dialog +RUN pwsh --command Install-Module platyPS,Pester -Force \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d59606e96..dbbc9c7ee 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,14 +1,16 @@ -// For format details, see https://aka.ms/vscode-remote/devcontainer.json or the definition README at -// https://github.com/microsoft/vscode-dev-containers/tree/master/containers/dotnetcore-3.0 +// For format details, see https://aka.ms/vscode-remote/devcontainer.json { - "name": "C# (.NET Core 3.1)", - "dockerFile": "Dockerfile", - "settings": { - "terminal.integrated.shell.linux": "/usr/bin/pwsh" - }, - "postCreateCommand": "dotnet restore", - "extensions": [ - "ms-dotnettools.csharp", - "ms-vscode.powershell" - ] + "name": "C# (.NET 8.0)", + "dockerFile": "Dockerfile", + "customizations": { + "vscode": { + "settings": { + "terminal.integrated.defaultProfile.linux": "pwsh" + }, + "extensions": [ + "ms-dotnettools.csharp", + "ms-vscode.powershell" + ] + } + } } \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..6313b56c5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..983234361 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,6 @@ +# Default owners +* @andyleejordan @bergmeister + +# Version bumps and documentation updates +Directory.Build.props @sdwheeler @michaeltlombardi +/docs/ @sdwheeler @michaeltlombardi diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..048d20ef0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Report a User documentation issue + url: https://github.com/MicrosoftDocs/PowerShell-Docs-Modules/issues/new/choose + about: Report issues about the user documentation available on learn.microsoft.com. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..f0ba62a3e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: +- package-ecosystem: nuget + directory: "/" + schedule: + interval: daily + time: "13:00" + timezone: America/Los_Angeles + open-pull-requests-limit: 10 + reviewers: + - "JamesWTruher" + - "bergmeister" + ignore: + - dependency-name: "System.Management.Automation" + - dependency-name: "PowerShellStandard.Library" diff --git a/.github/policies/resourceManagement.yml b/.github/policies/resourceManagement.yml new file mode 100644 index 000000000..c5262aa28 --- /dev/null +++ b/.github/policies/resourceManagement.yml @@ -0,0 +1,92 @@ +id: +name: GitOps.PullRequestIssueManagement +description: GitOps.PullRequestIssueManagement primitive +owner: +resource: repository +disabled: false +where: +configuration: + resourceManagementConfiguration: + scheduledSearches: + - description: + frequencies: + - hourly: + hour: 3 + filters: + - isIssue + - isOpen + - hasLabel: + label: Resolution - Duplicate + - noActivitySince: + days: 1 + actions: + - addReply: + reply: This issue has been marked as duplicate and has not had any activity for **1 day**. It will be closed for housekeeping purposes. + - closeIssue + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isOpen + - hasLabel: + label: Resolution - Answered + - noActivitySince: + days: 7 + actions: + - closeIssue + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isOpen + - hasLabel: + label: Resolution - External + - noActivitySince: + days: 7 + actions: + - closeIssue + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isOpen + - hasLabel: + label: Resolution - Fixed + - noActivitySince: + days: 7 + actions: + - closeIssue + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isOpen + - hasLabel: + label: Resolution - Won't Fix + - noActivitySince: + days: 7 + actions: + - closeIssue + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isOpen + - hasLabel: + label: Need Repro Info + - noActivitySince: + days: 14 + - isNotLabeledWith: + label: Issue - Bug + actions: + - addReply: + reply: Closing due to inactivity + - closeIssue + eventResponderTasks: [] +onFailure: +onSuccess: diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml new file mode 100644 index 000000000..5469c54a5 --- /dev/null +++ b/.github/workflows/ci-test.yml @@ -0,0 +1,83 @@ +name: CI Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + ci: + name: pester + strategy: + matrix: + os: [ windows-latest, macos-latest, ubuntu-latest ] + runs-on: ${{ matrix.os }} + env: + DOTNET_NOLOGO: true + DOTNET_GENERATE_ASPNET_CERTIFICATE: false + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dotnet + uses: actions/setup-dotnet@v4 + with: + cache: true + cache-dependency-path: '**/*.csproj' + + - name: Install PSResources + run: ./tools/installPSResources.ps1 + shell: pwsh + + - name: Build + run: ./build.ps1 -Configuration Release -All -Verbose + shell: pwsh + + - name: Package + run: ./build.ps1 -BuildNupkg -Verbose + shell: pwsh + + - name: Test + run: ./build.ps1 -Test -Verbose + shell: pwsh + + - name: Test Windows PowerShell + if: matrix.os == 'windows-latest' + run: | + Install-Module Pester -Scope CurrentUser -Force -SkipPublisherCheck + ./build.ps1 -Test -Verbose + shell: powershell + + - name: Download PowerShell install script + uses: actions/checkout@v4 + with: + repository: PowerShell/PowerShell + path: pwsh + sparse-checkout: tools/install-powershell.ps1 + sparse-checkout-cone-mode: false + + - name: Install preview + continue-on-error: true + run: ./pwsh/tools/install-powershell.ps1 -Preview -Destination ./preview + shell: pwsh + + - name: Test preview + run: | + $PwshPreview = if ($isWindows) { "./preview/pwsh.exe" } else { "./preview/pwsh" } + ./build.ps1 -Test -WithPowerShell:$PwshPreview -Verbose + shell: pwsh + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: PSScriptAnalyzer-package-${{ matrix.os }} + path: out/**/*.nupkg + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: PSScriptAnalyzer-tests-${{ matrix.os }} + path: testResults.xml diff --git a/.gitignore b/.gitignore index 4f674094d..fdf91a4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,223 +1,5 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -build/ -bld/ -[Bb]in/ -[Oo]bj/ - -# Visual Studo 2015 cache/options directory -.vs/ - -# VSCode configuration directory -.vscode/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# .Net Core CLI -project.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding addin-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config - -# Windows Azure Build Output -csx/ -*.build.csdef - -# Windows Store app package directory -AppPackages/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -[Ss]tyle[Cc]op.* -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.pfx -*.publishsettings -node_modules/ -bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -##Our project binplace location -PSScriptAnalyzer/ - -# Vim swap files -*.swp - -# Test result file -TestResults.xml - -# PSCompatibilityCollector module -PSCompatibilityCollector/out/ - -# Folder of build module -out +bin/ +obj/ +/module/ +/out/ +testResults.xml diff --git a/.pipelines/PSScriptAnalyzer-Official.yml b/.pipelines/PSScriptAnalyzer-Official.yml new file mode 100644 index 000000000..abea9ab3c --- /dev/null +++ b/.pipelines/PSScriptAnalyzer-Official.yml @@ -0,0 +1,174 @@ +################################################################################# +# OneBranch Pipelines # +# This pipeline was created by EasyStart from a sample located at: # +# https://aka.ms/obpipelines/easystart/samples # +# Documentation: https://aka.ms/obpipelines # +# Yaml Schema: https://aka.ms/obpipelines/yaml/schema # +# Retail Tasks: https://aka.ms/obpipelines/tasks # +# Support: https://aka.ms/onebranchsup # +################################################################################# + +trigger: +- main + +schedules: +- cron: '20 16 * * 4' + displayName: Weekly CodeQL + branches: + include: + - main + always: true + +parameters: +- name: debug + displayName: Enable debug output + type: boolean + default: false + +variables: + system.debug: ${{ parameters.debug }} + BuildConfiguration: Release + WindowsContainerImage: onebranch.azurecr.io/windows/ltsc2022/vse2022:latest + DOTNET_NOLOGO: true + DOTNET_GENERATE_ASPNET_CERTIFICATE: false + +resources: + repositories: + - repository: templates + type: git + name: OneBranch.Pipelines/GovernedTemplates + ref: refs/heads/main + +extends: + # https://aka.ms/obpipelines/templates + template: v2/OneBranch.Official.CrossPlat.yml@templates + parameters: + globalSdl: # https://aka.ms/obpipelines/sdl + asyncSdl: + enabled: true + forStages: [build] + featureFlags: + EnableCDPxPAT: false + WindowsHostVersion: + Version: 2022 + Network: KS3 + release: + category: NonAzure + stages: + - stage: build + jobs: + - job: main + displayName: Build package + pool: + type: windows + variables: + ob_outputDirectory: $(Build.SourcesDirectory)/out + steps: + - pwsh: | + [xml]$xml = Get-Content ./Directory.Build.props + $version = $xml.Project.PropertyGroup.ModuleVersion + Write-Output "##vso[task.setvariable variable=version;isOutput=true]$version" + name: package + displayName: Get version from project properties + - task: onebranch.pipeline.version@1 + displayName: Set OneBranch version + inputs: + system: Custom + customVersion: $(package.version) + - task: UseDotNet@2 + displayName: Use .NET SDK + inputs: + packageType: sdk + useGlobalJson: true + - pwsh: ./tools/installPSResources.ps1 -PSRepository CFS + displayName: Install PSResources + - pwsh: ./build.ps1 -Configuration Release -All + displayName: Build + - task: onebranch.pipeline.signing@1 + displayName: Sign 1st-party files + inputs: + command: sign + signing_profile: external_distribution + search_root: $(Build.SourcesDirectory)/out + files_to_sign: | + **/Microsoft.*.dll; + **/*.psd1; + **/*.psm1; + **/*.ps1xml; + - task: onebranch.pipeline.signing@1 + displayName: Sign 3rd-party files + inputs: + command: sign + signing_profile: 135020002 + search_root: $(Build.SourcesDirectory)/out + files_to_sign: | + **/Newtonsoft.Json.dll; + **/Pluralize.NET.dll; + - pwsh: ./build.ps1 -BuildNupkg + displayName: Package module + - task: onebranch.pipeline.signing@1 + displayName: Sign NuGet package + inputs: + command: sign + signing_profile: external_distribution + search_root: $(Build.SourcesDirectory)/out + files_to_sign: | + *.nupkg + - stage: release + dependsOn: build + condition: eq(variables['Build.Reason'], 'Manual') + variables: + ob_release_environment: Production + version: $[ stageDependencies.build.main.outputs['package.version'] ] + jobs: + - job: github + displayName: Publish draft to GitHub + pool: + type: release + templateContext: + inputs: + - input: pipelineArtifact + artifactName: drop_build_main + steps: + - task: GitHubRelease@1 + displayName: Create GitHub release + inputs: + gitHubConnection: GitHub + repositoryName: PowerShell/PSScriptAnalyzer + target: main + assets: $(Pipeline.Workspace)/PSScriptAnalyzer.$(version).nupkg + tagSource: userSpecifiedTag + tag: $(version) + isDraft: true + addChangeLog: false + releaseNotesSource: inline + releaseNotesInline: "" + - job: validation + displayName: Manual validation + pool: + type: server + timeoutInMinutes: 1440 + steps: + - task: ManualValidation@0 + displayName: Wait 24 hours for validation + inputs: + notifyUsers: $(Build.RequestedForEmail) + instructions: Please validate the release and then publish it! + timeoutInMinutes: 1440 + - job: publish + dependsOn: validation + displayName: Publish to PowerShell Gallery + pool: + type: release + templateContext: + inputs: + - input: pipelineArtifact + artifactName: drop_build_main + steps: + - task: NuGetCommand@2 + displayName: Publish module to PowerShell Gallery + inputs: + command: push + packagesToPush: $(Pipeline.Workspace)/PSScriptAnalyzer.$(version).nupkg + nuGetFeedType: external + publishFeedCredentials: PowerShellGallery diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 098ed86e6..bf1b08715 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,7 +3,6 @@ // for the documentation about the extensions.json format "recommendations": [ "ms-vscode.PowerShell", - "ms-dotnettools.csharp", - "ms-vscode-remote.remote-containers" + "ms-dotnettools.csharp" ] -} \ No newline at end of file +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 9beb418df..a352eafc2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,18 +1,55 @@ { "version": "0.2.0", "configurations": [ - { - "name": ".NET Core Launch (console)", - "type": "coreclr", + "type": "PowerShell", + "request": "launch", + "name": "Build", + "script": "./build.ps1", + "args": [ + "" + ], + "cwd": "${workspaceFolder}", + "createTemporaryIntegratedConsole": true + }, + { + "type": "PowerShell", + "request": "launch", + "name": "Build & Import PSSA", + "script": "./build.ps1; Import-Module ./out/PSScriptAnalyzer/*/PSScriptAnalyzer.psd1", + "args": [ + "" + ], + "cwd": "${workspaceFolder}", + "createTemporaryIntegratedConsole": true + }, + { + "type": "PowerShell", "request": "launch", - "preLaunchTask": "build", - "program": "${workspaceRoot}/bin/Debug//", - "args": [], - "cwd": "${workspaceRoot}", - "externalConsole": false, - "stopAtEntry": false, - "internalConsoleOptions": "openOnSessionStart" + "name": "Build & Run all tests", + "script": "./build.ps1; ./build.ps1 -Test", + "args": [ + "" + ], + "cwd": "${workspaceFolder}", + "createTemporaryIntegratedConsole": true + }, + { + "type": "PowerShell", + "request": "launch", + "name": "Run current Pester file with PSScriptAnalyzer development build", + "script": "Import-Module ./out/PSScriptAnalyzer/*/PSScriptAnalyzer.psd1; Invoke-Pester -Path '${file}'; $PID", + "args": [ + "" + ], + "cwd": "${workspaceFolder}", + "createTemporaryIntegratedConsole": true + }, + { + "name": "PowerShell Attach to Host Process", + "type": "PowerShell", + "request": "attach", + "runspaceId": 1 }, { "name": ".NET Core Attach", diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 9e8067f3b..76352e7c7 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,5 +1,333 @@ # CHANGELOG +## [1.24.0](https://github.com/PowerShell/PSScriptAnalyzer/releases/tag/1.24.0) + +### What's Changed +#### Breaking Changes + +Minimum required PowerShell version raised from 3 to 5.1 +* Drop v3 and v4 support from build by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2081 + +#### New Features + +* Add new options (enabled by default) to formatting rule `UseCorrectCasing` to also correct operators, keywords and commands - Add UseConsistentCasing by @Jaykul in https://github.com/PowerShell/PSScriptAnalyzer/pull/1704 + +#### Enhancements + +* PSAlignAssignmentStatement: Ignore hashtables with a single key-value pair by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/1986 +* Use `RequiredResource` hashtable to specify PowerShell module versions by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2053 +* Set exit code of `Invoke-ScriptAnalyzer -EnableExit` to total number of diagnostics (#2054) by @MatejKafka in https://github.com/PowerShell/PSScriptAnalyzer/pull/2055 +* PSAvoidAssignmentToAutomaticVariable: Ignore when a Parameter has an Attribute that contains a Variable expression by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/1988 +* Trim unnecessary trailing spaces from string resources in Strings.resx by @XPlantefeve in https://github.com/PowerShell/PSScriptAnalyzer/pull/1972 +* Do not print summary repeatedly for each logger by @MatejKafka in https://github.com/PowerShell/PSScriptAnalyzer/pull/2058 +* Make Settings type detection more robust by @Tadas in https://github.com/PowerShell/PSScriptAnalyzer/pull/1967 +* Add foreach Assignment to AvoidAssignmentToAutomaticVariable by @PoshAJ in https://github.com/PowerShell/PSScriptAnalyzer/pull/2021 +* Invoke-ScriptAnalyzer: Stream diagnostics instead of batching by @MatejKafka in https://github.com/PowerShell/PSScriptAnalyzer/pull/2062 +* Invoke-ScriptAnalyzer: Print summary only once per invocation by @MatejKafka in https://github.com/PowerShell/PSScriptAnalyzer/pull/2063 +* Invoke-ScriptAnalyzer: Include parse errors in reported error count by @MatejKafka in https://github.com/PowerShell/PSScriptAnalyzer/pull/2069 +* Add exception message for missing rules by @Tadas in https://github.com/PowerShell/PSScriptAnalyzer/pull/1968 + +#### Bug Fixes + +* Update links in module manifest by @martincostello in https://github.com/PowerShell/PSScriptAnalyzer/pull/2034 +* Fix incorrect `-ReportSummary` Pester test grouping by @MatejKafka in https://github.com/PowerShell/PSScriptAnalyzer/pull/2057 +* Fixed erroneous PSUseDeclaredVarsMoreThanAssignments for some globals variables by @John-Leitch in https://github.com/PowerShell/PSScriptAnalyzer/pull/2013 +* PSReservedParams: Make severity Error instead of Warning by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/1989 +* PSUseConsistentIndentation: Check indentation of lines where first token is a LParen not followed by comment or new line by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/1995 +* PSUseConsistentWhitespace: Correctly fix whitespace between command parameters when parameter value spans multiple lines by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/2064 +* PSAvoidTrailingWhitespace: Rule not applied when using formatter + single character lines with trailing whitespace are truncated by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/1993 +* PSUseConsistentWhitespace: Ignore whitespace between separator and comment by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/2065 +* PSReviewUnusedParameter false positive for ValueFromPipeline by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/2072 +* Change severity of UseCorrectCasing to be Information by @bergmeister in https://github.com/PowerShell/PSScriptAnalyzer/pull/2082 + +#### Process Changes + +* Copy more files to module root by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2037 +* Upgrade to .NET 8 since .NET 6 is past EOL by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2073 +* Use -NoProfile when invoking pwsh in Pester tests by @MatejKafka in https://github.com/PowerShell/PSScriptAnalyzer/pull/2061 +* Add GitHub Actions Ubuntu's dotnet path by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2080 +* Update README.md with recent upgrade to .NET 8 by @bergmeister in https://github.com/PowerShell/PSScriptAnalyzer/pull/2076 +* Update CHANGELOG.MD with 1.23.0 release notes by @bergmeister in https://github.com/PowerShell/PSScriptAnalyzer/pull/2078 +* Bring back Codespaces by @bergmeister in https://github.com/PowerShell/PSScriptAnalyzer/pull/2077 +* Update SMA version to 7.4.7 by @bergmeister in https://github.com/PowerShell/PSScriptAnalyzer/pull/2075 +* Test PowerShell Preview in CI by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2070 +* Backport MSDocs changes by @bergmeister in https://github.com/PowerShell/PSScriptAnalyzer/pull/2085 +* Document new optional parameters added to UseCorrectCasing by @bergmeister in https://github.com/PowerShell/PSScriptAnalyzer/pull/2086 + +### New Contributors +* @martincostello made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/2034 +* @MatejKafka made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/2055 +* @XPlantefeve made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/1972 +* @John-Leitch made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/2013 +* @Tadas made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/1967 +* @PoshAJ made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/2021 +* @Jaykul made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/1704 + +**Full Changelog**: https://github.com/PowerShell/PSScriptAnalyzer/compare/1.23.0...1.24.0 + +## [1.23.0](https://github.com/PowerShell/PSScriptAnalyzer/tree/1.23.0) - 2024-10-09 + +### What's Changed +* Adding OneBranch pipeline YAML config file for OSS_Microsoft_PSSA-Official by @adityapatwardhan in https://github.com/PowerShell/PSScriptAnalyzer/pull/1981 +* Update format and grammar of AvoidUsingAllowUnencryptedAuthentication by @sdwheeler in https://github.com/PowerShell/PSScriptAnalyzer/pull/1974 +* Move to OneBranch Signing and SBOM generation by @TravisEz13 in https://github.com/PowerShell/PSScriptAnalyzer/pull/1982 +* Sync rule docs changes by @sdwheeler in https://github.com/PowerShell/PSScriptAnalyzer/pull/1985 +* Sync docs changes from MicrosoftDocs/PowerShell-Docs-Modules#213 by @sdwheeler in https://github.com/PowerShell/PSScriptAnalyzer/pull/1987 +* Update CHANGELOG for 1.22.0 release by @sdwheeler in https://github.com/PowerShell/PSScriptAnalyzer/pull/1990 +* Update Code of Conduct by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2002 +* Update default type definition of `RuleInfo` by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/2011 +* PSUseConsistentWhitespace: Handle redirect operators which are not in stream order by @liamjpeters in https://github.com/PowerShell/PSScriptAnalyzer/pull/2001 +* Setup GitHub Actions CI by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2018 +* Setup new OneBranch pipeline by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2027 +* Bump SMA version by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2028 +* Package updates by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2030 +* v1.23.0: Update version for new release by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2032 +* Migrate release pipeline to DeployBox by @andyleejordan in https://github.com/PowerShell/PSScriptAnalyzer/pull/2033 + +### New Contributors +* @adityapatwardhan made their first contribution in https://github.com/PowerShell/PSScriptAnalyzer/pull/1981 + +**Full Changelog**: https://github.com/PowerShell/PSScriptAnalyzer/compare/1.22.0...1.23.0 + +## [1.22.0](https://github.com/PowerShell/PSScriptAnalyzer/tree/1.22.0) - 2024-03-05 + +Minimum required version when using PowerShell 7 is now `7.2.11`. + +### New Rule + +- Add AvoidUsingAllowUnencryptedAuthentication by @MJVL in (#1857) +- Add the AvoidExclaimOperator rule to warn about the use of the ! negation operator. Fixes (#1826) by + @liamjpeters in (#1922) + +### Enhancements + +- Enable suppression of PSAvoidAssignmentToAutomaticVariable for specific variable or parameter by + @fflaten in (#1896) +- Upgrade to use .NET 6 since PowerShell 7.0 is now out out of support by @bergmeister in (#1873) +- Convert UseSingularNouns to configurable rule and add Windows to allowlist by @MJVL in (#1858) +- Add ErrorView to SpecialVars.cs by @ewisniew0 in (#1865) +- Allow suppression of PSUseSingularNouns for specific function by @fflaten in (#1903) +- Adding ToString() methods to [CorrectionExtent] and [DiagnosticRecord] by @StartAutomating in (#1946) +- Add PSNativeCommandUseErrorActionPreference preference variable by @aelij in (#1954) +- AvoidUsingPositionalParameter: Check if command has parameters to avoid having az in default + CommandAllowList by @bergmeister in (#1850) +- PSReviewUnusedParameter: Add CommandsToTraverse option by @FriedrichWeinmann in (#1921) + +### Fixes + +- Prevent NullReferenceException for null analysis type. by @hubuk in (#1949) + +### Build & Test, Documentation and Maintenance + +- UseApprovedVerbs.md: Backport minor change of PR 104 in PowerShell-Docs-Modules by @bergmeister in + (#1849) +- Improve Pester bootstrap logic for CI by @bergmeister in (#1853) +- Bump .NET SDK from 3.1.419 to 3.1.424 by @bergmeister in (#1852) +- AvoidLongLines: Make internal function DiagnosticSeverity private by @bergmeister in (#1851) +- SupportsShouldProcess.md: Fix Typo - MicrosoftDocs backport of PR 121 there by @sdwheeler in (#1869) +- Minor test fix for UseCorrectCasing rule by @kilasuit in (#1885) +- Make Invoke-Formatter test case assertion fail in case of incorrect casing by @alexandear in (#1888) +- Fix `AvoidUsingDoubleQuotesForConstantString` information in overview README by @michaeltlombardi + in (#1883) +- Update dependabot reviewers to remove Rob by @fflaten in (#1897) +- Fix typo in AvoidUsingPlainTextForPassword error message: 'to' being repeated two times by + @ALiwoto in (#1902) +- CI: Use new Ubuntu 22.04 image and remove deprecated Ubuntu 18.04 by @bergmeister in (#1847) +- Change double quotes to single where possible by @sdwheeler in (#1911) +- Backport MicrosoftDocs PR 143 by @sdwheeler in (#1910) +- Fix typos in rules documentation by @sdwheeler in (#1913) +- add demand for compliance job by @TravisEz13 in (#1920) +- FabricBot: Onboarding to GitOps.ResourceManagement because of FabricBot decommissioning by + @microsoft-github-policy-service in (#1925) +- Sync changes from Docs repository by @sdwheeler in (#1929) +- Developer documentation fix and message fix of + PossibleIncorrectUsageOfRedirectionOperatorDescription by @JoelTipke in (#1928) +- Documentation corrections for AvoidUsingPositionalParameters by @ImportTaste in (#1917) +- Update minimum PowerShell Core version to 7.2.11 as 7.0 is now EOL by @bergmeister in (#1872) +- Remove dead code and simplify by @bergmeister in (#1856) +- PSReservedParams - link about_CommonParameters by @petervandivier in (#1908) +- Generate strongly typed resources as part of build by @bergmeister in (#1855) +- Bump Newtonsoft.Json to 13.0.3 by @dependabot in (#1866) +- Use latest .NET 6.0 SDK patch version and update devcontainer to use .NET 6 as well by + @bergmeister in (#1955) +- Bump Microsoft.Management.Infrastructure from 1.0.0 to 3.0.0 for PowerShell 7 only by @dependabot + in (#1947) +- Bump version from 1.21.0 to 1.22.0 by @bergmeister in (#1965) +- Remove Appveyor badge from main README by @bergmeister in (#1962) +- Do not hard code common parameters in module help test any more by @bergmeister in (#1963) + +### New Contributors + +- @fflaten made their first contribution in (#1897) +- @ALiwoto made their first contribution in (#1902) +- @microsoft-github-policy-service made their first contribution in (#1925) +- @JoelTipke made their first contribution in (#1928) +- @ImportTaste made their first contribution in (#1917) +- @liamjpeters made their first contribution in (#1922) +- @petervandivier made their first contribution in (#1908) +- @ewisniew0 made their first contribution in (#1865) +- @StartAutomating made their first contribution in (#1946) +- @aelij made their first contribution in (#1954) +- @FriedrichWeinmann made their first contribution in (#1921) + +**Full Changelog**: + +## [1.21.0](https://github.com/PowerShell/PSScriptAnalyzer/tree/1.21.0) - 2022-09-27 + +### New Rule + +- Add AvoidMultipleTypeAttributes rule (#1705) (thanks @hankyi95) +- Add the AvoidSemicolonsAsLineTerminators rule to warn about lines ending with a semicolon. Fix + (#824) (#1806) (thanks @tempora-mutantur) +- Add AvoidUsingBrokenHashAlgorithms (#1787) (thanks @MJVL) + +### Enhancements + +- Also return suggestion to use PSCredential for AvoidUsingPlainTextForPassword rule (#1782) (by @bergmeister) +- Invoke-Formatter: Accept input from pipeline (#1763) (by @bergmeister) +- Make messages of UseCorrectCasing more detailed (#1843) +- Exclude automatic variable FormatEnumerationLimit from analysis by PSAvoidGlobalVars and + PSUseDeclaredVarsMoreThanAssignments (#1836) (by @bergmeister) +- PSAvoidUsingPositionalParameters: Do not warn on AZ CLI (#1846) (by @bergmeister) + +### Fixes + +- Fix PSUseConsistentIndentation.PipelineIndentation.None to not remove code when the previous line ended with a pipe (#1746) (by @bergmeister) +- Fix edge case of UseConsistentIndentation where child pipeline is on the same line as hashtable (#1838) (by @bergmeister) +- Skip OpenBrace check when preceded by Dot token (#1750) (by @bergmeister) +- Fix NRE (NullReferenceException) when custom rules omit optional properties in diagnostics (#1715) (by @rjmholt) + +### Build, Documenation and Maintenance + +- Tweak documentation for AvoidUsingBrokenHashAlgorithms (#1829) (thanks @michaeltlombardi) +- Set MaxDepth in JsonSerializerSettings for more secure handling of exceptional conditions in Newtonsoft.Json (#1824) (by @bergmeister) +- Use latest Newtonsoft.Json version for Windows PowerShell builds and not exclude it any more in Dependabot (#1825) (by @bergmeister) +- Correct pipeline acceptance docs for Invoke-Formatter.md (#1833) (by @bergmeister) +- Sync remaining docs changes from Microsoft docs (#1835) (by @bergmeister) +- (MAINT) Update URLs for site rebrand from docs.microsoft.com to learn.microsoft.com (#1844) (by @sdwheeler) +- Reformat note block (#1837) (by @sdwheeler) +- Update AvoidMultipleTypeAttributes.md with example that has runtime error (#1831) (by @bergmeister) +- Fix minimum PowerShell version to be 7.0.11 instead of 7.1.7 (#1830) (by @bergmeister) +- Add `.github/fabricbot.json` (#1812) +- Sync changes from docs repo (#1814) (by @sdwheeler) +- Fix name of PowerShell 5.1 test stage in CI (#1820) (by @bergmeister) +- CI: Retry test tasks in cas1e of failure to reduce sporadic failures (#1770) +- Upgrade from net452 to net462 for Windows PowerShell (#1789) +- Move issue template config into folder (#1804) (thanks @michaeltlombardi) +- Bump version from 1.20.0 to 1.21.0 (#1796) (by @bergmeister) +- Bump .NET SDK and SMA patch version (#1795) (by @bergmeister) +- Be sure not to clobber existing files when installing dotnet. (#1788) (by @JamesWTruher) +- Add link to issues chooser for reporting docs issues (#1794) (by @sdwheeler) +- Update README and delete docs that were migrated (#1790) (by @sdwheeler) +- Fix errors in ShouldProcess rule document (#1766) (thanks @masaru-iritani) +- Replicate changes from docs repo (#1781) (by @sdwheeler) +- Update Windows CI images as windows-2016 has been deprecated (#1784) (by @bergmeister) +- Use latest mac image rather than a specific version. (#1777) (by @JamesWTruher) +- Add docs migration notice & fix formatting (#1779) (by @sdwheeler) +- use compliant build environment for release build. (#1776) (by @JamesWTruher) +- Enable SBOM creation for script analyzer (#1762) (by @JamesWTruher) +- Fix links to work on GitHub (#1738) (by @andschwa) +- Update AvoidUsingWMICmdlet.md (#1737) (thanks @C0smin) +- Fix broken doc link in README (#1735) (thanks @AndrewRathbun) +- Add metadata to docs (#1731) (by @sdwheeler) +- Remove Ubuntu 16.04 from test matrix (#1733) (by @rjmholt) +- Use PowerShell1ES pool for official build (#1719) (by @JamesWTruher) +- Update cmdlet docs for 1.20.0 (#1726) (by @sdwheeler) +- Fixes (#1720) - move rule docs and update tests (#1724) (by @sdwheeler) +- Update rule docs (#1711) (by @sdwheeler) + +## [1.20.0](https://github.com/PowerShell/PSScriptAnalyzer/tree/1.20.0) - 2021-08-20 + +### Fixes + +- Replace unhelpful warning around `process` aliasing `Get-Process` with warning about misused syntax (#1638) (by @bergmeister) +- Fix `FunctionInfo` fallback AST attribute analysis for UseShouldProcessCorrectly (#1659) (thanks @hubuk) +- Do not increase indentation after a left parenthesis if the previous token is a newline and the next token is not a newline (#1469) (by @bergmeister) +- UseConsistentWhitespace - CheckOpenBrace setting: Do not warn when being preceded by open parenthesis (#1633) (by @bergmeister) + +### New Rule + +- Make UseSingularNouns rule work in PowerShell 7 (#1627) (by @bergmeister) +- UseConsistentWhitespace: Create option to ignore assignment operator inside hashtable (#1566) (thanks @daviesj) + +### Miscellaneous + +- Implement -IncludeSuppressions parameter (#1701) (by @rjmholt) +- Combine multiple suppressions applied to the same diagnostic (#1699) (by @rjmholt) +- Use dependabot to keep dependencies up to date (#1664) (by @rjmholt) +- Add reviewers and explicit dependency exclusions to dependabot (#1676) (by @rjmholt) +- Remove explicit registries in dependabot (#1671) (by @rjmholt) + +### Documentation + +- Link to PSScriptAnalyzer rule documentation from the README (#1642) (thanks @bbodenmiller) +- Add HelpInfoUri to module manifest (#1651) (by @sdwheeler) +- Add documentation around CustomRulePath in Settings file to README.md (#1636) (thanks @johlju) +- Use allowlist and blocklist terminology, supply alternate configuration key for PSAvoidUsingCmdletAliases (#1604) (by @bergmeister) +- Fix suppression example errors in README (#1593) (by @rjmholt) +- Fix AvoidLongLines documentation example (#1554) (thanks @floh96) +- Remove broken waffle.io links from readme.md (#1558) (thanks @clcaldwell) +- Fix typo in AvoidUsingCmdletAliases documentation (#1590) (thanks @rubengonzalez-dev) + +### Testing + +- Fix compatibility rule tests (#1697) (by @rjmholt) +- Fix missing space in AvoidPositionalParameter test asset (#1573) (by @jameswtruher) + +### Development/Build Improvements + +- Fix build module tests (#1654) (thanks @clcaldwell) +- Move to new signing process for release build and prep for 1.20.0 (#1625) (by @jameswtruher) +- Handle cases where the signature generator fails (#1661) (by @jameswtruher) +- Update .NET SDK to 3.1.408 (#1630) (by @bergmeister) +- Simplify devcontainer setup and resolve git line ending conflicts (#1637) (by @bergmeister) +- Speedup CI by bootstrapping PowerShell module installations in background and in parallel, whilst the .NET SDK is being installed (#1634) (by @bergmeister) +- Upgrade release Dockerfile from Ubuntu 16.04 to 20.04 (#1631) (by @bergmeister) +- Remove old powershell-core NuGet feed, which sometimes causes build failures (#1632) (by @bergmeister) +- Remove reference to myget.org as a package resource (#1640) (by @jameswtruher) +- Add Ubuntu 20.04 to build matrix (#1628) (by @bergmeister) +- Remove conditional compilation for PSv6/netstandard2.0 for Engine and Rules projects (#1600) (by @bergmeister) +- Upgrade Microsoft.CSharp, Microsoft.Win32.Registry and System.Reflection.TypeExtensions from 4.5.0 to 4.7.0 (#1599) (by @bergmeister) +- Use PowerShell preview extension for Github Codespaces (#1596) (by @bergmeister) +- Update SMA reference for netcoreapp3.1 to from 7.0.0 to 7.0.3 (#1597) (by @bergmeister) +- 🧹 Make usage of Microsoft.Management.Infrastructure consistent and reference it only once in CrossCompatibility project (#1601) (by @bergmeister) +- Update launch.json (#1603) (by @bergmeister) +- Make CI fail if tests fail and fix failing tests by making them Pester v5 compatible (#1553) (coauthored by @bergmeister) + +## [1.19.1](https://github.com/PowerShell/PSScriptAnalyzer/tree/1.19.1) - 2020-07-28 + +### Fixes + +- `UseCorrectCasing`: Do not use CommandInfoCache when CommandInfoParameters property throws due to runspace affinity problem of PowerShell engine (#1523) (by @bergmeister) +- `ReviewUnusedParameter`: Do not trigger when MyInvocation.BoundParameters or PSCmdlet.MyInvocation.BoundParameters is used (#1520) (Thanks @jegannathanmaniganadan!) +- `PipelineIndentationStyle.None`: Fix bug that caused incorrect formatting in hashtables (#1497) (by @bergmeister) +- `UseUsingScopeModifierInNewRunspaces`: Fix ArgumentException when the same variable name is used in 2 different sessions. (#1493) (by @bergmeister) +- `UseConsistentWhitespace` + - Check previous token only if it starts on the same line (#1491) (by @bergmeister) + - Fix CheckParameter bug when using interpolated string (#1498) (by @bergmeister) + +### New Rule + +- New rule (disabled by default): [AvoidUsingDoubleQuotesForConstantString](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/RuleDocumentation/AvoidUsingDoubleQuotesForConstantString.md) (#1470) (by @bergmeister) + +### Miscellaneous + +- Set Assembly version attribute (#1549) (by @JamesWTruher) + +### Documentation + +- Remove references to Windows in Introduction of Readme.md (#1509) (Thanks @hjorslev!) + +### Testing + +- Pester v5 (#1444) (by @bergmeister) + +### Development improvements + +- Include PDB files in Debug build (#1535) (by @bergmeister) +- Add more useful launch configurations (#1499) (by @bergmeister) +- Make .Net Core version in Docker image of .devcontainer match the one in global.json (#1494) (by @bergmeister) + ## [1.19.0](https://github.com/PowerShell/PSScriptAnalyzer/tree/1.19.0) ### New Rules @@ -16,7 +344,7 @@ ### Compatibility Rules -- Make CompatibilityCollector able to parse a single version String #1446 (by @bergmeister) +- Make CompatibilityCollector able to parse a single version String (#1446) (by @bergmeister) - Update compatibility profiles for PowerShell 7 (#1429) (by @rjmholt) - Ps7 syntax (#1426) (by @rjmholt) - Fix ps3 syntax check (#1395) (by @rjmholt) @@ -117,7 +445,7 @@ The PRs for those improvements are: ## [1.18.3](https://github.com/PowerShell/PSScriptAnalyzer/tree/1.18.3) - 2019-09-13 -This release adds one more important fix, which prevents a leak of runspaces when repeatedly invoking the PSScriptAnalyzer cmdlets. Furthermore it ups the minimum version of PowerShell Core to `6.2.0` due to `6.1` reaching its [end of life](https://docs.microsoft.com/en-us/powershell/scripting/powershell-support-lifecycle?view=powershell-6#powershell-releases-end-of-life) this month. +This release adds one more important fix, which prevents a leak of runspaces when repeatedly invoking the PSScriptAnalyzer cmdlets. Furthermore it ups the minimum version of PowerShell Core to `6.2.0` due to `6.1` reaching its [end of life](https://learn.microsoft.com/powershell/scripting/install/PowerShell-Support-Lifecycle#powershell-end-of-support-dates) this month. - Change CommandInfoCache to implement IDisposable and clean up the runspace pool (#1335) (by @JamesWTruher) - Update Newtonsoft json to 12.0.1 due to PowerShell 6.1 going out of support (#1336) (by @bergmeister) @@ -183,11 +511,11 @@ The benefit to the user will be some enhancements and fixes in the formatter, es - Convert compatibility module to binary module, fix compatibility with Azure environments (#1212) (by @rjmholt) - Prevent .NET members with names differing only by case from crashing the compatibility profiler (#1195) (by @rjmholt) - Fix compatibility profile query API so that aliases referring to other modules appear (#1194) (by @rjmholt) - + - DSC - Instead of using the first cimClass and then having no superClass, use the first cimClass that has a non-null superClass (#1200) (by @bergmeister, thanks to @ykuijs! for the great collaboration) - Make `-SaveDscDependency` work on Linux (#1246) (by @bergmeister) - + - Enable suppression of custom rules when used together with `-IncludeDefaultRules` to allow all possible scenarios from 1.17.1 and 1.18.0. This removes also the check if the rule name in the suppression attribute can be found because this check is technically not possible in all scenarios (#1245) (by @bergmeister) - Fix NullReferenceException for class type (#1182) (by @bergmeister) @@ -400,7 +728,7 @@ Multi-threading efficiency was highly improved leading to a speedup whilst keepi - Add simple GitHub Pull Request template based off the one for PowerShell Core (#866) (by @bergmeister) - Add a simple GitHub issue template based on the one of PowerShell Core. (#865, #884) (by @bergmeister) - Fix Example 7 in Invoke-ScriptAnalyzer.md (#862) (Thanks @sethvs!) -- Use the typewriter apostrophe instead the typographic apostrophe (#855) (Thanks @alexandear!) +- Use the typewriter apostrophe instead the typographic apostrophe (#855) (Thanks @alexandear!) - Add justification to ReadMe (#848) (Thanks @KevinMarquette!) - Fix typo in README (#845) (Thanks @misterGF!) @@ -469,7 +797,7 @@ Many thanks to @rkeithhill for contributing the _Stroustrup_ style code formatti ## [1.11.1](https://github.com/PowerShell/PSScriptAnalyzer/tree/1.11.1) - 2017-04-04 ### Fixed - CodeFormatting settings file (#727, #728). -- Whitelisted aliases comparison in AvoidUsingCmdletAliases rule (#739). +- Allowlist aliases comparison in AvoidUsingCmdletAliases rule (#739). - PlaceCloseBrace rule behavior for NewLineAfter option (#741). - UseConsistentIndentation rule to ignore open brace in magic methods (#744). @@ -566,7 +894,7 @@ Here are some improvements since the last release. - Fix `SaveDscDependency` switch implementation, which use fail if more than one parameter is given to `Import-DSCResource` dynamic keyword. - Add support for external AST based rule suppression - Fix rule suppression caused by inavlid offsets -- Whitelist `Data` in `PSUseSingularNoun` rule +- Allowlist `Data` in `PSUseSingularNoun` rule - Fix rule documentation of `PSDSCExamplesPresent` - Fix false positives caused by PSD1 files which are not module manifests - affects `PSUseToExportFieldsInManifest`, `PSMissingModuleManifestField` and `PSAvoidUsingDeprecatedManifestFields` rules @@ -574,10 +902,10 @@ Here are some improvements since the last release. - Add build script to automate building and testing the solution A big **Thank You!** to the following folks for making PSScriptAnalyzer even better: -- [Kieran Jacobsen (@kjacobsen)](https://github.com/PowerShell/PSScriptAnalyzer/commits/development?author=kjacobsen): Fix rule documentation of `PSDSCExamplesPresent` [PR #591](https://github.com/PowerShell/PSScriptAnalyzer/pull/591) -- [Charlie Schmidt (@charlieschmidt)](https://github.com/PowerShell/PSScriptAnalyzer/commits/development?author=charlieschmidt): Suppress External Rules [PR #585](https://github.com/PowerShell/PSScriptAnalyzer/pull/585) +- [Kieran Jacobsen (@kjacobsen)](https://github.com/PowerShell/PSScriptAnalyzer/commits/development?author=kjacobsen): Fix rule documentation of `PSDSCExamplesPresent` [PR #591](#591) +- [Charlie Schmidt (@charlieschmidt)](https://github.com/PowerShell/PSScriptAnalyzer/commits/development?author=charlieschmidt): Suppress External Rules [PR #585](#585) - [June Blender (@juneb)](https://github.com/PowerShell/PSScriptAnalyzer/commits/development?author=juneb): Add tests for module help [058f65e1](https://github.com/PowerShell/PSScriptAnalyzer/commit/058f65e1f6278222378fedf444eecb2e32865b1e) -- [Shayde Nofziger (@Blackbaud-ShaydeNofziger)](https://github.com/PowerShell/PSScriptAnalyzer/commits/development?author=Blackbaud-ShaydeNofziger): Fix rule name typo and comment [PR #560](https://github.com/PowerShell/PSScriptAnalyzer/pull/560) +- [Shayde Nofziger (@Blackbaud-ShaydeNofziger)](https://github.com/PowerShell/PSScriptAnalyzer/commits/development?author=Blackbaud-ShaydeNofziger): Fix rule name typo and comment [PR #560](#560) ## [1.6.0](https://github.com/PowerShell/PSScriptAnalyzer/tree/1.6.0) - 2016-06-07 @@ -676,7 +1004,8 @@ A big **Thank You!** to the following folks for making PSScriptAnalyzer even bet ### Fixes: #### Engine: - Engine update to prevent script based injection attacks -- CustomizedRulePath is now called CustomRulePath – Fixes to handle folder paths +- CustomizedRulePath is now called CustomRulePath +- Fixes to handle folder paths - Fixes for RecurseCustomRulePath functionality - Fix to binplace cmdlet help file as part of build process - ScriptAnalyzer Profile is now called Settings @@ -694,7 +1023,8 @@ A big **Thank You!** to the following folks for making PSScriptAnalyzer even bet - Update to Credential based rules to validate the presence of CredentialAttribute and PSCredential type ### Documentation: -- Rule & Cmdlet documentation updates – Cmdlet help file addition +- Rule & Cmdlet documentation updates +- Cmdlet help file addition ## [1.1.1](https://github.com/PowerShell/PSScriptAnalyzer/tree/1.1.1) - 2015-11-03 ### Features: @@ -735,7 +1065,7 @@ A big **Thank You!** to the following folks for making PSScriptAnalyzer even bet ### Rules: - New rule to validate the presence of deprecated module manifest fields. -- Removed PSAvoidTrapStatement rule from the builtin set – since this is not deprecated and using trap is a better choice in certain scenarios. +- Removed PSAvoidTrapStatement rule from the builtin set since this is not deprecated and using trap is a better choice in certain scenarios. ### Fixes: - Verbose Message rule applies to only DSC cmdlet based resources. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..686e5e7a0 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,10 @@ +# Microsoft Open Source Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +Resources: + +- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) +- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns +- Employees can reach out at [aka.ms/opensource/moderation-support](https://aka.ms/opensource/moderation-support) diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 000000000..f7d809c1d --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,7 @@ + + + + 1.24.0 + true + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 000000000..d79bb8840 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/Engine/CommandInfoCache.cs b/Engine/CommandInfoCache.cs index 02e926b53..aa9d725f3 100644 --- a/Engine/CommandInfoCache.cs +++ b/Engine/CommandInfoCache.cs @@ -15,17 +15,15 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer internal class CommandInfoCache : IDisposable { private readonly ConcurrentDictionary> _commandInfoCache; - private readonly Helper _helperInstance; private readonly RunspacePool _runspacePool; private bool disposed = false; /// /// Create a fresh command info cache instance. /// - public CommandInfoCache(Helper pssaHelperInstance) + public CommandInfoCache() { _commandInfoCache = new ConcurrentDictionary>(); - _helperInstance = pssaHelperInstance; _runspacePool = RunspaceFactory.CreateRunspacePool(1, 10); _runspacePool.Open(); } @@ -57,8 +55,9 @@ protected virtual void Dispose(bool disposing) /// /// Name of the command to get a commandinfo object for. /// What types of command are needed. If omitted, all types are retrieved. + /// When needed due to runspace affinity problems of some PowerShell objects. /// - public CommandInfo GetCommandInfo(string commandName, CommandTypes? commandTypes = null) + public CommandInfo GetCommandInfo(string commandName, CommandTypes? commandTypes = null, bool bypassCache = false) { if (string.IsNullOrWhiteSpace(commandName)) { @@ -66,6 +65,10 @@ public CommandInfo GetCommandInfo(string commandName, CommandTypes? commandTypes } var key = new CommandLookupKey(commandName, commandTypes); + if (bypassCache) + { + return GetCommandInfoInternal(commandName, commandTypes); + } // Atomically either use PowerShell to query a command info object, or fetch it from the cache return _commandInfoCache.GetOrAdd(key, new Lazy(() => GetCommandInfoInternal(commandName, commandTypes))).Value; } @@ -77,16 +80,31 @@ public CommandInfo GetCommandInfo(string commandName, CommandTypes? commandTypes /// Returns null if command does not exists private CommandInfo GetCommandInfoInternal(string cmdName, CommandTypes? commandType) { + string moduleName = null; + string actualCmdName = cmdName; + + // Check if cmdName is in the format "moduleName\CmdletName" (exactly one backslash) + int backslashIndex = cmdName.IndexOf('\\'); + if ( + backslashIndex > 0 && + backslashIndex == cmdName.LastIndexOf('\\') && + backslashIndex != cmdName.Length - 1 && + backslashIndex != 0 + ) + { + moduleName = cmdName.Substring(0, backslashIndex); + actualCmdName = cmdName.Substring(backslashIndex + 1); + } // 'Get-Command ?' would return % for example due to PowerShell interpreting is a single-character-wildcard search and not just the ? alias. // For more details see https://github.com/PowerShell/PowerShell/issues/9308 - cmdName = WildcardPattern.Escape(cmdName); + actualCmdName = WildcardPattern.Escape(actualCmdName); using (var ps = System.Management.Automation.PowerShell.Create()) { ps.RunspacePool = _runspacePool; ps.AddCommand("Get-Command") - .AddParameter("Name", cmdName) + .AddParameter("Name", actualCmdName) .AddParameter("ErrorAction", "SilentlyContinue"); if (commandType != null) @@ -94,6 +112,11 @@ private CommandInfo GetCommandInfoInternal(string cmdName, CommandTypes? command ps.AddParameter("CommandType", commandType); } + if (!string.IsNullOrEmpty(moduleName)) + { + ps.AddParameter("Module", moduleName); + } + return ps.Invoke() .FirstOrDefault(); } diff --git a/Engine/Commands/GetScriptAnalyzerRuleCommand.cs b/Engine/Commands/GetScriptAnalyzerRuleCommand.cs index 7a9d50561..3219affa7 100644 --- a/Engine/Commands/GetScriptAnalyzerRuleCommand.cs +++ b/Engine/Commands/GetScriptAnalyzerRuleCommand.cs @@ -84,13 +84,12 @@ protected override void BeginProcessing() // Initialize helper Helper.Instance = new Helper( - SessionState.InvokeCommand, - this); + SessionState.InvokeCommand); Helper.Instance.Initialize(); string[] rulePaths = Helper.ProcessCustomRulePaths(customRulePath, this.SessionState, recurseCustomRulePath); - ScriptAnalyzer.Instance.Initialize(this, rulePaths, null, null, null, null == rulePaths ? true : false); + ScriptAnalyzer.Instance.Initialize(this, rulePaths, null, null, null, null == rulePaths); } /// diff --git a/Engine/Commands/InvokeFormatterCommand.cs b/Engine/Commands/InvokeFormatterCommand.cs index c0d9fd168..25a2d364e 100644 --- a/Engine/Commands/InvokeFormatterCommand.cs +++ b/Engine/Commands/InvokeFormatterCommand.cs @@ -25,14 +25,14 @@ public class InvokeFormatterCommand : PSCmdlet, IOutputWriter /// /// *NOTE*: Unlike ScriptBlock parameter, the ScriptDefinition parameter require a string value. /// - [ParameterAttribute(Mandatory = true, Position = 1)] + [ParameterAttribute(Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 1)] [ValidateNotNull] public string ScriptDefinition { get; set; } /// /// A settings hashtable or a path to a PowerShell data file (.psd1) file that contains the settings. /// - [Parameter(Mandatory = false, Position = 2)] + [Parameter(Mandatory = false, ValueFromPipelineByPropertyName = true, Position = 2)] [ValidateNotNull] public object Settings { get; set; } = defaultSettingsPreset; @@ -44,7 +44,7 @@ public class InvokeFormatterCommand : PSCmdlet, IOutputWriter /// end column number. These numbers must be greater than 0. /// /// - [Parameter(Mandatory = false, Position = 3)] + [Parameter(Mandatory = false, ValueFromPipelineByPropertyName = true, Position = 3)] [ValidateNotNull] [ValidateCount(4, 4)] public int[] Range { get; set; } @@ -124,11 +124,5 @@ protected override void StopProcessing() ScriptAnalyzer.Instance.CleanUp(); base.StopProcessing(); } - - private void ValidateInputSettings() - { - // todo implement this - return; - } } } diff --git a/Engine/Commands/InvokeScriptAnalyzerCommand.cs b/Engine/Commands/InvokeScriptAnalyzerCommand.cs index f5598d5bd..a444327e0 100644 --- a/Engine/Commands/InvokeScriptAnalyzerCommand.cs +++ b/Engine/Commands/InvokeScriptAnalyzerCommand.cs @@ -21,15 +21,22 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands /// [Cmdlet(VerbsLifecycle.Invoke, "ScriptAnalyzer", - DefaultParameterSetName = "File", + DefaultParameterSetName = ParameterSet_Path_SuppressedOnly, SupportsShouldProcess = true, HelpUri = "https://go.microsoft.com/fwlink/?LinkId=525914")] - [OutputType(typeof(DiagnosticRecord))] - [OutputType(typeof(SuppressedRecord))] + [OutputType(typeof(DiagnosticRecord), typeof(SuppressedRecord))] public class InvokeScriptAnalyzerCommand : PSCmdlet, IOutputWriter { + private const string ParameterSet_Path_SuppressedOnly = nameof(Path) + "_" + nameof(SuppressedOnly); + private const string ParameterSet_Path_IncludeSuppressed = nameof(Path) + "_" + nameof(IncludeSuppressed); + private const string ParameterSet_ScriptDefinition_SuppressedOnly = nameof(ScriptDefinition) + "_" + nameof(SuppressedOnly); + private const string ParameterSet_ScriptDefinition_IncludeSuppressed = nameof(ScriptDefinition) + "_" + nameof(IncludeSuppressed); + #region Private variables List processedPaths; + // initialize to zero for all severity enum values + private Dictionary diagnosticCounts = + Enum.GetValues(typeof(DiagnosticSeverity)).Cast().ToDictionary(s => s, _ => 0); #endregion // Private variables #region Parameters @@ -37,7 +44,12 @@ public class InvokeScriptAnalyzerCommand : PSCmdlet, IOutputWriter /// Path: The path to the file or folder to invoke PSScriptAnalyzer on. /// [Parameter(Position = 0, - ParameterSetName = "File", + ParameterSetName = ParameterSet_Path_IncludeSuppressed, + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + [Parameter(Position = 0, + ParameterSetName = ParameterSet_Path_SuppressedOnly, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] @@ -54,7 +66,12 @@ public string Path /// ScriptDefinition: a script definition in the form of a string to run rules on. /// [Parameter(Position = 0, - ParameterSetName = "ScriptDefinition", + ParameterSetName = ParameterSet_ScriptDefinition_IncludeSuppressed, + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + [Parameter(Position = 0, + ParameterSetName = ParameterSet_ScriptDefinition_SuppressedOnly, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] @@ -84,7 +101,6 @@ public string[] CustomRulePath /// RecurseCustomRulePath: Find rules within subfolders under the path /// [Parameter(Mandatory = false)] - [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] public SwitchParameter RecurseCustomRulePath { get { return recurseCustomRulePath; } @@ -96,7 +112,6 @@ public SwitchParameter RecurseCustomRulePath /// IncludeDefaultRules: Invoke default rules along with Custom rules /// [Parameter(Mandatory = false)] - [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] public SwitchParameter IncludeDefaultRules { get { return includeDefaultRules; } @@ -143,11 +158,15 @@ public string[] Severity } private string[] severity; + // TODO: This should be only in the Path parameter sets, and is ignored otherwise, + // but we already have a test that depends on it being otherwise + //[Parameter(ParameterSetName = ParameterSet_Path_IncludeSuppressed)] + //[Parameter(ParameterSetName = ParameterSet_Path_SuppressedOnly)] + // /// /// Recurse: Apply to all files within subfolders under the path /// - [Parameter(Mandatory = false)] - [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] + [Parameter] public SwitchParameter Recurse { get { return recurse; } @@ -158,19 +177,22 @@ public SwitchParameter Recurse /// /// ShowSuppressed: Show the suppressed message /// - [Parameter(Mandatory = false)] - [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] - public SwitchParameter SuppressedOnly - { - get { return suppressedOnly; } - set { suppressedOnly = value; } - } - private bool suppressedOnly; + [Parameter(ParameterSetName = ParameterSet_Path_SuppressedOnly)] + [Parameter(ParameterSetName = ParameterSet_ScriptDefinition_SuppressedOnly)] + public SwitchParameter SuppressedOnly { get; set; } + + /// + /// Include suppressed diagnostics in the output. + /// + [Parameter(ParameterSetName = ParameterSet_Path_IncludeSuppressed, Mandatory = true)] + [Parameter(ParameterSetName = ParameterSet_ScriptDefinition_IncludeSuppressed, Mandatory = true)] + public SwitchParameter IncludeSuppressed { get; set; } /// /// Resolves rule violations automatically where possible. /// - [Parameter(Mandatory = false, ParameterSetName = "File")] + [Parameter(Mandatory = false, ParameterSetName = ParameterSet_Path_IncludeSuppressed)] + [Parameter(Mandatory = false, ParameterSetName = ParameterSet_Path_SuppressedOnly)] public SwitchParameter Fix { get { return fix; } @@ -266,8 +288,7 @@ protected override void BeginProcessing() } #endif Helper.Instance = new Helper( - SessionState.InvokeCommand, - this); + SessionState.InvokeCommand); Helper.Instance.Initialize(); var psVersionTable = this.SessionState.PSVariable.GetValue("PSVersionTable") as Hashtable; @@ -334,6 +355,12 @@ protected override void BeginProcessing() this.settings)); } + SuppressionPreference suppressionPreference = SuppressedOnly + ? SuppressionPreference.SuppressedOnly + : IncludeSuppressed + ? SuppressionPreference.Include + : SuppressionPreference.Omit; + ScriptAnalyzer.Instance.Initialize( this, combRulePaths, @@ -341,7 +368,7 @@ protected override void BeginProcessing() this.excludeRule, this.severity, combRulePaths == null || combIncludeDefaultRules, - this.suppressedOnly); + suppressionPreference); } /// @@ -388,6 +415,37 @@ protected override void EndProcessing() { ScriptAnalyzer.Instance.CleanUp(); base.EndProcessing(); + + var diagnosticCount = diagnosticCounts.Values.Sum(); + + if (ReportSummary.IsPresent) + { + if (diagnosticCount == 0) + { + Host.UI.WriteLine("0 rule violations found."); + } + else + { + var infoCount = diagnosticCounts[DiagnosticSeverity.Information]; + var warningCount = diagnosticCounts[DiagnosticSeverity.Warning]; + var errorCount = diagnosticCounts[DiagnosticSeverity.Error] + diagnosticCounts[DiagnosticSeverity.ParseError]; + var severeDiagnosticCount = diagnosticCount - infoCount; + + var colorPropertyPrefix = severeDiagnosticCount == 0 ? "Warning" : "Error"; + var pluralS = diagnosticCount > 1 ? "s" : string.Empty; + ConsoleHostHelper.DisplayMessageUsingSystemProperties( + Host, colorPropertyPrefix + "ForegroundColor", colorPropertyPrefix + "BackgroundColor", + $"{diagnosticCount} rule violation{pluralS} found. Severity distribution: " + + $"{DiagnosticSeverity.Error} = {errorCount}, " + + $"{DiagnosticSeverity.Warning} = {warningCount}, " + + $"{DiagnosticSeverity.Information} = {infoCount}"); + } + } + + if (EnableExit) + { + this.Host.SetShouldExit(diagnosticCount); + } } protected override void StopProcessing() @@ -402,89 +460,50 @@ protected override void StopProcessing() private void ProcessInput() { - IEnumerable diagnosticsList = Enumerable.Empty(); - if (IsFileParameterSet()) + foreach (var diagnostic in RunAnalysis()) { - foreach (var p in processedPaths) + diagnosticCounts[diagnostic.Severity]++; + + foreach (var logger in ScriptAnalyzer.Instance.Loggers) { - if (fix) - { - ShouldProcess(p, $"Analyzing and fixing path with Recurse={this.recurse}"); - diagnosticsList = ScriptAnalyzer.Instance.AnalyzeAndFixPath(p, this.ShouldProcess, this.recurse); - } - else - { - ShouldProcess(p, $"Analyzing path with Recurse={this.recurse}"); - diagnosticsList = ScriptAnalyzer.Instance.AnalyzePath(p, this.ShouldProcess, this.recurse); - } - WriteToOutput(diagnosticsList); + logger.LogObject(diagnostic, this); } } - else if (String.Equals(this.ParameterSetName, "ScriptDefinition", StringComparison.OrdinalIgnoreCase)) - { - diagnosticsList = ScriptAnalyzer.Instance.AnalyzeScriptDefinition(scriptDefinition, out _, out _); - WriteToOutput(diagnosticsList); - } } - private void WriteToOutput(IEnumerable diagnosticRecords) + private IEnumerable RunAnalysis() { - foreach (ILogger logger in ScriptAnalyzer.Instance.Loggers) + if (!IsFileParameterSet()) { - var errorCount = 0; - var warningCount = 0; - var infoCount = 0; - var parseErrorCount = 0; + foreach (var record in ScriptAnalyzer.Instance.AnalyzeScriptDefinition(scriptDefinition, out _, out _)) + { + yield return record; + } + yield break; + } - foreach (DiagnosticRecord diagnostic in diagnosticRecords) + foreach (var path in this.processedPaths) + { + if (!ShouldProcess(path, $"Analyzing path with Fix={this.fix} and Recurse={this.recurse}")) { - logger.LogObject(diagnostic, this); - switch (diagnostic.Severity) - { - case DiagnosticSeverity.Information: - infoCount++; - break; - case DiagnosticSeverity.Warning: - warningCount++; - break; - case DiagnosticSeverity.Error: - errorCount++; - break; - case DiagnosticSeverity.ParseError: - parseErrorCount++; - break; - default: - throw new ArgumentOutOfRangeException(nameof(diagnostic.Severity), $"Severity '{diagnostic.Severity}' is unknown"); - } + continue; } - if (ReportSummary.IsPresent) + if (fix) { - var numberOfRuleViolations = infoCount + warningCount + errorCount; - if (numberOfRuleViolations == 0) + foreach (var record in ScriptAnalyzer.Instance.AnalyzeAndFixPath(path, this.ShouldProcess, this.recurse)) { - Host.UI.WriteLine("0 rule violations found."); + yield return record; } - else + } + else + { + foreach (var record in ScriptAnalyzer.Instance.AnalyzePath(path, this.ShouldProcess, this.recurse)) { - var pluralS = numberOfRuleViolations > 1 ? "s" : string.Empty; - var message = $"{numberOfRuleViolations} rule violation{pluralS} found. Severity distribution: {DiagnosticSeverity.Error} = {errorCount}, {DiagnosticSeverity.Warning} = {warningCount}, {DiagnosticSeverity.Information} = {infoCount}"; - if (warningCount + errorCount == 0) - { - ConsoleHostHelper.DisplayMessageUsingSystemProperties(Host, "WarningForegroundColor", "WarningBackgroundColor", message); - } - else - { - ConsoleHostHelper.DisplayMessageUsingSystemProperties(Host, "ErrorForegroundColor", "ErrorBackgroundColor", message); - } + yield return record; } } } - - if (EnableExit.IsPresent) - { - this.Host.SetShouldExit(diagnosticRecords.Count()); - } } private void ProcessPath() @@ -497,10 +516,7 @@ private void ProcessPath() } } - private bool IsFileParameterSet() - { - return String.Equals(this.ParameterSetName, "File", StringComparison.OrdinalIgnoreCase); - } + private bool IsFileParameterSet() => Path is not null; private bool OverrideSwitchParam(bool paramValue, string paramName) { @@ -511,4 +527,4 @@ private bool OverrideSwitchParam(bool paramValue, string paramName) #endregion // Private Methods } -} +} \ No newline at end of file diff --git a/Engine/Engine.csproj b/Engine/Engine.csproj index 1f8d6b6e0..63b9a1b9c 100644 --- a/Engine/Engine.csproj +++ b/Engine/Engine.csproj @@ -1,26 +1,28 @@  - 1.19.0 - netcoreapp3.1;netstandard2.0;net452 + $(ModuleVersion) + net8;net462 Microsoft.Windows.PowerShell.ScriptAnalyzer + $(ModuleVersion) Engine Microsoft.Windows.PowerShell.ScriptAnalyzer + 9.0 - + - + portable - + $(DefineConstants);CORECLR - + @@ -43,63 +45,39 @@ - - - - - - True - True - Strings.resx - + + - ResXFileCodeGenerator + + MSBuild:Compile Strings.Designer.cs + $(IntermediateOutputPath)\Strings.Designer.cs + CSharp + Microsoft.Windows.PowerShell.ScriptAnalyzer + Strings - - - - - - $(DefineConstants);PSV7;CORECLR - - - $(DefineConstants);PSV6;CORECLR + + + PrepareResources;$(CompileDependsOn) - - - - - - - - + + + + $(DefineConstants);PSV7;CORECLR + - - + + - - - $(DefineConstants);PSV3 - - - - $(DefineConstants);PSV3;PSV4 - - - - $(DefineConstants);PSV3 - - - - $(DefineConstants);PSV3;PSV4 - diff --git a/Engine/Extensions.cs b/Engine/Extensions.cs index 5e10b1d78..46b67bf65 100644 --- a/Engine/Extensions.cs +++ b/Engine/Extensions.cs @@ -178,5 +178,19 @@ public static bool GetValue(this NamedAttributeArgumentAst attrAst, out Expressi return false; } + + internal static bool TryGetPropertyValue(this PSObject psObject, string propertyName, out object value) + { + PSMemberInfo property = psObject.Properties[propertyName]; + + if (property is null) + { + value = default; + return false; + } + + value = property.Value; + return true; + } } } diff --git a/Engine/FindAstPositionVisitor.cs b/Engine/FindAstPositionVisitor.cs new file mode 100644 index 000000000..459581cbc --- /dev/null +++ b/Engine/FindAstPositionVisitor.cs @@ -0,0 +1,369 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Management.Automation.Language; + +namespace Microsoft.Windows.PowerShell.ScriptAnalyzer +{ + /// + /// Provides an efficient way to find the position in the AST corresponding to a given script position. + /// +#if !(PSV3 || PSV4) + internal class FindAstPositionVisitor : AstVisitor2 +#else + internal class FindAstPositionVisitor : AstVisitor +#endif + { + private IScriptPosition searchPosition; + + /// + /// Contains the position in the AST corresponding to the provided upon completion of the method. + /// + public Ast AstPosition { get; private set; } + + /// + /// Initializes a new instance of the class with the postition to search for. + /// + /// The script position to search for. + public FindAstPositionVisitor(IScriptPosition position) + { + this.searchPosition = position; + } + + public override AstVisitAction VisitArrayExpression(ArrayExpressionAst arrayExpressionAst) + { + return Visit(arrayExpressionAst); + } + + public override AstVisitAction VisitArrayLiteral(ArrayLiteralAst arrayLiteralAst) + { + return Visit(arrayLiteralAst); + } + + public override AstVisitAction VisitAssignmentStatement(AssignmentStatementAst assignmentStatementAst) + { + return Visit(assignmentStatementAst); + } + + public override AstVisitAction VisitAttribute(AttributeAst attributeAst) + { + return Visit(attributeAst); + } + + public override AstVisitAction VisitAttributedExpression(AttributedExpressionAst attributedExpressionAst) + { + return Visit(attributedExpressionAst); + } + + public override AstVisitAction VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst) + { + return Visit(binaryExpressionAst); + } + + public override AstVisitAction VisitBlockStatement(BlockStatementAst blockStatementAst) + { + return Visit(blockStatementAst); + } + + public override AstVisitAction VisitBreakStatement(BreakStatementAst breakStatementAst) + { + return Visit(breakStatementAst); + } + + public override AstVisitAction VisitCatchClause(CatchClauseAst catchClauseAst) + { + return Visit(catchClauseAst); + } + + public override AstVisitAction VisitCommand(CommandAst commandAst) + { + return Visit(commandAst); + } + + public override AstVisitAction VisitCommandExpression(CommandExpressionAst commandExpressionAst) + { + return Visit(commandExpressionAst); + } + + public override AstVisitAction VisitCommandParameter(CommandParameterAst commandParameterAst) + { + return Visit(commandParameterAst); + } + + public override AstVisitAction VisitConstantExpression(ConstantExpressionAst constantExpressionAst) + { + return Visit(constantExpressionAst); + } + + public override AstVisitAction VisitContinueStatement(ContinueStatementAst continueStatementAst) + { + return Visit(continueStatementAst); + } + + public override AstVisitAction VisitConvertExpression(ConvertExpressionAst convertExpressionAst) + { + return Visit(convertExpressionAst); + } + + public override AstVisitAction VisitDataStatement(DataStatementAst dataStatementAst) + { + return Visit(dataStatementAst); + } + + public override AstVisitAction VisitDoUntilStatement(DoUntilStatementAst doUntilStatementAst) + { + return Visit(doUntilStatementAst); + } + + public override AstVisitAction VisitDoWhileStatement(DoWhileStatementAst doWhileStatementAst) + { + return Visit(doWhileStatementAst); + } + + public override AstVisitAction VisitErrorExpression(ErrorExpressionAst errorExpressionAst) + { + return Visit(errorExpressionAst); + } + + public override AstVisitAction VisitErrorStatement(ErrorStatementAst errorStatementAst) + { + return Visit(errorStatementAst); + } + + public override AstVisitAction VisitExitStatement(ExitStatementAst exitStatementAst) + { + return Visit(exitStatementAst); + } + + public override AstVisitAction VisitExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst) + { + return Visit(expandableStringExpressionAst); + } + + public override AstVisitAction VisitFileRedirection(FileRedirectionAst fileRedirectionAst) + { + return Visit(fileRedirectionAst); + } + + public override AstVisitAction VisitForEachStatement(ForEachStatementAst forEachStatementAst) + { + return Visit(forEachStatementAst); + } + + public override AstVisitAction VisitForStatement(ForStatementAst forStatementAst) + { + return Visit(forStatementAst); + } + + public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) + { + return Visit(functionDefinitionAst); + } + + public override AstVisitAction VisitHashtable(HashtableAst hashtableAst) + { + return Visit(hashtableAst); + } + + public override AstVisitAction VisitIfStatement(IfStatementAst ifStatementAst) + { + return Visit(ifStatementAst); + } + + public override AstVisitAction VisitIndexExpression(IndexExpressionAst indexExpressionAst) + { + return Visit(indexExpressionAst); + } + + public override AstVisitAction VisitInvokeMemberExpression(InvokeMemberExpressionAst invokeMemberExpressionAst) + { + return Visit(invokeMemberExpressionAst); + } + + public override AstVisitAction VisitMemberExpression(MemberExpressionAst memberExpressionAst) + { + return Visit(memberExpressionAst); + } + + public override AstVisitAction VisitMergingRedirection(MergingRedirectionAst mergingRedirectionAst) + { + return Visit(mergingRedirectionAst); + } + + public override AstVisitAction VisitNamedAttributeArgument(NamedAttributeArgumentAst namedAttributeArgumentAst) + { + return Visit(namedAttributeArgumentAst); + } + + public override AstVisitAction VisitNamedBlock(NamedBlockAst namedBlockAst) + { + return Visit(namedBlockAst); + } + + public override AstVisitAction VisitParamBlock(ParamBlockAst paramBlockAst) + { + return Visit(paramBlockAst); + } + + public override AstVisitAction VisitParameter(ParameterAst parameterAst) + { + return Visit(parameterAst); + } + + public override AstVisitAction VisitParenExpression(ParenExpressionAst parenExpressionAst) + { + return Visit(parenExpressionAst); + } + + public override AstVisitAction VisitPipeline(PipelineAst pipelineAst) + { + return Visit(pipelineAst); + } + + public override AstVisitAction VisitReturnStatement(ReturnStatementAst returnStatementAst) + { + return Visit(returnStatementAst); + } + + public override AstVisitAction VisitScriptBlock(ScriptBlockAst scriptBlockAst) + { + return Visit(scriptBlockAst); + } + + public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst) + { + return Visit(scriptBlockExpressionAst); + } + + public override AstVisitAction VisitStatementBlock(StatementBlockAst statementBlockAst) + { + return Visit(statementBlockAst); + } + + public override AstVisitAction VisitStringConstantExpression(StringConstantExpressionAst stringConstantExpressionAst) + { + return Visit(stringConstantExpressionAst); + } + + public override AstVisitAction VisitSubExpression(SubExpressionAst subExpressionAst) + { + return Visit(subExpressionAst); + } + + public override AstVisitAction VisitSwitchStatement(SwitchStatementAst switchStatementAst) + { + return Visit(switchStatementAst); + } + + public override AstVisitAction VisitThrowStatement(ThrowStatementAst throwStatementAst) + { + return Visit(throwStatementAst); + } + + public override AstVisitAction VisitTrap(TrapStatementAst trapStatementAst) + { + return Visit(trapStatementAst); + } + + public override AstVisitAction VisitTryStatement(TryStatementAst tryStatementAst) + { + return Visit(tryStatementAst); + } + + public override AstVisitAction VisitTypeConstraint(TypeConstraintAst typeConstraintAst) + { + return Visit(typeConstraintAst); + } + + public override AstVisitAction VisitTypeExpression(TypeExpressionAst typeExpressionAst) + { + return Visit(typeExpressionAst); + } + + public override AstVisitAction VisitUnaryExpression(UnaryExpressionAst unaryExpressionAst) + { + return Visit(unaryExpressionAst); + } + + public override AstVisitAction VisitUsingExpression(UsingExpressionAst usingExpressionAst) + { + return Visit(usingExpressionAst); + } + + public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) + { + return Visit(variableExpressionAst); + } + + public override AstVisitAction VisitWhileStatement(WhileStatementAst whileStatementAst) + { + return Visit(whileStatementAst); + } + +#if !(PSV3 || PSV4) + public override AstVisitAction VisitBaseCtorInvokeMemberExpression(BaseCtorInvokeMemberExpressionAst baseCtorInvokeMemberExpressionAst) + { + return Visit(baseCtorInvokeMemberExpressionAst); + } + + public override AstVisitAction VisitConfigurationDefinition(ConfigurationDefinitionAst configurationDefinitionAst) + { + return Visit(configurationDefinitionAst); + } + + public override AstVisitAction VisitDynamicKeywordStatement(DynamicKeywordStatementAst dynamicKeywordStatementAst) + { + return Visit(dynamicKeywordStatementAst); + } + + public override AstVisitAction VisitFunctionMember(FunctionMemberAst functionMemberAst) + { + return Visit(functionMemberAst); + } + + public override AstVisitAction VisitPropertyMember(PropertyMemberAst propertyMemberAst) + { + return Visit(propertyMemberAst); + } + + public override AstVisitAction VisitTypeDefinition(TypeDefinitionAst typeDefinitionAst) + { + return Visit(typeDefinitionAst); + } + + public override AstVisitAction VisitUsingStatement(UsingStatementAst usingStatementAst) + { + return AstVisitAction.Continue; + } +#endif + +#if !(NET462 || PSV7) // net462 includes V3,4,5 + public override AstVisitAction VisitPipelineChain(PipelineChainAst pipelineChainAst) + { + return Visit(pipelineChainAst); + } + + public override AstVisitAction VisitTernaryExpression(TernaryExpressionAst ternaryExpressionAst) + { + return Visit(ternaryExpressionAst); + } +#endif + + /// + /// Traverses the AST based on offests to find the leaf-most node which contains the provided . + /// This method implements the entire functionality of this visitor. All methods are overridden to simply invoke this one. + /// + /// Current AST node to process. + /// An indicating whether to visit children of the current node. + private AstVisitAction Visit(Ast ast) + { + if (ast.Extent.StartOffset > searchPosition.Offset || ast.Extent.EndOffset <= searchPosition.Offset) + { + return AstVisitAction.SkipChildren; + } + AstPosition = ast; + return AstVisitAction.Continue; + } + + } +} \ No newline at end of file diff --git a/Engine/Formatter.cs b/Engine/Formatter.cs index 15db1b39d..a6a25f0fb 100644 --- a/Engine/Formatter.cs +++ b/Engine/Formatter.cs @@ -32,7 +32,7 @@ public static string Format( ValidateNotNull(settings, "settings"); ValidateNotNull(cmdlet, "cmdlet"); - Helper.Instance = new Helper(cmdlet.SessionState.InvokeCommand, cmdlet); + Helper.Instance = new Helper(cmdlet.SessionState.InvokeCommand); Helper.Instance.Initialize(); var ruleOrder = new string[] @@ -44,6 +44,10 @@ public static string Format( "PSAlignAssignmentStatement", "PSUseCorrectCasing", "PSAvoidUsingCmdletAliases", + "PSAvoidUsingDoubleQuotesForConstantString", + "PSAvoidSemicolonsAsLineTerminators", + "PSAvoidExclaimOperator", + "PSAvoidTrailingWhitespace", }; var text = new EditableText(scriptDefinition); @@ -59,7 +63,7 @@ public static string Format( var currentSettings = GetCurrentSettings(settings, rule); ScriptAnalyzer.Instance.UpdateSettings(currentSettings); - ScriptAnalyzer.Instance.Initialize(cmdlet, null, null, null, null, true, false); + ScriptAnalyzer.Instance.Initialize(cmdlet, null, null, null, null, true, SuppressionPreference.Omit); text = ScriptAnalyzer.Instance.Fix(text, range, skipParsing, out Range updatedRange, out bool fixesWereApplied, ref scriptAst, ref scriptTokens, skipVariableAnalysis: true); skipParsing = !fixesWereApplied; diff --git a/Engine/Generic/CorrectionExtent.cs b/Engine/Generic/CorrectionExtent.cs index caad49cdb..f92070844 100644 --- a/Engine/Generic/CorrectionExtent.cs +++ b/Engine/Generic/CorrectionExtent.cs @@ -104,5 +104,13 @@ public CorrectionExtent( { } + + /// + /// Outputs a CorrectionExtent as a string. + /// + /// Returns the text in a CorrectionExtent. + public override string ToString() { + return this.Text; + } } } diff --git a/Engine/Generic/DiagnosticRecord.cs b/Engine/Generic/DiagnosticRecord.cs index a02f8a273..41eb86a05 100644 --- a/Engine/Generic/DiagnosticRecord.cs +++ b/Engine/Generic/DiagnosticRecord.cs @@ -74,7 +74,7 @@ public string ScriptPath } /// - /// Returns the rule id for this record + /// Returns the rule suppression id for this record /// public string RuleSuppressionID { @@ -88,18 +88,19 @@ public string RuleSuppressionID /// public IEnumerable SuggestedCorrections { - get { return suggestedCorrections; } + get { return suggestedCorrections; } set { suggestedCorrections = value; } } + public bool IsSuppressed { get; protected set; } = false; + /// /// DiagnosticRecord: The constructor for DiagnosticRecord class. /// public DiagnosticRecord() { - } - + /// /// DiagnosticRecord: The constructor for DiagnosticRecord class that takes in suggestedCorrection /// @@ -107,9 +108,17 @@ public DiagnosticRecord() /// The place in the script this diagnostic refers to /// The name of the rule that created this diagnostic /// The severity of this diagnostic + /// The rule suppression ID of this diagnostic /// The full path of the script file being analyzed /// The correction suggested by the rule to replace the extent text - public DiagnosticRecord(string message, IScriptExtent extent, string ruleName, DiagnosticSeverity severity, string scriptPath, string ruleId = null, IEnumerable suggestedCorrections = null) + public DiagnosticRecord( + string message, + IScriptExtent extent, + string ruleName, + DiagnosticSeverity severity, + string scriptPath, + string ruleId = null, + IEnumerable suggestedCorrections = null) { Message = message; RuleName = ruleName; @@ -120,6 +129,13 @@ public DiagnosticRecord(string message, IScriptExtent extent, string ruleName, D this.suggestedCorrections = suggestedCorrections; } + /// + /// Outputs a DiagnosticRecord as a string. + /// + /// Returns the message in a DiagnosticRecord. + public override string ToString() { + return this.Message; + } } diff --git a/Engine/Generic/ModuleDependencyHandler.cs b/Engine/Generic/ModuleDependencyHandler.cs index 347b9a9a1..31a43d6ca 100644 --- a/Engine/Generic/ModuleDependencyHandler.cs +++ b/Engine/Generic/ModuleDependencyHandler.cs @@ -22,7 +22,6 @@ public class ModuleDependencyHandler : IDisposable private string moduleRepository; private string tempPath; // path to the user temporary directory private string tempModulePath; // path to temp directory containing modules - Dictionary modulesFound; private string localAppdataPath; private string pssaAppDataPath; private const string symLinkName = "TempModuleDir"; @@ -271,8 +270,6 @@ public ModuleDependencyHandler( ? "PSScriptAnalyzer" : pssaAppDataPath); - modulesFound = new Dictionary(StringComparer.OrdinalIgnoreCase); - // TODO Add PSSA Version in the path symLinkPath = Path.Combine(pssaAppDataPath, symLinkName); SetupPSSAAppData(); diff --git a/Engine/Generic/RuleSuppression.cs b/Engine/Generic/RuleSuppression.cs index d8a41f974..d912eee0c 100644 --- a/Engine/Generic/RuleSuppression.cs +++ b/Engine/Generic/RuleSuppression.cs @@ -308,7 +308,7 @@ public static List GetSuppressions(IEnumerable at } IEnumerable suppressionAttribute = attrAsts.Where( - item => item.TypeName.GetReflectionType() == typeof(System.Diagnostics.CodeAnalysis.SuppressMessageAttribute)); + item => item.TypeName.GetReflectionAttributeType() == typeof(System.Diagnostics.CodeAnalysis.SuppressMessageAttribute)); foreach (var attributeAst in suppressionAttribute) { diff --git a/Engine/Generic/SuppressedRecord.cs b/Engine/Generic/SuppressedRecord.cs index ee50ea48b..66d1b4739 100644 --- a/Engine/Generic/SuppressedRecord.cs +++ b/Engine/Generic/SuppressedRecord.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Collections.Generic; +using System.Collections.ObjectModel; + namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic { /// @@ -9,22 +12,19 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic public class SuppressedRecord : DiagnosticRecord { /// - /// The rule suppression of this record + /// The rule suppressions applied to this record. /// - public RuleSuppression Suppression - { - get; - set; - } + public IReadOnlyList Suppression { get; set; } /// /// Creates a suppressed record based on a diagnostic record and the rule suppression /// /// /// - public SuppressedRecord(DiagnosticRecord record, RuleSuppression suppression) + public SuppressedRecord(DiagnosticRecord record, IReadOnlyList suppressions) { - Suppression = suppression; + Suppression = new ReadOnlyCollection(new List(suppressions)); + IsSuppressed = true; if (record != null) { RuleName = record.RuleName; diff --git a/Engine/Helper.cs b/Engine/Helper.cs index cbd8d0cd3..098d8a276 100644 --- a/Engine/Helper.cs +++ b/Engine/Helper.cs @@ -24,7 +24,6 @@ public class Helper #region Private members private CommandInvocationIntrinsics invokeCommand; - private IOutputWriter outputWriter; private readonly static Version minSupportedPSVersion = new Version(3, 0); private Dictionary> ruleArguments; private PSVersionTable psVersionTable; @@ -115,7 +114,7 @@ internal set /// private Helper() { - _commandInfoCacheLazy = new Lazy(() => new CommandInfoCache(pssaHelperInstance: this)); + _commandInfoCacheLazy = new Lazy(() => new CommandInfoCache()); } /// @@ -125,16 +124,11 @@ private Helper() /// A CommandInvocationIntrinsics instance for use in gathering /// information about available commands and aliases. /// - /// - /// An IOutputWriter instance for use in writing output - /// to the PowerShell environment. - /// public Helper( - CommandInvocationIntrinsics invokeCommand, - IOutputWriter outputWriter) : this() + CommandInvocationIntrinsics invokeCommand + ): this() { this.invokeCommand = invokeCommand; - this.outputWriter = outputWriter; } #region Methods @@ -615,14 +609,15 @@ public bool HasSplattedVariable(CommandAst cmdAst) /// /// /// - public bool IsKnownCmdletFunctionOrExternalScript(CommandAst cmdAst) + public bool IsKnownCmdletFunctionOrExternalScript(CommandAst cmdAst, out CommandInfo commandInfo) { + commandInfo = null; if (cmdAst == null) { return false; } - var commandInfo = GetCommandInfo(cmdAst.GetCommandName()); + commandInfo = GetCommandInfo(cmdAst.GetCommandName()); if (commandInfo == null) { return false; @@ -673,10 +668,11 @@ public bool PositionalParameterUsed(CommandAst cmdAst, bool moreThanTwoPositiona /// /// /// + /// /// - public CommandInfo GetCommandInfo(string name, CommandTypes? commandType = null) + public CommandInfo GetCommandInfo(string name, CommandTypes? commandType = null, bool bypassCache = false) { - return CommandInfoCache.GetCommandInfo(name, commandTypes: commandType); + return CommandInfoCache.GetCommandInfo(name, commandTypes: commandType, bypassCache: bypassCache); } /// @@ -765,8 +761,15 @@ public IScriptExtent GetScriptExtentForFunctionName(FunctionDefinitionAst functi token => ContainsExtent(functionDefinitionAst.Extent, token.Extent) && token.Text.Equals(functionDefinitionAst.Name)); - var funcNameToken = funcNameTokens.FirstOrDefault(); - return funcNameToken == null ? null : funcNameToken.Extent; + + // If the functions name is 'function' then the first token in the + // list is the function keyword itself, so we need to skip it + if (functionDefinitionAst.Name.Equals("function", StringComparison.OrdinalIgnoreCase)) + { + var funcNameToken = funcNameTokens.Skip(1).FirstOrDefault() ?? funcNameTokens.FirstOrDefault(); + return funcNameToken?.Extent; + } + return funcNameTokens.FirstOrDefault()?.Extent; } /// @@ -874,19 +877,13 @@ public bool IsUninitialized(VariableExpressionAst varAst, Ast ast) } /// - /// Returns true if varaible is either a global variable or an environment variable + /// Returns true if variable is either a global variable or an environment variable /// /// - /// /// - public bool IsVariableGlobalOrEnvironment(VariableExpressionAst varAst, Ast ast) + public bool IsVariableGlobalOrEnvironment(VariableExpressionAst varAst) { - if (!VariableAnalysisDictionary.ContainsKey(ast) || VariableAnalysisDictionary[ast] == null) - { - return false; - } - - return VariableAnalysisDictionary[ast].IsGlobalOrEnvironment(varAst); + return VariableAnalysis.IsGlobalOrEnvironment(varAst); } @@ -1322,156 +1319,177 @@ internal List GetSuppressionsConfiguration(ConfigurationDefinit /// Suppress the rules from the diagnostic records list. /// Returns a list of suppressed records as well as the ones that are not suppressed /// - /// - /// + /// The name of the rule possibly being suppressed. + /// A lookup table from rule name to suppressions of that rule. + /// The list of all diagnostics emitted by the given rule. + /// Any errors that arise due to misconfigured suppression settings. + /// A pair, with the first item being the list of suppressed diagnostics, and the second the list of unsuppressed diagnostics. public Tuple, List> SuppressRule( string ruleName, Dictionary> ruleSuppressionsDict, List diagnostics, - out List errorRecords) + out List errors) { - List suppressedRecords = new List(); - List unSuppressedRecords = new List(); - Tuple, List> result = Tuple.Create(suppressedRecords, unSuppressedRecords); - errorRecords = new List(); - if (diagnostics == null || diagnostics.Count == 0) + var suppressedRecords = new List(); + var unsuppressedRecords = new List(); + errors = new List(); + var result = new Tuple, List>(suppressedRecords, unsuppressedRecords); + + if (diagnostics is null || diagnostics.Count == 0) { return result; } - if (ruleSuppressionsDict == null || !ruleSuppressionsDict.ContainsKey(ruleName) - || ruleSuppressionsDict[ruleName].Count == 0) + if (ruleSuppressionsDict is null + || !ruleSuppressionsDict.TryGetValue(ruleName, out List ruleSuppressions) + || ruleSuppressions.Count == 0) { - unSuppressedRecords.AddRange(diagnostics); + unsuppressedRecords.AddRange(diagnostics); return result; } - List ruleSuppressions = ruleSuppressionsDict[ruleName]; - var offsetArr = GetOffsetArray(diagnostics); - int recordIndex = 0; - int startRecord = 0; - bool[] suppressed = new bool[diagnostics.Count]; - foreach (RuleSuppression ruleSuppression in ruleSuppressions) + // We need to report errors for any rule suppressions with IDs that are not applied to anything + // Start by assuming all suppressions are unapplied and then we'll remove them as we apply them later + var unappliedSuppressions = new HashSet(); + foreach (RuleSuppression suppression in ruleSuppressions) { - int suppressionCount = 0; - while (startRecord < diagnostics.Count - // && diagnostics[startRecord].Extent.StartOffset < ruleSuppression.StartOffset) - // && diagnostics[startRecord].Extent.StartLineNumber < ruleSuppression.st) - && offsetArr[startRecord] != null && offsetArr[startRecord].Item1 < ruleSuppression.StartOffset) + if (!string.IsNullOrWhiteSpace(suppression.RuleSuppressionID)) { - startRecord += 1; + unappliedSuppressions.Add(suppression); } - - // at this point, start offset of startRecord is greater or equals to rulesuppression.startoffset - recordIndex = startRecord; - - while (recordIndex < diagnostics.Count) + } + + // We have suppressions and diagnostics + // For each diagnostic, collect all the suppressions that apply to it + // and if there are any, record the diagnostic as suppressed + foreach (DiagnosticRecord diagnostic in diagnostics) + { + Tuple offsetPair = GetOffset(diagnostic); + + var appliedSuppressions = new List(); + foreach (RuleSuppression suppression in ruleSuppressions) { - DiagnosticRecord record = diagnostics[recordIndex]; - var curOffset = offsetArr[recordIndex]; - - //if (record.Extent.EndOffset > ruleSuppression.EndOffset) - if (curOffset != null && curOffset.Item2 > ruleSuppression.EndOffset) + // Check that the diagnostic is within the suppressed extent, if we have such an extent + if (!(offsetPair is null) + && (offsetPair.Item1 < suppression.StartOffset || offsetPair.Item2 > suppression.EndOffset)) { - break; + continue; } - - // we suppress if there is no suppression id or if there is suppression id and it matches - if (string.IsNullOrWhiteSpace(ruleSuppression.RuleSuppressionID) - || (!String.IsNullOrWhiteSpace(record.RuleSuppressionID) && - string.Equals(ruleSuppression.RuleSuppressionID, record.RuleSuppressionID, StringComparison.OrdinalIgnoreCase))) + + // If there's a rule suppression ID, check that it matches the diagnostic + if (!string.IsNullOrWhiteSpace(suppression.RuleSuppressionID) + && !string.Equals(diagnostic.RuleSuppressionID, suppression.RuleSuppressionID, StringComparison.OrdinalIgnoreCase)) { - suppressed[recordIndex] = true; - suppressedRecords.Add(new SuppressedRecord(record, ruleSuppression)); - suppressionCount += 1; + continue; } - - recordIndex += 1; + + unappliedSuppressions.Remove(suppression); + appliedSuppressions.Add(suppression); } - - // If we cannot found any error but the rulesuppression has a rulesuppressionid then it must be used wrongly - if (!String.IsNullOrWhiteSpace(ruleSuppression.RuleSuppressionID) && suppressionCount == 0) + + if (appliedSuppressions.Count > 0) { - // checks whether are given a string or a file path - if (String.IsNullOrWhiteSpace(diagnostics.First().Extent.File)) - { - ruleSuppression.Error = String.Format(CultureInfo.CurrentCulture, Strings.RuleSuppressionErrorFormatScriptDefinition, ruleSuppression.StartAttributeLine, - String.Format(Strings.RuleSuppressionIDError, ruleSuppression.RuleSuppressionID)); - } - else - { - ruleSuppression.Error = String.Format(CultureInfo.CurrentCulture, Strings.RuleSuppressionErrorFormat, ruleSuppression.StartAttributeLine, - System.IO.Path.GetFileName(diagnostics.First().Extent.File), String.Format(Strings.RuleSuppressionIDError, ruleSuppression.RuleSuppressionID)); - } - errorRecords.Add(new ErrorRecord(new ArgumentException(ruleSuppression.Error), ruleSuppression.Error, ErrorCategory.InvalidArgument, ruleSuppression)); - //this.outputWriter.WriteError(new ErrorRecord(new ArgumentException(ruleSuppression.Error), ruleSuppression.Error, ErrorCategory.InvalidArgument, ruleSuppression)); + suppressedRecords.Add(new SuppressedRecord(diagnostic, appliedSuppressions)); + } + else + { + unsuppressedRecords.Add(diagnostic); } } - - for (int i = 0; i < suppressed.Length; i += 1) + + // Do any error reporting for misused RuleSuppressionIDs here. + // If the user applied a suppression qualified by a suppression ID, + // but that suppression didn't apply only because the suppression ID didn't match + // we let the user know in the form of an error. + string analyzedFile = diagnostics[0]?.Extent?.File; + bool appliedToFile = !string.IsNullOrEmpty(analyzedFile); + string analyzedFileName = appliedToFile ? Path.GetFileName(analyzedFile) : null; + foreach (RuleSuppression unappliedSuppression in unappliedSuppressions) { - if (!suppressed[i]) + string suppressionIDError = string.Format(Strings.RuleSuppressionIDError, unappliedSuppression.RuleSuppressionID); + + if (appliedToFile) { - unSuppressedRecords.Add(diagnostics[i]); + unappliedSuppression.Error = string.Format( + CultureInfo.CurrentCulture, + Strings.RuleSuppressionErrorFormat, + unappliedSuppression.StartAttributeLine, + analyzedFileName, + suppressionIDError); } + else + { + unappliedSuppression.Error = string.Format( + CultureInfo.CurrentCulture, + Strings.RuleSuppressionErrorFormatScriptDefinition, + unappliedSuppression.StartAttributeLine, + suppressionIDError); + } + + errors.Add( + new ErrorRecord( + new ArgumentException(unappliedSuppression.Error), + unappliedSuppression.Error, + ErrorCategory.InvalidArgument, + unappliedSuppression)); } return result; } - private Tuple[] GetOffsetArray(List diagnostics) + private Tuple GetOffset(DiagnosticRecord diagnostic) { - Func> GetTuple = (x, y) => new Tuple(x, y); - Func> GetDefaultTuple = () => GetTuple(0, 0); - var offsets = new Tuple[diagnostics.Count]; - for (int k = 0; k < diagnostics.Count; k++) + IScriptExtent extent = diagnostic.Extent; + + if (extent is null) { - var ext = diagnostics[k].Extent; - if (ext == null) + return null; + } + + if (extent.StartOffset != 0 && extent.EndOffset != 0) + { + return Tuple.Create(extent.StartOffset, extent.EndOffset); + } + + // We're forced to determine the offset ourselves from the lines and columns now + // Rather than counting every line in the file, we scan through the tokens looking for ones matching the offset + + Token startToken = null; + int i = 0; + for (; i < Tokens.Length; i++) + { + Token curr = Tokens[i]; + if (curr.Extent.StartLineNumber == extent.StartLineNumber + && curr.Extent.StartColumnNumber == extent.StartColumnNumber) { - continue; + startToken = curr; + break; } - if (ext.StartOffset == 0 && ext.EndOffset == 0) - { - // check if line and column number correspond to 0 offsets - if (ext.StartLineNumber == 1 - && ext.StartColumnNumber == 1 - && ext.EndLineNumber == 1 - && ext.EndColumnNumber == 1) - { - offsets[k] = GetDefaultTuple(); - continue; - } - // created using the ScriptExtent constructor, which sets - // StartOffset and EndOffset to 0 - // find the token the corresponding start line and column number - var startToken = Tokens.Where(x - => x.Extent.StartLineNumber == ext.StartLineNumber - && x.Extent.StartColumnNumber == ext.StartColumnNumber) - .FirstOrDefault(); - if (startToken == null) - { - offsets[k] = GetDefaultTuple(); - continue; - } - var endToken = Tokens.Where(x - => x.Extent.EndLineNumber == ext.EndLineNumber - && x.Extent.EndColumnNumber == ext.EndColumnNumber) - .FirstOrDefault(); - if (endToken == null) - { - offsets[k] = GetDefaultTuple(); - continue; - } - offsets[k] = GetTuple(startToken.Extent.StartOffset, endToken.Extent.EndOffset); - } - else + } + + if (startToken is null) + { + return Tuple.Create(0, 0); + } + + Token endToken = null; + for (; i < Tokens.Length; i++) + { + Token curr = Tokens[i]; + if (curr.Extent.EndLineNumber == extent.EndLineNumber + && curr.Extent.EndColumnNumber == extent.EndColumnNumber) { - // Extent has valid offsets - offsets[k] = GetTuple(ext.StartOffset, ext.EndOffset); + endToken = curr; + break; } } - return offsets; + + if (endToken is null) + { + return Tuple.Create(0, 0); + } + + return Tuple.Create(startToken.Extent.StartOffset, endToken.Extent.EndOffset); } public static string[] ProcessCustomRulePaths(string[] rulePaths, SessionState sessionState, bool recurse = false) diff --git a/Engine/PSScriptAnalyzer.psd1 b/Engine/PSScriptAnalyzer.psd1 index 716464e34..49fb93227 100644 --- a/Engine/PSScriptAnalyzer.psd1 +++ b/Engine/PSScriptAnalyzer.psd1 @@ -11,7 +11,7 @@ Author = 'Microsoft Corporation' RootModule = 'PSScriptAnalyzer.psm1' # Version number of this module. -ModuleVersion = '1.19.0' +ModuleVersion = '{{ModuleVersion}}' # ID used to uniquely identify this module GUID = 'd6245802-193d-4068-a631-8863a4342a18' @@ -20,13 +20,13 @@ GUID = 'd6245802-193d-4068-a631-8863a4342a18' CompanyName = 'Microsoft Corporation' # Copyright statement for this module -Copyright = '(c) Microsoft Corporation 2016. All rights reserved.' +Copyright = '(c) Microsoft Corporation 2025. All rights reserved.' # Description of the functionality provided by this module Description = 'PSScriptAnalyzer provides script analysis and checks for potential code defects in the scripts by applying a group of built-in or customized rules on the scripts being analyzed.' # Minimum version of the Windows PowerShell engine required by this module -PowerShellVersion = '3.0' +PowerShellVersion = '5.1' # Name of the Windows PowerShell host required by this module # PowerShellHostName = '' @@ -83,15 +83,15 @@ AliasesToExport = @() PrivateData = @{ PSData = @{ Tags = 'lint', 'bestpractice' - LicenseUri = 'https://github.com/PowerShell/PSScriptAnalyzer/blob/master/LICENSE' + LicenseUri = 'https://github.com/PowerShell/PSScriptAnalyzer/blob/main/LICENSE' ProjectUri = 'https://github.com/PowerShell/PSScriptAnalyzer' - IconUri = 'https://raw.githubusercontent.com/powershell/psscriptanalyzer/master/logo.png' + IconUri = 'https://raw.githubusercontent.com/powershell/psscriptanalyzer/main/logo.png' ReleaseNotes = '' } } # HelpInfo URI of this module -# HelpInfoURI = '' +HelpInfoURI = 'https://aka.ms/ps-modules-help' # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. # DefaultCommandPrefix = '' diff --git a/Engine/PSScriptAnalyzer.psm1 b/Engine/PSScriptAnalyzer.psm1 index e98e1c88a..121e96d6f 100644 --- a/Engine/PSScriptAnalyzer.psm1 +++ b/Engine/PSScriptAnalyzer.psm1 @@ -9,15 +9,13 @@ $PSModuleRoot = $PSModule.ModuleBase # Import the appropriate nested binary module based on the current PowerShell version $binaryModuleRoot = $PSModuleRoot -if ($PSVersionTable.PSVersion.Major -eq 7 ) { - $binaryModuleRoot = Join-Path -Path $PSModuleRoot -ChildPath "PSv$($PSVersionTable.PSVersion.Major)" -} -elseif ($PSVersionTable.PSVersion.Major -eq 6 ) { +[Version] $minimumPowerShellCoreVersion = '7.4.7' +if ($PSVersionTable.PSVersion.Major -ge 6) { $binaryModuleRoot = Join-Path -Path $PSModuleRoot -ChildPath "PSv$($PSVersionTable.PSVersion.Major)" # Minimum PowerShell Core version given by PowerShell Core support itself and - # also the version of NewtonSoft.Json implicitly that PSSA ships with, - # which cannot be higher than the one that PowerShell ships with. - [Version] $minimumPowerShellCoreVersion = '6.2.4' + # the version of Nuget references, such as e.g. Newtonsoft.Json inside PowerShell itself + # or the version of the System.Management.Automation NuGet reference in PSSA. + if ($PSVersionTable.PSVersion -lt $minimumPowerShellCoreVersion) { throw "Minimum supported version of PSScriptAnalyzer for PowerShell Core is $minimumPowerShellCoreVersion but current version is '$($PSVersionTable.PSVersion)'. Please update PowerShell Core." } diff --git a/Engine/ScriptAnalyzer.cs b/Engine/ScriptAnalyzer.cs index ffe277375..f250336b5 100644 --- a/Engine/ScriptAnalyzer.cs +++ b/Engine/ScriptAnalyzer.cs @@ -22,9 +22,17 @@ using System.Collections; using System.Diagnostics; using System.Text; +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Extensions; namespace Microsoft.Windows.PowerShell.ScriptAnalyzer { + internal enum SuppressionPreference + { + Omit = 0, + Include = 1, + SuppressedOnly = 2, + } + public sealed class ScriptAnalyzer { #region Private members @@ -41,7 +49,7 @@ public sealed class ScriptAnalyzer string[] severity; List includeRegexList; List excludeRegexList; - bool suppressedOnly; + private SuppressionPreference _suppressionPreference; #if !PSV3 ModuleDependencyHandler moduleHandler; #endif @@ -118,7 +126,7 @@ internal void Initialize( string[] excludeRuleNames = null, string[] severity = null, bool includeDefaultRules = false, - bool suppressedOnly = false) + SuppressionPreference suppressionPreference = SuppressionPreference.Omit) where TCmdlet : PSCmdlet, IOutputWriter { if (cmdlet == null) @@ -135,7 +143,7 @@ internal void Initialize( excludeRuleNames, severity, includeDefaultRules, - suppressedOnly); + suppressionPreference); } /// @@ -150,6 +158,7 @@ public void Initialize( string[] severity = null, bool includeDefaultRules = false, bool suppressedOnly = false, + bool includeSuppression = false, string profile = null) { if (runspace == null) @@ -159,10 +168,14 @@ public void Initialize( //initialize helper Helper.Instance = new Helper( - runspace.SessionStateProxy.InvokeCommand, - outputWriter); + runspace.SessionStateProxy.InvokeCommand); Helper.Instance.Initialize(); + SuppressionPreference suppressionPreference = suppressedOnly + ? SuppressionPreference.SuppressedOnly + : includeSuppression + ? SuppressionPreference.Include + : SuppressionPreference.Omit; this.Initialize( outputWriter, @@ -173,7 +186,7 @@ public void Initialize( excludeRuleNames, severity, includeDefaultRules, - suppressedOnly, + suppressionPreference, profile); } @@ -187,7 +200,7 @@ public void CleanUp() severity = null; includeRegexList = null; excludeRegexList = null; - suppressedOnly = false; + _suppressionPreference = SuppressionPreference.Omit; } /// @@ -671,7 +684,7 @@ private void Initialize( string[] excludeRuleNames, string[] severity, bool includeDefaultRules = false, - bool suppressedOnly = false, + SuppressionPreference suppressionPreference = SuppressionPreference.Omit, string profile = null) { if (outputWriter == null) @@ -730,7 +743,7 @@ private void Initialize( } } - this.suppressedOnly = suppressedOnly; + _suppressionPreference = suppressionPreference; this.includeRegexList = new List(); this.excludeRegexList = new List(); @@ -809,13 +822,13 @@ private void Initialize( // Ensure that rules were actually loaded if (rules == null || rules.Any() == false) { + string errorMessage = string.Format(CultureInfo.CurrentCulture, Strings.RulesNotFound); + this.outputWriter.ThrowTerminatingError( new ErrorRecord( - new Exception(), - string.Format( - CultureInfo.CurrentCulture, - Strings.RulesNotFound), - ErrorCategory.ResourceExists, + new Exception(errorMessage), + errorMessage, + ErrorCategory.ObjectNotFound, this)); } @@ -1250,13 +1263,6 @@ internal IEnumerable GetExternalRecord(Ast ast, Token[] token, foreach (var psobject in psobjects) { - DiagnosticSeverity severity; - IScriptExtent extent; - string message = string.Empty; - string ruleName = string.Empty; - string ruleSuppressionID = string.Empty; - IEnumerable suggestedCorrections; - if (psobject != null && psobject.ImmediateBaseObject != null) { // Because error stream is merged to output stream, @@ -1269,28 +1275,9 @@ internal IEnumerable GetExternalRecord(Ast ast, Token[] token, } // DiagnosticRecord may not be correctly returned from external rule. - try - { - severity = (DiagnosticSeverity)Enum.Parse(typeof(DiagnosticSeverity), psobject.Properties["Severity"].Value.ToString()); - message = psobject.Properties["Message"].Value.ToString(); - extent = (IScriptExtent)psobject.Properties["Extent"].Value; - ruleName = psobject.Properties["RuleName"].Value.ToString(); - ruleSuppressionID = psobject.Properties["RuleSuppressionID"].Value?.ToString(); - suggestedCorrections = (IEnumerable)psobject.Properties["SuggestedCorrections"].Value; - } - catch (Exception ex) - { - this.outputWriter.WriteError(new ErrorRecord(ex, ex.HResult.ToString("X"), ErrorCategory.NotSpecified, this)); - continue; - } - - if (!string.IsNullOrEmpty(message)) + if (TryConvertPSObjectToDiagnostic(psobject, filePath, out DiagnosticRecord diagnostic)) { - diagnostics.Add(new DiagnosticRecord(message, extent, ruleName, severity, filePath) - { - SuggestedCorrections = suggestedCorrections, - RuleSuppressionID = ruleSuppressionID, - }); + diagnostics.Add(diagnostic); } } } @@ -1501,7 +1488,7 @@ public IEnumerable AnalyzeAndFixPath(string path, FuncParsed tokens of /// Whether variable analysis can be skipped (applicable if rules do not use variable analysis APIs). /// - public IEnumerable AnalyzeScriptDefinition(string scriptDefinition, out ScriptBlockAst scriptAst, out Token[] scriptTokens, bool skipVariableAnalysis = false) + public List AnalyzeScriptDefinition(string scriptDefinition, out ScriptBlockAst scriptAst, out Token[] scriptTokens, bool skipVariableAnalysis = false) { scriptAst = null; scriptTokens = null; @@ -1516,7 +1503,7 @@ public IEnumerable AnalyzeScriptDefinition(string scriptDefini catch (Exception e) { this.outputWriter.WriteWarning(e.ToString()); - return null; + return new(); } var relevantParseErrors = RemoveTypeNotFoundParseErrors(errors, out List diagnosticRecords); @@ -1541,7 +1528,8 @@ public IEnumerable AnalyzeScriptDefinition(string scriptDefini } // now, analyze the script definition - return diagnosticRecords.Concat(this.AnalyzeSyntaxTree(scriptAst, scriptTokens, String.Empty, skipVariableAnalysis)); + diagnosticRecords.AddRange(this.AnalyzeSyntaxTree(scriptAst, scriptTokens, null, skipVariableAnalysis)); + return diagnosticRecords; } /// @@ -1647,6 +1635,57 @@ public EditableText Fix(EditableText text, Range range, bool skipParsing, out Ra return text; } + private bool TryConvertPSObjectToDiagnostic(PSObject psObject, string filePath, out DiagnosticRecord diagnostic) + { + string message = psObject.Properties["Message"]?.Value?.ToString(); + object extentValue = psObject.Properties["Extent"]?.Value; + string ruleName = psObject.Properties["RuleName"]?.Value?.ToString(); + string ruleSuppressionID = psObject.Properties["RuleSuppressionID"]?.Value?.ToString(); + CorrectionExtent[] suggestedCorrections = psObject.TryGetPropertyValue("SuggestedCorrections", out object correctionsValue) + ? LanguagePrimitives.ConvertTo(correctionsValue) + : null; + DiagnosticSeverity severity = psObject.TryGetPropertyValue("Severity", out object severityValue) + ? LanguagePrimitives.ConvertTo(severityValue) + : DiagnosticSeverity.Warning; + + bool isValid = true; + isValid &= CheckHasRequiredProperty("Message", message); + isValid &= CheckHasRequiredProperty("RuleName", ruleName); + + if (extentValue is not null && extentValue is not IScriptExtent) + { + this.outputWriter.WriteError( + new ErrorRecord( + new ArgumentException($"Property 'Extent' is expected to be of type '{typeof(IScriptExtent)}' but was instead of type '{extentValue.GetType()}'"), + "CustomRuleDiagnosticPropertyInvalidType", + ErrorCategory.InvalidArgument, + this)); + isValid = false; + } + + if (!isValid) + { + diagnostic = null; + return false; + } + + diagnostic = new DiagnosticRecord(message, (IScriptExtent)extentValue, ruleName, severity, filePath, ruleSuppressionID, suggestedCorrections); + return true; + } + + private bool CheckHasRequiredProperty(string propertyName, object propertyValue) + { + if (propertyValue is null) + { + var exception = new ArgumentNullException(propertyName, $"The '{propertyName}' property is required on custom rule diagnostics"); + this.outputWriter.WriteError(new ErrorRecord(exception, "CustomRuleDiagnosticPropertyMissing", ErrorCategory.InvalidArgument, this)); + return false; + } + + return true; + } + + private static Encoding GetFileEncoding(string path) { using (var stream = new FileStream(path, FileMode.Open)) @@ -2324,9 +2363,13 @@ public IEnumerable AnalyzeSyntaxTree( // Need to reverse the concurrentbag to ensure that results are sorted in the increasing order of line numbers IEnumerable diagnosticsList = diagnostics.Reverse(); - return this.suppressedOnly ? - suppressed.OfType() : - diagnosticsList; + return _suppressionPreference switch + { + SuppressionPreference.SuppressedOnly => suppressed.OfType(), + SuppressionPreference.Omit => diagnosticsList, + SuppressionPreference.Include => diagnosticsList.Concat(suppressed.OfType()), + _ => throw new ArgumentException($"SuppressionPreference has invalid value '{_suppressionPreference}'"), + }; } } } diff --git a/Engine/ScriptAnalyzer.types.ps1xml b/Engine/ScriptAnalyzer.types.ps1xml index da989657e..03bf3937e 100644 --- a/Engine/ScriptAnalyzer.types.ps1xml +++ b/Engine/ScriptAnalyzer.types.ps1xml @@ -79,7 +79,7 @@ DefaultDisplayPropertySet - Name + RuleName Severity Description SourceName diff --git a/Engine/Settings.cs b/Engine/Settings.cs index b13f9405b..b0c424c64 100644 --- a/Engine/Settings.cs +++ b/Engine/Settings.cs @@ -285,17 +285,6 @@ private Dictionary GetDictionaryFromHashtable(Hashtable hashtabl return dictionary; } - private bool IsStringOrStringArray(object val) - { - if (val is string) - { - return true; - } - - var valArr = val as object[]; - return val == null ? false : valArr.All(x => x is string); - } - private List GetData(object val, string key) { // value must be either string or or an array of strings @@ -508,6 +497,13 @@ private static bool IsBuiltinSettingPreset(object settingPreset) internal static SettingsMode FindSettingsMode(object settings, string path, out object settingsFound) { var settingsMode = SettingsMode.None; + + // if the provided settings argument is wrapped in an expressions then PowerShell resolves it but it will be of type PSObject and we have to operate then on the BaseObject + if (settings is PSObject settingsFoundPSObject) + { + settings = settingsFoundPSObject.BaseObject; + } + settingsFound = settings; if (settingsFound == null) { @@ -543,11 +539,6 @@ internal static SettingsMode FindSettingsMode(object settings, string path, out { settingsMode = SettingsMode.Hashtable; } - // if the provided argument is wrapped in an expressions then PowerShell resolves it but it will be of type PSObject and we have to operate then on the BaseObject - else if (settingsFound is PSObject settingsFoundPSObject) - { - TryResolveSettingForStringType(settingsFoundPSObject.BaseObject, ref settingsMode, ref settingsFound); - } } } diff --git a/Engine/Settings/CodeFormatting.psd1 b/Engine/Settings/CodeFormatting.psd1 index 0676fccf1..8b53c1ae4 100644 --- a/Engine/Settings/CodeFormatting.psd1 +++ b/Engine/Settings/CodeFormatting.psd1 @@ -40,6 +40,7 @@ CheckPipeForRedundantWhitespace = $false CheckSeparator = $true CheckParameter = $false + IgnoreAssignmentOperatorInsideHashTable = $true } PSAlignAssignmentStatement = @{ diff --git a/Engine/Settings/CodeFormattingAllman.psd1 b/Engine/Settings/CodeFormattingAllman.psd1 index a0c649457..188b2c651 100644 --- a/Engine/Settings/CodeFormattingAllman.psd1 +++ b/Engine/Settings/CodeFormattingAllman.psd1 @@ -40,6 +40,7 @@ CheckPipeForRedundantWhitespace = $false CheckSeparator = $true CheckParameter = $false + IgnoreAssignmentOperatorInsideHashTable = $true } PSAlignAssignmentStatement = @{ diff --git a/Engine/Settings/CodeFormattingOTBS.psd1 b/Engine/Settings/CodeFormattingOTBS.psd1 index 21f4c0995..0e966bc46 100644 --- a/Engine/Settings/CodeFormattingOTBS.psd1 +++ b/Engine/Settings/CodeFormattingOTBS.psd1 @@ -40,6 +40,7 @@ CheckPipeForRedundantWhitespace = $false CheckSeparator = $true CheckParameter = $false + IgnoreAssignmentOperatorInsideHashTable = $true } PSAlignAssignmentStatement = @{ diff --git a/Engine/Settings/CodeFormattingStroustrup.psd1 b/Engine/Settings/CodeFormattingStroustrup.psd1 index 34ea924a5..d9bb16be5 100644 --- a/Engine/Settings/CodeFormattingStroustrup.psd1 +++ b/Engine/Settings/CodeFormattingStroustrup.psd1 @@ -41,6 +41,7 @@ CheckPipeForRedundantWhitespace = $false CheckSeparator = $true CheckParameter = $false + IgnoreAssignmentOperatorInsideHashTable = $true } PSAlignAssignmentStatement = @{ diff --git a/Engine/SpecialVars.cs b/Engine/SpecialVars.cs index 1e57028cf..3416a5ad2 100644 --- a/Engine/SpecialVars.cs +++ b/Engine/SpecialVars.cs @@ -28,6 +28,7 @@ internal class SpecialVars internal const string Matches = "Matches"; internal const string PSVersionTable = "PSVersionTable"; internal const string OFS = "OFS"; + internal const string FormatEnumerationLimit = "FormatEnumerationLimit"; internal static readonly string[] InitializedVariables; @@ -59,8 +60,9 @@ static SpecialVars() PSCommandPath, ExecutionContext, Matches, - PSVersionTable, - OFS + PSVersionTable, + OFS, + FormatEnumerationLimit, }; internal static readonly Type[] AutomaticVariableTypes = new Type[] { @@ -76,7 +78,8 @@ static SpecialVars() /* ExecutionContext */ typeof(EngineIntrinsics), /* Matches */ typeof(System.Collections.Hashtable), /* PSVersionTable */ typeof(System.Collections.Hashtable), - /* OFS */ typeof(object) + /* OFS */ typeof(object), + /* FormatEnumerationLimit */ typeof(int) }; @@ -88,6 +91,8 @@ static SpecialVars() internal const string ConfirmPreference = "ConfirmPreference"; internal const string ProgressPreference = "ProgressPreference"; internal const string InformationPreference = "InformationPreference"; + internal const string ErrorView = "ErrorView"; + internal const string PSNativeCommandUseErrorActionPreference = "PSNativeCommandUseErrorActionPreference"; internal static readonly string[] PreferenceVariables = new string[] { @@ -98,7 +103,9 @@ static SpecialVars() WarningPreference, ConfirmPreference, ProgressPreference, - InformationPreference + InformationPreference, + ErrorView, + PSNativeCommandUseErrorActionPreference, }; internal static readonly Type[] PreferenceVariableTypes = new Type[] @@ -111,6 +118,8 @@ static SpecialVars() /* ConfirmPreference */ typeof(ConfirmImpact), /* ProgressPreference */ typeof(Enum), /* InformationPreference */ typeof(ActionPreference), + /* ErrorView */ typeof(Enum), //ErrorView type not available on PS3 + /* PSNativeCommandUseErrorActionPreference */ typeof(bool), }; internal enum AutomaticVariable @@ -151,6 +160,7 @@ internal enum PreferenceVariable internal const string PSEmailServer = "PSEmailServer"; internal const string PSDefaultParameterValues = "PSDefaultParameterValues"; internal const string PSModuleAutoLoadingPreference = "PSModuleAutoLoadingPreference"; + internal const string PSNativeCommandArgumentPassing = "PSNativeCommandArgumentPassing"; internal const string pwd = "PWD"; internal const string Null = "null"; internal const string True = "true"; @@ -173,6 +183,7 @@ internal enum PreferenceVariable PSEmailServer, PSDefaultParameterValues, PSModuleAutoLoadingPreference, + PSNativeCommandArgumentPassing, pwd, Null, True, diff --git a/Engine/Strings.Designer.cs b/Engine/Strings.Designer.cs deleted file mode 100644 index 1fed6e234..000000000 --- a/Engine/Strings.Designer.cs +++ /dev/null @@ -1,684 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Microsoft.Windows.PowerShell.ScriptAnalyzer { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Strings { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Strings() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Windows.PowerShell.ScriptAnalyzer.Strings", typeof(Strings).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Checking assembly file '{0}' .... - /// - internal static string CheckAssemblyFile { - get { - return ResourceManager.GetString("CheckAssemblyFile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Checking module '{0}' .... - /// - internal static string CheckModuleName { - get { - return ResourceManager.GetString("CheckModuleName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to CommandInfo not found for function: {0}. - /// - internal static string CommandInfoNotFound { - get { - return ResourceManager.GetString("CommandInfoNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "Argument should not be null.".. - /// - internal static string ConfigurableScriptRuleNRE { - get { - return ResourceManager.GetString("ConfigurableScriptRuleNRE", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "Cannot find a ConfigurableRuleProperty attribute on property {0}".. - /// - internal static string ConfigurableScriptRulePropertyHasNotAttribute { - get { - return ResourceManager.GetString("ConfigurableScriptRulePropertyHasNotAttribute", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SettingsFileHasInvalidHashtable. - /// - internal static string ConfigurationFileHasInvalidHashtable { - get { - return ResourceManager.GetString("ConfigurationFileHasInvalidHashtable", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SettingsFileHasNoHashTable. - /// - internal static string ConfigurationFileHasNoHashTable { - get { - return ResourceManager.GetString("ConfigurationFileHasNoHashTable", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SettingsFileNotFound. - /// - internal static string ConfigurationFileNotFound { - get { - return ResourceManager.GetString("ConfigurationFileNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SettingsKeyNotAString. - /// - internal static string ConfigurationKeyNotAString { - get { - return ResourceManager.GetString("ConfigurationKeyNotAString", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SettingsValueNotAString. - /// - internal static string ConfigurationValueNotAString { - get { - return ResourceManager.GetString("ConfigurationValueNotAString", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SettingsValueWrongFormat. - /// - internal static string ConfigurationValueWrongFormat { - get { - return ResourceManager.GetString("ConfigurationValueWrongFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Writes all diagnostics to WriteObject.. - /// - internal static string DefaultLoggerDescription { - get { - return ResourceManager.GetString("DefaultLoggerDescription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to WriteObjects. - /// - internal static string DefaultLoggerName { - get { - return ResourceManager.GetString("DefaultLoggerName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Edge from {0} to {1} already exists.. - /// - internal static string DigraphEdgeAlreadyExists { - get { - return ResourceManager.GetString("DigraphEdgeAlreadyExists", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Vertex {0} already exists! Cannot add it to the digraph.. - /// - internal static string DigraphVertexAlreadyExists { - get { - return ResourceManager.GetString("DigraphVertexAlreadyExists", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Vertex {0} does not exist in the digraph.. - /// - internal static string DigraphVertexDoesNotExists { - get { - return ResourceManager.GetString("DigraphVertexDoesNotExists", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot determine line endings as the text probably contain mixed line endings.. - /// - internal static string EditableTextInvalidLineEnding { - get { - return ResourceManager.GetString("EditableTextInvalidLineEnding", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to TextEdit extent not completely contained in EditableText.. - /// - internal static string EditableTextRangeIsNotContained { - get { - return ResourceManager.GetString("EditableTextRangeIsNotContained", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot find file '{0}'.. - /// - internal static string FileNotFound { - get { - return ResourceManager.GetString("FileNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot find the path '{0}'.. - /// - internal static string InvalidPath { - get { - return ResourceManager.GetString("InvalidPath", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Settings file '{0}' is invalid because it does not contain a hashtable.. - /// - internal static string InvalidProfile { - get { - return ResourceManager.GetString("InvalidProfile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Key {0} in the settings is not a string.. - /// - internal static string KeyNotString { - get { - return ResourceManager.GetString("KeyNotString", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to No loggers found.. - /// - internal static string LoggersNotFound { - get { - return ResourceManager.GetString("LoggersNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot find rule extension '{0}'.. - /// - internal static string MissingRuleExtension { - get { - return ResourceManager.GetString("MissingRuleExtension", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Temporary module location: {0}.. - /// - internal static string ModuleDepHandlerTempLocation { - get { - return ResourceManager.GetString("ModuleDepHandlerTempLocation", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} cannot be set by both positional and named arguments.. - /// - internal static string NamedAndPositionalArgumentsConflictError { - get { - return ResourceManager.GetString("NamedAndPositionalArgumentsConflictError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Named arguments must always come after positional arguments.. - /// - internal static string NamedArgumentsBeforePositionalError { - get { - return ResourceManager.GetString("NamedArgumentsBeforePositionalError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to RuleName must not be null.. - /// - internal static string NullRuleNameError { - get { - return ResourceManager.GetString("NullRuleNameError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Parse error in script definition: {0} at line {1} column {2}.. - /// - internal static string ParseErrorFormatForScriptDefinition { - get { - return ResourceManager.GetString("ParseErrorFormatForScriptDefinition", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Parse error in file {0}: {1} at line {2} column {3}.. - /// - internal static string ParserErrorFormat { - get { - return ResourceManager.GetString("ParserErrorFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to There are too many parser errors in {0}. Please correct them before running ScriptAnalyzer.. - /// - internal static string ParserErrorMessage { - get { - return ResourceManager.GetString("ParserErrorMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to There are too many parser errors in the script definition. Please correct them before running ScriptAnalyzer.. - /// - internal static string ParserErrorMessageForScriptDefinition { - get { - return ResourceManager.GetString("ParserErrorMessageForScriptDefinition", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Column number cannot be less than 1.. - /// - internal static string PositionColumnLessThanOne { - get { - return ResourceManager.GetString("PositionColumnLessThanOne", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Line number cannot be less than 1.. - /// - internal static string PositionLineLessThanOne { - get { - return ResourceManager.GetString("PositionLineLessThanOne", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Input position should be less than that of the invoking object.. - /// - internal static string PositionRefPosLessThanInputPos { - get { - return ResourceManager.GetString("PositionRefPosLessThanInputPos", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Reference Position should begin before start Position of Range.. - /// - internal static string RangeRefPosShouldStartBeforeRangeStartPos { - get { - return ResourceManager.GetString("RangeRefPosShouldStartBeforeRangeStartPos", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Start position cannot be before End position.. - /// - internal static string RangeStartPosGreaterThanEndPos { - get { - return ResourceManager.GetString("RangeStartPosGreaterThanEndPos", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to RULE_ERROR. - /// - internal static string RuleErrorMessage { - get { - return ResourceManager.GetString("RuleErrorMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot find ScriptAnalyzer rules in the specified path. - /// - internal static string RulesNotFound { - get { - return ResourceManager.GetString("RulesNotFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Suppression Message Attribute error at line {0} in {1} : {2}. - /// - internal static string RuleSuppressionErrorFormat { - get { - return ResourceManager.GetString("RuleSuppressionErrorFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Suppression Message Attribute error at line {0} in script definition : {1}. - /// - internal static string RuleSuppressionErrorFormatScriptDefinition { - get { - return ResourceManager.GetString("RuleSuppressionErrorFormatScriptDefinition", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot find any DiagnosticRecord with the Rule Suppression ID {0}.. - /// - internal static string RuleSuppressionIDError { - get { - return ResourceManager.GetString("RuleSuppressionIDError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Found {0}. Will use it to provide settings for this invocation.. - /// - internal static string SettingsAutoDiscovered { - get { - return ResourceManager.GetString("SettingsAutoDiscovered", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot resolve settings file path '{0}'.. - /// - internal static string SettingsCannotFindFile { - get { - return ResourceManager.GetString("SettingsCannotFindFile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Dictionary should be indexable in a case-insensitive manner.. - /// - internal static string SettingsDictionaryShouldBeCaseInsesitive { - get { - return ResourceManager.GetString("SettingsDictionaryShouldBeCaseInsesitive", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Input should be a dictionary type.. - /// - internal static string SettingsInputShouldBeDictionary { - get { - return ResourceManager.GetString("SettingsInputShouldBeDictionary", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Settings should be either a file path, built-in preset or a hashtable.. - /// - internal static string SettingsInvalidType { - get { - return ResourceManager.GetString("SettingsInvalidType", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot parse settings. Will abort the invocation.. - /// - internal static string SettingsNotParsable { - get { - return ResourceManager.GetString("SettingsNotParsable", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Settings not provided. Will look for settings file in the given path {0}.. - /// - internal static string SettingsNotProvided { - get { - return ResourceManager.GetString("SettingsNotProvided", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Settings object could not be resolved.. - /// - internal static string SettingsObjectCouldNotBResolved { - get { - return ResourceManager.GetString("SettingsObjectCouldNotBResolved", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Using settings file at {0}.. - /// - internal static string SettingsUsingFile { - get { - return ResourceManager.GetString("SettingsUsingFile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Using settings hashtable.. - /// - internal static string SettingsUsingHashtable { - get { - return ResourceManager.GetString("SettingsUsingHashtable", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} property must be of type bool.. - /// - internal static string SettingsValueTypeMustBeBool { - get { - return ResourceManager.GetString("SettingsValueTypeMustBeBool", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to All the arguments of the Suppress Message Attribute should be string constants.. - /// - internal static string StringConstantArgumentsSuppressionAttributeError { - get { - return ResourceManager.GetString("StringConstantArgumentsSuppressionAttributeError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot find any Targets {0} that match the Scope {1} to apply the SuppressMessageAttribute.. - /// - internal static string TargetCannotBeFoundError { - get { - return ResourceManager.GetString("TargetCannotBeFoundError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to If Target is specified, Scope must be specified.. - /// - internal static string TargetWithoutScopeSuppressionAttributeError { - get { - return ResourceManager.GetString("TargetWithoutScopeSuppressionAttributeError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Line element cannot be null.. - /// - internal static string TextEditNoNullItem { - get { - return ResourceManager.GetString("TextEditNoNullItem", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Line element cannot be null.. - /// - internal static string TextLinesNoNullItem { - get { - return ResourceManager.GetString("TextLinesNoNullItem", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Ignoring 'TypeNotFound' parse error on type '{0}'. Check if the specified type is correct. This can also be due the type not being known at parse time due to types imported by 'using' statements.. - /// - internal static string TypeNotFoundParseErrorFound { - get { - return ResourceManager.GetString("TypeNotFoundParseErrorFound", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Analyzing file: {0}. - /// - internal static string VerboseFileMessage { - get { - return ResourceManager.GetString("VerboseFileMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Running {0} rule.. - /// - internal static string VerboseRunningMessage { - get { - return ResourceManager.GetString("VerboseRunningMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Analyzing Script Definition.. - /// - internal static string VerboseScriptDefinitionMessage { - get { - return ResourceManager.GetString("VerboseScriptDefinitionMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to WrongSettingsKey. - /// - internal static string WrongConfigurationKey { - get { - return ResourceManager.GetString("WrongConfigurationKey", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} is not a valid key in the settings hashtable: file {3}. Valid keys are ExcludeRules, IncludeRules and Severity.. - /// - internal static string WrongKey { - get { - return ResourceManager.GetString("WrongKey", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Key in the settings hashtable should be a string: line {0} column {1} in file {2}. - /// - internal static string WrongKeyFormat { - get { - return ResourceManager.GetString("WrongKeyFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to {0} is not a valid key in the settings hashtable. Valid keys are CustomRulePath, ExcludeRules, IncludeRules, IncludeDefaultRules, RecurseCustomRulePath, Rules and Severity.. - /// - internal static string WrongKeyHashTable { - get { - return ResourceManager.GetString("WrongKeyHashTable", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Scope can only be either function or class.. - /// - internal static string WrongScopeArgumentSuppressionAttributeError { - get { - return ResourceManager.GetString("WrongScopeArgumentSuppressionAttributeError", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value in the settings hashtable should be a string or an array of strings: line {0} column {1} in file {2}. - /// - internal static string WrongValueFormat { - get { - return ResourceManager.GetString("WrongValueFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value {0} for key {1} has the wrong data type.. - /// - internal static string WrongValueHashTable { - get { - return ResourceManager.GetString("WrongValueHashTable", resourceCulture); - } - } - } -} diff --git a/Engine/TokenOperations.cs b/Engine/TokenOperations.cs index 0a47ab3cc..fa9a1978a 100644 --- a/Engine/TokenOperations.cs +++ b/Engine/TokenOperations.cs @@ -232,5 +232,18 @@ private bool OnSameLine(Token token1, Token token2) { return token1.Extent.StartLineNumber == token2.Extent.EndLineNumber; } + + /// + /// Finds the position of a given token in the AST. + /// + /// The to search for. + /// The Ast node directly containing the provided . + public Ast GetAstPosition(Token token) + { + FindAstPositionVisitor findAstVisitor = new FindAstPositionVisitor(token.Extent.StartScriptPosition); + ast.Visit(findAstVisitor); + return findAstVisitor.AstPosition; + } + } } diff --git a/Engine/VariableAnalysis.cs b/Engine/VariableAnalysis.cs index fd66ea2c4..2870d442f 100644 --- a/Engine/VariableAnalysis.cs +++ b/Engine/VariableAnalysis.cs @@ -375,7 +375,7 @@ public bool IsUninitialized(VariableExpressionAst varTarget) /// /// /// - public bool IsGlobalOrEnvironment(VariableExpressionAst varTarget) + public static bool IsGlobalOrEnvironment(VariableExpressionAst varTarget) { if (varTarget != null) { diff --git a/Engine/VariableAnalysisBase.cs b/Engine/VariableAnalysisBase.cs index b55119d7a..77c421f3c 100644 --- a/Engine/VariableAnalysisBase.cs +++ b/Engine/VariableAnalysisBase.cs @@ -995,7 +995,7 @@ internal static Tuple, Dictionary String.Equals(item.Name, analysis.Type.FullName, StringComparison.OrdinalIgnoreCase)); + TypeDefinitionAst psClass = Classes.FirstOrDefault(item => String.Equals(item.Name, analysis.Type?.FullName, StringComparison.OrdinalIgnoreCase)); Type possibleType = AssignmentTarget.GetTypeFromMemberExpressionAst(memAst, analysis, psClass); #endif diff --git a/LICENSE b/LICENSE index cec380d8e..48ea6616b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -The MIT License (MIT) +MIT License -Copyright (c) 2015 Microsoft Corporation. +Copyright (c) Microsoft Corporation. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE diff --git a/NuGet.Config b/NuGet.Config index acba4478b..f003b0fbd 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,8 +1,7 @@ - + - - + diff --git a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Collection/PlatformInformationCollector.cs b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Collection/PlatformInformationCollector.cs index dfd62b806..26aa0ee7a 100644 --- a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Collection/PlatformInformationCollector.cs +++ b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Collection/PlatformInformationCollector.cs @@ -330,7 +330,7 @@ private Architecture GetProcessArchitecture() return (Architecture)RuntimeInformation.ProcessArchitecture; #else // We assume .NET Framework must be on an Intel architecture - // net452 does not reliably have the above API + // net462 does not reliably have the above API return Environment.Is64BitProcess ? Architecture.X64 : Architecture.X86; @@ -344,25 +344,13 @@ private Architecture GetOSArchitecture() return (Architecture)RuntimeInformation.OSArchitecture; #else // We assume .NET Framework must be on an Intel architecture - // net452 does not reliably have the above API + // net462 does not reliably have the above API return Environment.Is64BitOperatingSystem ? Architecture.X64 : Architecture.X86; #endif } - private DotnetRuntime GetDotnetRuntime() - { -#if CoreCLR - // Our CoreCLR is actuall .NET Standard, so we could be loaded into net47 - return RuntimeInformation.FrameworkDescription.StartsWith(".NET Core") - ? DotnetRuntime.Core - : DotnetRuntime.Framework; -#else - return DotnetRuntime.Framework; -#endif - } - /// /// Get the Windows SKU ID of the current PowerShell session. /// diff --git a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Collection/TypeDataCollector.cs b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Collection/TypeDataCollector.cs index 8ec0d0f03..19710dde6 100644 --- a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Collection/TypeDataCollector.cs +++ b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Collection/TypeDataCollector.cs @@ -347,7 +347,9 @@ private MemberData AssembleMembers(Type type, BindingFlags memberBinding) switch (member) { case ConstructorInfo constructor: - constructors.Add(AssembleConstructor(constructor)); + if (TryAssembleConstructor(constructor, out string[] cInfo)) { + constructors.Add(cInfo); + } break; case FieldInfo field: @@ -400,7 +402,10 @@ private MemberData AssembleMembers(Type type, BindingFlags memberBinding) var methodDatas = new JsonDictionary(); foreach (KeyValuePair> method in methods) { - methodDatas[method.Key] = AssembleMethod(method.Value); + if (TryAssembleMethod(method.Value, out MethodData md)) + { + methodDatas[method.Key] = md; + } } return new MemberData() @@ -457,35 +462,61 @@ private EventData AssembleEvent(EventInfo e) }; } - private string[] AssembleConstructor(ConstructorInfo ctor) + // private string[] AssembleConstructor(ConstructorInfo ctor) + private bool TryAssembleConstructor(ConstructorInfo ctor, out string[] result) { + bool success = false; var parameters = new List(); - foreach (ParameterInfo param in ctor.GetParameters()) + try + { + foreach (ParameterInfo param in ctor.GetParameters()) + { + parameters.Add(TypeNaming.GetFullTypeName(param.ParameterType)); + } + + result = parameters.ToArray(); + success = true; + } + catch { - parameters.Add(TypeNaming.GetFullTypeName(param.ParameterType)); + result = null; } - return parameters.ToArray(); + return success; } - private MethodData AssembleMethod(List methodOverloads) + // private MethodData AssembleMethod(List methodOverloads) + private bool TryAssembleMethod(List methodOverloads, out MethodData result) { var overloads = new List(); foreach (MethodInfo overload in methodOverloads) { var parameters = new List(); - foreach (ParameterInfo param in overload.GetParameters()) + try { - parameters.Add(TypeNaming.GetFullTypeName(param.ParameterType)); + foreach (ParameterInfo param in overload.GetParameters()) + { + parameters.Add(TypeNaming.GetFullTypeName(param.ParameterType)); + } + overloads.Add(parameters.ToArray()); } - overloads.Add(parameters.ToArray()); + catch + { + } + } + + if (overloads.Count == 0) + { + result = null; + return false; } - return new MethodData() + result = new MethodData() { ReturnType = TypeNaming.GetFullTypeName(methodOverloads[0].ReturnType), OverloadParameters = overloads.ToArray() }; + return true; } private bool IsAssemblyPathExcluded(string path) diff --git a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Commands/CommandUtilities.cs b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Commands/CommandUtilities.cs index 3e14494f1..6a957f7cc 100644 --- a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Commands/CommandUtilities.cs +++ b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Commands/CommandUtilities.cs @@ -13,8 +13,6 @@ namespace Microsoft.PowerShell.CrossCompatibility.Commands /// internal static class CommandUtilities { - private const string COMPATIBILITY_ERROR_ID = "CompatibilityAnalysisError"; - public const string MODULE_PREFIX = "PSCompatibility"; /// diff --git a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Data/Platform/OperatingSystemData.cs b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Data/Platform/OperatingSystemData.cs index 89fe9445a..cb0455a78 100644 --- a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Data/Platform/OperatingSystemData.cs +++ b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Data/Platform/OperatingSystemData.cs @@ -69,7 +69,7 @@ public class OperatingSystemData : ICloneable /// /// The Windows SKU identifier, corresponding to /// the GetProductInfo() sysinfo API: - /// https://docs.microsoft.com/en-us/windows/desktop/api/sysinfoapi/nf-sysinfoapi-getproductinfo + /// https://learn.microsoft.com/windows/desktop/api/sysinfoapi/nf-sysinfoapi-getproductinfo /// [DataMember(EmitDefaultValue = false)] public uint? SkuId { get; set; } diff --git a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj index 641cb159e..c4667a950 100644 --- a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj +++ b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Microsoft.PowerShell.CrossCompatibility.csproj @@ -1,7 +1,9 @@  - netstandard2.0;net452 + $(ModuleVersion) + netstandard2.0;net462 + $(ModuleVersion) @@ -9,18 +11,20 @@ - - + + + + - - + + + + - - - + diff --git a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Retrieval/JsonProfileSerializer.cs b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Retrieval/JsonProfileSerializer.cs index 85ed9b82d..27d2e298b 100644 --- a/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Retrieval/JsonProfileSerializer.cs +++ b/PSCompatibilityCollector/Microsoft.PowerShell.CrossCompatibility/Retrieval/JsonProfileSerializer.cs @@ -84,6 +84,7 @@ public static JsonProfileSerializer Create(Formatting formatting) Converters = GetFormatConverters(), MissingMemberHandling = MissingMemberHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, + MaxDepth = 128, }; var serializer = JsonSerializer.Create(settings); diff --git a/PSCompatibilityCollector/PSCompatibilityCollector.psm1 b/PSCompatibilityCollector/PSCompatibilityCollector.psm1 index aea680da4..0e3f6a8e4 100644 --- a/PSCompatibilityCollector/PSCompatibilityCollector.psm1 +++ b/PSCompatibilityCollector/PSCompatibilityCollector.psm1 @@ -8,5 +8,5 @@ if ($PSVersionTable.PSVersion.Major -ge 6) } else { - Import-Module ([System.IO.Path]::Combine($PSScriptRoot, 'net452', 'Microsoft.PowerShell.CrossCompatibility.dll')) -Force + Import-Module ([System.IO.Path]::Combine($PSScriptRoot, 'net462', 'Microsoft.PowerShell.CrossCompatibility.dll')) -Force } diff --git a/PSCompatibilityCollector/Tests/UtilityApi.Tests.ps1 b/PSCompatibilityCollector/Tests/UtilityApi.Tests.ps1 index 3d0108b50..0ebcec746 100644 --- a/PSCompatibilityCollector/Tests/UtilityApi.Tests.ps1 +++ b/PSCompatibilityCollector/Tests/UtilityApi.Tests.ps1 @@ -1,52 +1,44 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -function Get-TypeNameAstFromScript -{ - param([string]$Script) +BeforeAll { + function Get-TypeNameAstFromScript + { + param([string]$Script) - $ast = [System.Management.Automation.Language.Parser]::ParseInput($Script, [ref]$null, [ref]$null) - $typeExpAst = $ast.Find({ - $args[0] -is [System.Management.Automation.Language.TypeExpressionAst] - }, $true) + $ast = [System.Management.Automation.Language.Parser]::ParseInput($Script, [ref]$null, [ref]$null) + $typeExpAst = $ast.Find({ + $args[0] -is [System.Management.Automation.Language.TypeExpressionAst] + }, $true) - return $typeExpAst.TypeName -} + return $typeExpAst.TypeName + } -function Get-TypeAccelerators -{ - [psobject].Assembly.GetType('System.Management.Automation.TypeAccelerators', 'nonpublic')::Get.GetEnumerator() + function Get-TypeAccelerators + { + [psobject].Assembly.GetType('System.Management.Automation.TypeAccelerators', 'nonpublic')::Get.GetEnumerator() + } } -Describe "Type name serialization" { - BeforeAll { - $typeNameTestCases = @( - @{ InputType = [System.Reflection.Assembly]; ExpectedName = "System.Reflection.Assembly" } - @{ InputType = [string]; ExpectedName = "System.String" } - @{ InputType = [datetime]; ExpectedName = "System.DateTime" } - @{ InputType = [string[]]; ExpectedName = "System.String[]" } - @{ InputType = [System.TimeZoneInfo+AdjustmentRule]; ExpectedName = "System.TimeZoneInfo+AdjustmentRule" } - @{ InputType = [System.Func`1]; ExpectedName = "System.Func``1" } - @{ InputType = [System.Collections.Generic.Dictionary`2]; ExpectedName = "System.Collections.Generic.Dictionary``2" } - @{ InputType = [System.Collections.Generic.Dictionary`2+Enumerator]; ExpectedName = "System.Collections.Generic.Dictionary``2+Enumerator" } - @{ InputType = [System.Collections.Generic.Dictionary[string,object]].GetNestedType('Enumerator'); ExpectedName = "System.Collections.Generic.Dictionary``2+Enumerator" } - @{ InputType = [System.Collections.Generic.List[object]]; ExpectedName = "System.Collections.Generic.List``1[System.Object]" } - @{ InputType = [System.Collections.Generic.Dictionary[string, object]]; ExpectedName = "System.Collections.Generic.Dictionary``2[System.String,System.Object]" } - @{ InputType = [System.Collections.Generic.Dictionary`2+Enumerator[string,object]]; ExpectedName = "System.Collections.Generic.Dictionary``2+Enumerator[System.String,System.Object]" } - @{ InputType = [System.Collections.Concurrent.ConcurrentDictionary`2].GetMethod('ToArray').ReturnType; ExpectedName = "System.Collections.Generic.KeyValuePair``2[]"} - ) - - $genericStrippingTests = @( - @{ RawTypeName = "String"; StrippedTypeName = "String" } - @{ RawTypeName = "Dictionary``2"; StrippedTypeName = "Dictionary" } - @{ RawTypeName = "Dictionary``2"; StrippedTypeName = "Dictionary" } - @{ RawTypeName = "Dictionary``2+Enumerator"; StrippedTypeName = "Dictionary+Enumerator" } - ) - } - It "Serializes the name of type to " -TestCases $typeNameTestCases { - param([type]$InputType, [string]$ExpectedName) +Describe "Type name serialization" { + It "Serializes the name of type to " -TestCases @( + @{ InputType = [System.Reflection.Assembly]; ExpectedName = "System.Reflection.Assembly" } + @{ InputType = [string]; ExpectedName = "System.String" } + @{ InputType = [datetime]; ExpectedName = "System.DateTime" } + @{ InputType = [string[]]; ExpectedName = "System.String[]" } + @{ InputType = [System.TimeZoneInfo+AdjustmentRule]; ExpectedName = "System.TimeZoneInfo+AdjustmentRule" } + @{ InputType = [System.Func`1]; ExpectedName = "System.Func``1" } + @{ InputType = [System.Collections.Generic.Dictionary`2]; ExpectedName = "System.Collections.Generic.Dictionary``2" } + @{ InputType = [System.Collections.Generic.Dictionary`2+Enumerator]; ExpectedName = "System.Collections.Generic.Dictionary``2+Enumerator" } + @{ InputType = [System.Collections.Generic.Dictionary[string, object]].GetNestedType('Enumerator'); ExpectedName = "System.Collections.Generic.Dictionary``2+Enumerator" } + @{ InputType = [System.Collections.Generic.List[object]]; ExpectedName = "System.Collections.Generic.List``1[System.Object]" } + @{ InputType = [System.Collections.Generic.Dictionary[string, object]]; ExpectedName = "System.Collections.Generic.Dictionary``2[System.String,System.Object]" } + @{ InputType = [System.Collections.Generic.Dictionary`2+Enumerator[string, object]]; ExpectedName = "System.Collections.Generic.Dictionary``2+Enumerator[System.String,System.Object]" } + @{ InputType = [System.Collections.Concurrent.ConcurrentDictionary`2].GetMethod('ToArray').ReturnType; ExpectedName = "System.Collections.Generic.KeyValuePair``2[]" } + ) { + param ( $InputType, $ExpectedName ) $name = [Microsoft.PowerShell.CrossCompatibility.TypeNaming]::GetFullTypeName($InputType) $name | Should -BeExactly $ExpectedName } @@ -57,86 +49,80 @@ Describe "Type name serialization" { } | Should -Throw -ErrorId "ArgumentNullException" } - It "Strips generic quantifiers from '' to return ''" -TestCases $genericStrippingTests { + It "Strips generic quantifiers from '' to return ''" { param([string]$RawTypeName, [string]$StrippedTypeName) $stripped = [Microsoft.PowerShell.CrossCompatibility.TypeNaming]::StripGenericQuantifiers($RawTypeName) $stripped | Should -BeExactly $StrippedTypeName - } + } -TestCases @( + @{ RawTypeName = "String"; StrippedTypeName = "String" } + @{ RawTypeName = "Dictionary``2"; StrippedTypeName = "Dictionary" } + @{ RawTypeName = "Dictionary``2"; StrippedTypeName = "Dictionary" } + @{ RawTypeName = "Dictionary``2+Enumerator"; StrippedTypeName = "Dictionary+Enumerator" } + ) } Describe "Type accelerator expansion" { BeforeAll { $typeAccelerators = Get-TypeAccelerators | ForEach-Object { $d = New-Object 'System.Collections.Generic.Dictionary[string,string]' } { $d.Add($_.Key, $_.Value.FullName) } { $d } - - $typeAcceleratorTestCases = @( - @{ Raw = "[System.Exception]"; Expanded = "System.Exception" } - @{ Raw = "[string]"; Expanded = "System.String" } - @{ Raw = "[psmoduleinfo]"; Expanded = "System.Management.Automation.PSModuleInfo" } - @{ Raw = "[System.Collections.Generic.List[int]]"; Expanded = "System.Collections.Generic.List``1[System.Int32]" } - @{ Raw = "[System.Collections.Generic.Dictionary[string,psmoduleinfo]]"; Expanded = "System.Collections.Generic.Dictionary``2[System.String,System.Management.Automation.PSModuleInfo]" } - @{ Raw = "[System.Collections.Generic.Dictionary[string, psmoduleinfo]]"; Expanded = "System.Collections.Generic.Dictionary``2[System.String,System.Management.Automation.PSModuleInfo]" } - @{ Raw = "[System.Collections.Generic.Dictionary [string, psmoduleinfo]]"; Expanded = "System.Collections.Generic.Dictionary``2[System.String,System.Management.Automation.PSModuleInfo]" } - @{ Raw = "[System.Collections.Generic.List``1[uri]]"; Expanded = "System.Collections.Generic.List``1[System.Uri]" } - @{ Raw = "[System.Collections.Generic.Dictionary``2[string,psmoduleinfo]]"; Expanded = "System.Collections.Generic.Dictionary``2[System.String,System.Management.Automation.PSModuleInfo]" } - @{ Raw = "[System.Collections.Generic.Dictionary``2 [string, psmoduleinfo]]"; Expanded = "System.Collections.Generic.Dictionary``2[System.String,System.Management.Automation.PSModuleInfo]" } - @{ Raw = "[object]"; Expanded = "System.Object" } - ) } - It "Expands the typename in to " -TestCases $typeAcceleratorTestCases { - param([string]$Raw, [string]$Expanded) - + It "Expands the typename in to " -TestCases @( + @{ Raw = "[System.Exception]"; Expanded = "System.Exception" } + @{ Raw = "[string]"; Expanded = "System.String" } + @{ Raw = "[psmoduleinfo]"; Expanded = "System.Management.Automation.PSModuleInfo" } + @{ Raw = "[System.Collections.Generic.List[int]]"; Expanded = "System.Collections.Generic.List``1[System.Int32]" } + @{ Raw = "[System.Collections.Generic.Dictionary[string,psmoduleinfo]]"; Expanded = "System.Collections.Generic.Dictionary``2[System.String,System.Management.Automation.PSModuleInfo]" } + @{ Raw = "[System.Collections.Generic.Dictionary[string, psmoduleinfo]]"; Expanded = "System.Collections.Generic.Dictionary``2[System.String,System.Management.Automation.PSModuleInfo]" } + @{ Raw = "[System.Collections.Generic.Dictionary [string, psmoduleinfo]]"; Expanded = "System.Collections.Generic.Dictionary``2[System.String,System.Management.Automation.PSModuleInfo]" } + @{ Raw = "[System.Collections.Generic.List``1[uri]]"; Expanded = "System.Collections.Generic.List``1[System.Uri]" } + @{ Raw = "[System.Collections.Generic.Dictionary``2[string,psmoduleinfo]]"; Expanded = "System.Collections.Generic.Dictionary``2[System.String,System.Management.Automation.PSModuleInfo]" } + @{ Raw = "[System.Collections.Generic.Dictionary``2 [string, psmoduleinfo]]"; Expanded = "System.Collections.Generic.Dictionary``2[System.String,System.Management.Automation.PSModuleInfo]" } + @{ Raw = "[object]"; Expanded = "System.Object" } + ) -Test { + + param ( $Raw, $Expanded ) $typeName = Get-TypeNameAstFromScript -Script $Raw $canonicalName = [Microsoft.PowerShell.CrossCompatibility.TypeNaming]::GetCanonicalTypeName($typeAccelerators, $typeName) $canonicalName | Should -BeExactly $Expanded - } } +$versionCreationTests = @( + @{ Version = '6.1'; Major = 6; Minor = 1; Patch = -1 } + @{ Version = '6.1.4'; Major = 6; Minor = 1; Patch = 4; } + @{ Version = '5.1.8-preview.2'; Major = 5; Minor = 1; Patch = 8; Label = 'preview.2' } + @{ Version = [version]'4.2'; Major = 4; Minor = 2; Patch = -1; Revision = -1 } + @{ Version = [version]'4.2.1'; Major = 4; Minor = 2; Patch = 1; Revision = -1 } + @{ Version = [version]'4.2.1.7'; Major = 4; Minor = 2; Patch = 1; Revision = 7 } +) + +if ($PSVersionTable.PSVersion.Major -ge 6) +{ + $versionCreationTests += @( + @{ Version = [semver]'6.1.2'; Major = 6; Minor = 1; Patch = 2; Label = $null } + @{ Version = [semver]'6.1.2-rc.1'; Major = 6; Minor = 1; Patch = 2; Label = 'rc.1' } + @{ Version = [semver]'6.1-rc.1'; Major = 6; Minor = 1; Patch = 0; Label = 'rc.1' } + @{ Version = [semver]'6-rc.1'; Major = 6; Minor = 0; Patch = 0; Label = 'rc.1' } + @{ Version = [semver]'6.1.2-rc.1+duck'; Major = 6; Minor = 1; Patch = 2; Label = 'rc.1'; BuildLabel = 'duck' } + @{ Version = [semver]'6.1-rc.1+duck'; Major = 6; Minor = 1; Patch = 0; Label = 'rc.1'; BuildLabel = 'duck' } + @{ Version = [semver]'6-rc.1+duck'; Major = 6; Minor = 0; Patch = 0; Label = 'rc.1'; BuildLabel = 'duck' } + ) +} + Describe "PowerShell version object" { Context "Version parsing" { - BeforeAll { - $genericVerCases = @( - @{ VerStr = '42'; Major = 42; Minor = -1; Patch = -1 } - @{ VerStr = '7'; Major = 7; Minor = -1; Patch = -1 } - @{ VerStr = '6.1'; Major = 6; Minor = 1; Patch = -1 } - @{ VerStr = '5.2.7'; Major = 5; Minor = 2; Patch = 7 } - @{ VerStr = '512.2124.71'; Major = 512; Minor = 2124; Patch = 71 } - ) - - $semVerCases = @( - @{ VerStr = '6.1.0-rc.1'; Major = 6; Minor = 1; Patch = 0; Label = 'rc.1' } - @{ VerStr = '6.2-preview.2'; Major = 6; Minor = 2; Patch = -1; Label = 'preview.2' } - @{ VerStr = '6-preview.2'; Major = 6; Minor = -1; Patch = -1; Label = 'preview.2' } - @{ VerStr = '6.1.0-rc.1+moo'; Major = 6; Minor = 1; Patch = 0; Label = 'rc.1'; BuildLabel = 'moo' } - @{ VerStr = '6.2-preview.2+horse'; Major = 6; Minor = 2; Patch = -1; Label = 'preview.2'; BuildLabel = 'horse' } - @{ VerStr = '6-preview.2+veryimportant'; Major = 6; Minor = -1; Patch = -1; Label = 'preview.2'; BuildLabel = 'veryimportant' } - ) - - $systemVerCases = @( - @{ VerStr = '5.2.1.12312'; Major = 5; Minor = 2; Patch = 1; Revision = 12312 } - ) - - $versionFailCases = @( - @{ VerStr = 'banana' } - @{ VerStr = '' } - @{ VerStr = '1.' } - @{ VerStr = '.6' } - @{ VerStr = '5.1.' } - @{ VerStr = '5.1.2.' } - @{ VerStr = '4.1.5.7.' } - @{ VerStr = '4.1.5.7.4' } - @{ VerStr = '4.1.5.7-rc.2' } - @{ VerStr = '4.1.5.-rc.2' } - ) - } - It "Parses version string '' as .." -TestCases $genericVerCases { - param([string]$VerStr, [int]$Major, [int]$Minor, [int]$Patch) + It "Parses version string '' as .." -TestCases @( + @{ VerStr = '42'; Major = 42; Minor = -1; Patch = -1 } + @{ VerStr = '7'; Major = 7; Minor = -1; Patch = -1 } + @{ VerStr = '6.1'; Major = 6; Minor = 1; Patch = -1 } + @{ VerStr = '5.2.7'; Major = 5; Minor = 2; Patch = 7 } + @{ VerStr = '512.2124.71'; Major = 512; Minor = 2124; Patch = 71 } + ) -Test { $v = [Microsoft.PowerShell.CrossCompatibility.PowerShellVersion]$VerStr @@ -145,7 +131,14 @@ Describe "PowerShell version object" { $v.Patch | Should -Be $Patch } - It "Parses version string '' as ..-