diff --git a/.vsts-ci.yml b/.vsts-ci.yml index 23eac359e..5f66064b0 100644 --- a/.vsts-ci.yml +++ b/.vsts-ci.yml @@ -12,12 +12,18 @@ variables: # Branches that trigger a build on commit trigger: - main +- dev17.0 stages: - stage: build displayName: Build and Test jobs: + - template: /eng/common/templates/job/onelocbuild.yml + parameters: + LclSource: lclFilesfromPackage + LclPackageId: 'LCL-JUNO-PROD-ROSLYNSDK' + - job: OfficialBuild displayName: Official Build pool: @@ -72,7 +78,7 @@ stages: PathtoPublish: '$(Build.SourcesDirectory)\artifacts\log\$(BuildConfiguration)' ArtifactName: 'Logs' continueOnError: true - condition: not(succeeded()) + condition: always() # Publish an artifact that the RoslynInsertionTool is able to find by its name. - task: PublishBuildArtifacts@1 @@ -115,6 +121,7 @@ stages: - ${{ if and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - template: eng\common\templates\post-build\post-build.yml parameters: + publishingInfraVersion: 3 # Symbol validation is not entirely reliable as of yet, so should be turned off until # https://github.com/dotnet/arcade/issues/2871 is resolved. enableSymbolValidation: false diff --git a/.vsts-pr.yaml b/.vsts-pr.yaml index ab86e2cdc..8f1ccdb33 100644 --- a/.vsts-pr.yaml +++ b/.vsts-pr.yaml @@ -1,3 +1,13 @@ +# Branches that trigger a build on commit +trigger: +- main +- dev17.0 + +# Branches that trigger builds on PR +pr: +- main +- dev17.0 + variables: - name: DOTNET_ROOT value: $(Build.SourcesDirectory)\.dotnet diff --git a/NuGet.config b/NuGet.config index 712e69220..9680657aa 100644 --- a/NuGet.config +++ b/NuGet.config @@ -8,6 +8,7 @@ + diff --git a/Roslyn-SDK.sln b/Roslyn-SDK.sln index ef1484fb0..ae7e94b1b 100644 --- a/Roslyn-SDK.sln +++ b/Roslyn-SDK.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29814.53 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31706.66 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{924F7971-780C-4E70-A306-86482469502E}" EndProject @@ -191,6 +191,10 @@ Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "Microsoft.CodeAnalysis.Visu EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Roslyn.ComponentDebugger", "src\VisualStudio.Roslyn.SDK\ComponentDebugger\Roslyn.ComponentDebugger.csproj", "{421DE59C-8246-4679-9D69-79F16A7187BE}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "util", "util", "{7A94E723-ADD6-48C4-BBE7-1D5B311187A8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AssemblyVersionGenerator", "src\VisualStudio.Roslyn.SDK\AssemblyVersionGenerator\AssemblyVersionGenerator.csproj", "{AB6B3C69-9F6F-461C-BFD8-D3F25B9F44AD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -545,6 +549,10 @@ Global {421DE59C-8246-4679-9D69-79F16A7187BE}.Debug|Any CPU.Build.0 = Debug|Any CPU {421DE59C-8246-4679-9D69-79F16A7187BE}.Release|Any CPU.ActiveCfg = Release|Any CPU {421DE59C-8246-4679-9D69-79F16A7187BE}.Release|Any CPU.Build.0 = Release|Any CPU + {AB6B3C69-9F6F-461C-BFD8-D3F25B9F44AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB6B3C69-9F6F-461C-BFD8-D3F25B9F44AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB6B3C69-9F6F-461C-BFD8-D3F25B9F44AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB6B3C69-9F6F-461C-BFD8-D3F25B9F44AD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -642,6 +650,7 @@ Global {7C3FE60E-055B-4E0C-BB85-C7E94A640074} = {9905147E-CC1F-42A0-BD27-05586C583DF7} {92BD1781-5DB4-4F72-BCCB-0D64C0790A2B} = {9905147E-CC1F-42A0-BD27-05586C583DF7} {421DE59C-8246-4679-9D69-79F16A7187BE} = {F9B73995-76C6-4056-ADA9-18342F951361} + {AB6B3C69-9F6F-461C-BFD8-D3F25B9F44AD} = {7A94E723-ADD6-48C4-BBE7-1D5B311187A8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {56695AA9-EA80-47A7-8562-E51285906C54} diff --git a/Samples.sln b/Samples.sln index 349b0c8cd..71d51bee6 100644 --- a/Samples.sln +++ b/Samples.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.28606.18 @@ -113,9 +113,15 @@ Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "VisualBasicToCSharpConverte EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SourceGenerators", "SourceGenerators", "{14D18F51-6B59-49D5-9AB7-08B38417A459}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceGeneratorSamples", "samples\CSharp\SourceGenerators\SourceGeneratorSamples\SourceGeneratorSamples.csproj", "{2ADE5CFA-5DF4-44A9-BD67-E884BCFBA045}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSharpSourceGeneratorSamples", "samples\CSharp\SourceGenerators\SourceGeneratorSamples\CSharpSourceGeneratorSamples.csproj", "{2ADE5CFA-5DF4-44A9-BD67-E884BCFBA045}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeneratedDemo", "samples\CSharp\SourceGenerators\GeneratedDemo\GeneratedDemo.csproj", "{EC4DB63B-C2B4-4D06-AF98-15253035C6D5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSharpGeneratedDemo", "samples\CSharp\SourceGenerators\GeneratedDemo\CSharpGeneratedDemo.csproj", "{EC4DB63B-C2B4-4D06-AF98-15253035C6D5}" +EndProject +Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "VisualBasicGeneratedDemo", "samples\VisualBasic\SourceGenerators\GeneratedDemo\VisualBasicGeneratedDemo.vbproj", "{DA924876-9CF5-47E0-AA01-ADAF47653D39}" +EndProject +Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "VisualBasicSourceGeneratorSamples", "samples\VisualBasic\SourceGenerators\SourceGeneratorSamples\VisualBasicSourceGeneratorSamples.vbproj", "{8322B6E4-0CB1-4EC1-A2CC-2E4DB02C834A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SourceGenerators", "SourceGenerators", "{E79B07C8-0859-4B5C-9650-68D855833C6E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -283,6 +289,14 @@ Global {EC4DB63B-C2B4-4D06-AF98-15253035C6D5}.Debug|Any CPU.Build.0 = Debug|Any CPU {EC4DB63B-C2B4-4D06-AF98-15253035C6D5}.Release|Any CPU.ActiveCfg = Release|Any CPU {EC4DB63B-C2B4-4D06-AF98-15253035C6D5}.Release|Any CPU.Build.0 = Release|Any CPU + {DA924876-9CF5-47E0-AA01-ADAF47653D39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA924876-9CF5-47E0-AA01-ADAF47653D39}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA924876-9CF5-47E0-AA01-ADAF47653D39}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA924876-9CF5-47E0-AA01-ADAF47653D39}.Release|Any CPU.Build.0 = Release|Any CPU + {8322B6E4-0CB1-4EC1-A2CC-2E4DB02C834A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8322B6E4-0CB1-4EC1-A2CC-2E4DB02C834A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8322B6E4-0CB1-4EC1-A2CC-2E4DB02C834A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8322B6E4-0CB1-4EC1-A2CC-2E4DB02C834A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -342,6 +356,9 @@ Global {14D18F51-6B59-49D5-9AB7-08B38417A459} = {C3FB27E9-C8EE-4F76-B0AA-7CD67A7E652B} {2ADE5CFA-5DF4-44A9-BD67-E884BCFBA045} = {14D18F51-6B59-49D5-9AB7-08B38417A459} {EC4DB63B-C2B4-4D06-AF98-15253035C6D5} = {14D18F51-6B59-49D5-9AB7-08B38417A459} + {DA924876-9CF5-47E0-AA01-ADAF47653D39} = {E79B07C8-0859-4B5C-9650-68D855833C6E} + {8322B6E4-0CB1-4EC1-A2CC-2E4DB02C834A} = {E79B07C8-0859-4B5C-9650-68D855833C6E} + {E79B07C8-0859-4B5C-9650-68D855833C6E} = {CDA94F62-E35A-4913-8045-D9D42416513C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B849838B-3D7A-4B6B-BE07-285DCB1588F4} diff --git a/eng/Publishing.props b/eng/Publishing.props new file mode 100644 index 000000000..797de4ea1 --- /dev/null +++ b/eng/Publishing.props @@ -0,0 +1,6 @@ + + + + 3 + + \ No newline at end of file diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 52d5f57f6..90c1df549 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -3,9 +3,9 @@ - + https://github.com/dotnet/arcade - db49d790a4bfa977a9ed7436bf2aa242cefae45e + 85a65ea1fca1d0867f699fed44d191358270bf6a diff --git a/eng/Versions.props b/eng/Versions.props index 9358a5cd8..5e8ce06d3 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -3,13 +3,14 @@ 4.0.0 - 1.0.1 + 1.1.1 beta1 true true true true + true 3.9.0 16.1.1 @@ -61,12 +62,14 @@ 17.0.0-beta1-10413-02 17.0.0-beta1-10413-02 + 17.0.667-pre - 1.4.4 + 1.5.0 2.6.1 3.9.0 1.0.1-beta1.20374.2 + $(xunitVersion) 1.2.7 0.1.49-beta diff --git a/eng/common/build.ps1 b/eng/common/build.ps1 index 94a91c081..8943da242 100644 --- a/eng/common/build.ps1 +++ b/eng/common/build.ps1 @@ -25,6 +25,7 @@ Param( [switch] $prepareMachine, [string] $runtimeSourceFeed = '', [string] $runtimeSourceFeedKey = '', + [switch] $excludePrereleaseVS, [switch] $help, [Parameter(ValueFromRemainingArguments=$true)][String[]]$properties ) @@ -65,6 +66,7 @@ function Print-Usage() { Write-Host " -prepareMachine Prepare machine for CI run, clean up processes after build" Write-Host " -warnAsError Sets warnaserror msbuild parameter ('true' or 'false')" Write-Host " -msbuildEngine Msbuild engine to use to run build ('dotnet', 'vs', or unspecified)." + Write-Host " -excludePrereleaseVS Set to exclude build engines in prerelease versions of Visual Studio" Write-Host "" Write-Host "Command line arguments not listed above are passed thru to msbuild." diff --git a/eng/common/cross/build-android-rootfs.sh b/eng/common/cross/build-android-rootfs.sh index c29c8267e..42516bbee 100755 --- a/eng/common/cross/build-android-rootfs.sh +++ b/eng/common/cross/build-android-rootfs.sh @@ -106,7 +106,6 @@ __AndroidPackages+=" libandroid-glob" __AndroidPackages+=" liblzma" __AndroidPackages+=" krb5" __AndroidPackages+=" openssl" -__AndroidPackages+=" openldap" for path in $(wget -qO- http://termux.net/dists/stable/main/binary-$__AndroidArch/Packages |\ grep -A15 "Package: \(${__AndroidPackages// /\\|}\)" | grep -v "static\|tool" | grep Filename); do diff --git a/eng/common/cross/build-rootfs.sh b/eng/common/cross/build-rootfs.sh index 81e641a57..591d8666a 100755 --- a/eng/common/cross/build-rootfs.sh +++ b/eng/common/cross/build-rootfs.sh @@ -55,13 +55,11 @@ __UbuntuPackages+=" libcurl4-openssl-dev" __UbuntuPackages+=" libkrb5-dev" __UbuntuPackages+=" libssl-dev" __UbuntuPackages+=" zlib1g-dev" -__UbuntuPackages+=" libldap2-dev" __AlpinePackages+=" curl-dev" __AlpinePackages+=" krb5-dev" __AlpinePackages+=" openssl-dev" __AlpinePackages+=" zlib-dev" -__AlpinePackages+=" openldap-dev" __FreeBSDBase="12.1-RELEASE" __FreeBSDPkg="1.12.0" @@ -70,13 +68,15 @@ __FreeBSDPackages+=" icu" __FreeBSDPackages+=" libinotify" __FreeBSDPackages+=" lttng-ust" __FreeBSDPackages+=" krb5" -__FreeBSDPackages+=" libslapi-2.4" __IllumosPackages="icu-64.2nb2" __IllumosPackages+=" mit-krb5-1.16.2nb4" __IllumosPackages+=" openssl-1.1.1e" __IllumosPackages+=" zlib-1.2.11" -__IllumosPackages+=" openldap-client-2.4.49" + +# ML.NET dependencies +__UbuntuPackages+=" libomp5" +__UbuntuPackages+=" libomp-dev" __UseMirror=0 diff --git a/eng/common/generate-locproject.ps1 b/eng/common/generate-locproject.ps1 index 7225ddc66..de348a2e2 100644 --- a/eng/common/generate-locproject.ps1 +++ b/eng/common/generate-locproject.ps1 @@ -14,7 +14,7 @@ $ErrorActionPreference = "Stop" Import-Module -Name (Join-Path $PSScriptRoot 'native\CommonLibrary.psm1') -$exclusionsFilePath = "$SourcesDirectory\Localize\LocExclusions.json" +$exclusionsFilePath = "$SourcesDirectory\eng\Localize\LocExclusions.json" $exclusions = @{ Exclusions = @() } if (Test-Path -Path $exclusionsFilePath) { @@ -66,10 +66,19 @@ $locJson = @{ } if ($continue) { - return @{ - SourceFile = $sourceFile - CopyOption = "LangIDOnName" - OutputPath = $outputPath + if ($_.Directory.Name -eq 'en' -and $_.Extension -eq '.json') { + return @{ + SourceFile = $sourceFile + CopyOption = "LangIDOnPath" + OutputPath = "$($_.Directory.Parent.FullName | Resolve-Path -Relative)\" + } + } + else { + return @{ + SourceFile = $sourceFile + CopyOption = "LangIDOnName" + OutputPath = $outputPath + } } } } @@ -83,14 +92,14 @@ Write-Host "LocProject.json generated:`n`n$json`n`n" Pop-Location if (!$UseCheckedInLocProjectJson) { - New-Item "$SourcesDirectory\Localize\LocProject.json" -Force # Need this to make sure the Localize directory is created - Set-Content "$SourcesDirectory\Localize\LocProject.json" $json + New-Item "$SourcesDirectory\eng\Localize\LocProject.json" -Force # Need this to make sure the Localize directory is created + Set-Content "$SourcesDirectory\eng\Localize\LocProject.json" $json } else { - New-Item "$SourcesDirectory\Localize\LocProject-generated.json" -Force # Need this to make sure the Localize directory is created - Set-Content "$SourcesDirectory\Localize\LocProject-generated.json" $json + New-Item "$SourcesDirectory\eng\Localize\LocProject-generated.json" -Force # Need this to make sure the Localize directory is created + Set-Content "$SourcesDirectory\eng\Localize\LocProject-generated.json" $json - if ((Get-FileHash "$SourcesDirectory\Localize\LocProject-generated.json").Hash -ne (Get-FileHash "$SourcesDirectory\Localize\LocProject.json").Hash) { + if ((Get-FileHash "$SourcesDirectory\eng\Localize\LocProject-generated.json").Hash -ne (Get-FileHash "$SourcesDirectory\eng\Localize\LocProject.json").Hash) { Write-PipelineTelemetryError -Category "OneLocBuild" -Message "Existing LocProject.json differs from generated LocProject.json. Download LocProject-generated.json and compare them." exit 1 diff --git a/eng/common/msbuild.ps1 b/eng/common/msbuild.ps1 index c64012300..eea19cd84 100644 --- a/eng/common/msbuild.ps1 +++ b/eng/common/msbuild.ps1 @@ -5,6 +5,7 @@ Param( [bool] $nodeReuse = $true, [switch] $ci, [switch] $prepareMachine, + [switch] $excludePrereleaseVS, [Parameter(ValueFromRemainingArguments=$true)][String[]]$extraArgs ) diff --git a/eng/common/post-build/sourcelink-validation.ps1 b/eng/common/post-build/sourcelink-validation.ps1 index 1c46f7b63..85c898617 100644 --- a/eng/common/post-build/sourcelink-validation.ps1 +++ b/eng/common/post-build/sourcelink-validation.ps1 @@ -14,7 +14,9 @@ param( $global:RepoFiles = @{} # Maximum number of jobs to run in parallel -$MaxParallelJobs = 6 +$MaxParallelJobs = 16 + +$MaxRetries = 5 # Wait time between check for system load $SecondsBetweenLoadChecks = 10 @@ -29,7 +31,10 @@ $ValidatePackage = { # Ensure input file exist if (!(Test-Path $PackagePath)) { Write-Host "Input file does not exist: $PackagePath" - return 1 + return [pscustomobject]@{ + result = 1 + packagePath = $PackagePath + } } # Extensions for which we'll look for SourceLink information @@ -59,7 +64,10 @@ $ValidatePackage = { # We ignore resource DLLs if ($FileName.EndsWith('.resources.dll')) { - return + return [pscustomobject]@{ + result = 0 + packagePath = $PackagePath + } } [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $TargetFile, $true) @@ -91,36 +99,49 @@ $ValidatePackage = { $Status = 200 $Cache = $using:RepoFiles - if ( !($Cache.ContainsKey($FilePath)) ) { - try { - $Uri = $Link -as [System.URI] - - # Only GitHub links are valid - if ($Uri.AbsoluteURI -ne $null -and ($Uri.Host -match 'github' -or $Uri.Host -match 'githubusercontent')) { - $Status = (Invoke-WebRequest -Uri $Link -UseBasicParsing -Method HEAD -TimeoutSec 5).StatusCode + $totalRetries = 0 + + while ($totalRetries -lt $using:MaxRetries) { + if ( !($Cache.ContainsKey($FilePath)) ) { + try { + $Uri = $Link -as [System.URI] + + # Only GitHub links are valid + if ($Uri.AbsoluteURI -ne $null -and ($Uri.Host -match 'github' -or $Uri.Host -match 'githubusercontent')) { + $Status = (Invoke-WebRequest -Uri $Link -UseBasicParsing -Method HEAD -TimeoutSec 5).StatusCode + } + else { + # If it's not a github link, we want to break out of the loop and not retry. + $Status = 0 + $totalRetries = $using:MaxRetries + } } - else { + catch { + Write-Host $_ $Status = 0 } } - catch { - write-host $_ - $Status = 0 - } - } - if ($Status -ne 200) { - if ($NumFailedLinks -eq 0) { - if ($FailedFiles.Value -eq 0) { - Write-Host + if ($Status -ne 200) { + $totalRetries++ + + if ($totalRetries -ge $using:MaxRetries) { + if ($NumFailedLinks -eq 0) { + if ($FailedFiles.Value -eq 0) { + Write-Host + } + + Write-Host "`tFile $RealPath has broken links:" + } + + Write-Host "`t`tFailed to retrieve $Link" + + $NumFailedLinks++ } - - Write-Host "`tFile $RealPath has broken links:" } - - Write-Host "`t`tFailed to retrieve $Link" - - $NumFailedLinks++ + else { + break + } } } } @@ -136,7 +157,7 @@ $ValidatePackage = { } } catch { - + Write-Host $_ } finally { $zip.Dispose() @@ -220,6 +241,7 @@ function ValidateSourceLinkLinks { # Process each NuGet package in parallel Get-ChildItem "$InputPath\*.symbols.nupkg" | ForEach-Object { + Write-Host "Starting $($_.FullName)" Start-Job -ScriptBlock $ValidatePackage -ArgumentList $_.FullName | Out-Null $NumJobs = @(Get-Job -State 'Running').Count @@ -267,6 +289,10 @@ function InstallSourcelinkCli { try { InstallSourcelinkCli + foreach ($Job in @(Get-Job)) { + Remove-Job -Id $Job.Id + } + ValidateSourceLinkLinks } catch { diff --git a/eng/common/post-build/symbols-validation.ps1 b/eng/common/post-build/symbols-validation.ps1 index 99bf28cd5..a5af041ba 100644 --- a/eng/common/post-build/symbols-validation.ps1 +++ b/eng/common/post-build/symbols-validation.ps1 @@ -1,13 +1,14 @@ param( - [Parameter(Mandatory=$true)][string] $InputPath, # Full path to directory where NuGet packages to be checked are stored - [Parameter(Mandatory=$true)][string] $ExtractPath, # Full path to directory where the packages will be extracted during validation - [Parameter(Mandatory=$true)][string] $DotnetSymbolVersion, # Version of dotnet symbol to use - [Parameter(Mandatory=$false)][switch] $ContinueOnError, # If we should keep checking symbols after an error - [Parameter(Mandatory=$false)][switch] $Clean # Clean extracted symbols directory after checking symbols + [Parameter(Mandatory = $true)][string] $InputPath, # Full path to directory where NuGet packages to be checked are stored + [Parameter(Mandatory = $true)][string] $ExtractPath, # Full path to directory where the packages will be extracted during validation + [Parameter(Mandatory = $true)][string] $DotnetSymbolVersion, # Version of dotnet symbol to use + [Parameter(Mandatory = $false)][switch] $CheckForWindowsPdbs, # If we should check for the existence of windows pdbs in addition to portable PDBs + [Parameter(Mandatory = $false)][switch] $ContinueOnError, # If we should keep checking symbols after an error + [Parameter(Mandatory = $false)][switch] $Clean # Clean extracted symbols directory after checking symbols ) # Maximum number of jobs to run in parallel -$MaxParallelJobs = 6 +$MaxParallelJobs = 16 # Max number of retries $MaxRetry = 5 @@ -19,9 +20,15 @@ $SecondsBetweenLoadChecks = 10 Set-Variable -Name "ERROR_BADEXTRACT" -Option Constant -Value -1 Set-Variable -Name "ERROR_FILEDOESNOTEXIST" -Option Constant -Value -2 +$WindowsPdbVerificationParam = "" +if ($CheckForWindowsPdbs) { + $WindowsPdbVerificationParam = "--windows-pdbs" +} + $CountMissingSymbols = { param( - [string] $PackagePath # Path to a NuGet package + [string] $PackagePath, # Path to a NuGet package + [string] $WindowsPdbVerificationParam # If we should check for the existence of windows pdbs in addition to portable PDBs ) . $using:PSScriptRoot\..\tools.ps1 @@ -34,7 +41,7 @@ $CountMissingSymbols = { if (!(Test-Path $PackagePath)) { Write-PipelineTaskError "Input file does not exist: $PackagePath" return [pscustomobject]@{ - result = $using:ERROR_FILEDOESNOTEXIST + result = $using:ERROR_FILEDOESNOTEXIST packagePath = $PackagePath } } @@ -57,24 +64,25 @@ $CountMissingSymbols = { Write-Host "Something went wrong extracting $PackagePath" Write-Host $_ return [pscustomobject]@{ - result = $using:ERROR_BADEXTRACT + result = $using:ERROR_BADEXTRACT packagePath = $PackagePath } } Get-ChildItem -Recurse $ExtractPath | - Where-Object {$RelevantExtensions -contains $_.Extension} | - ForEach-Object { - $FileName = $_.FullName - if ($FileName -Match '\\ref\\') { - Write-Host "`t Ignoring reference assembly file " $FileName - return - } + Where-Object { $RelevantExtensions -contains $_.Extension } | + ForEach-Object { + $FileName = $_.FullName + if ($FileName -Match '\\ref\\') { + Write-Host "`t Ignoring reference assembly file " $FileName + return + } - $FirstMatchingSymbolDescriptionOrDefault = { + $FirstMatchingSymbolDescriptionOrDefault = { param( - [string] $FullPath, # Full path to the module that has to be checked - [string] $TargetServerParam, # Parameter to pass to `Symbol Tool` indicating the server to lookup for symbols + [string] $FullPath, # Full path to the module that has to be checked + [string] $TargetServerParam, # Parameter to pass to `Symbol Tool` indicating the server to lookup for symbols + [string] $WindowsPdbVerificationParam, # Parameter to pass to potential check for windows-pdbs. [string] $SymbolsPath ) @@ -99,15 +107,16 @@ $CountMissingSymbols = { # DWARF file for a .dylib $DylibDwarf = $SymbolPath.Replace($Extension, '.dylib.dwarf') - + $dotnetSymbolExe = "$env:USERPROFILE\.dotnet\tools" $dotnetSymbolExe = Resolve-Path "$dotnetSymbolExe\dotnet-symbol.exe" $totalRetries = 0 while ($totalRetries -lt $using:MaxRetry) { + # Save the output and get diagnostic output - $output = & $dotnetSymbolExe --symbols --modules --windows-pdbs $TargetServerParam $FullPath -o $SymbolsPath --diagnostics | Out-String + $output = & $dotnetSymbolExe --symbols --modules $WindowsPdbVerificationParam $TargetServerParam $FullPath -o $SymbolsPath --diagnostics | Out-String if (Test-Path $PdbPath) { return 'PDB' @@ -124,42 +133,50 @@ $CountMissingSymbols = { elseif (Test-Path $SymbolPath) { return 'Module' } - elseif ($output.Contains("503 Service Unavailable")) { - # If we got a 503 error, we should retry. + else + { $totalRetries++ } - else { - return $null - } } return $null } - $SymbolsOnMSDL = & $FirstMatchingSymbolDescriptionOrDefault $FileName '--microsoft-symbol-server' $SymbolsPath - $SymbolsOnSymWeb = & $FirstMatchingSymbolDescriptionOrDefault $FileName '--internal-server' $SymbolsPath - - Write-Host -NoNewLine "`t Checking file " $FileName "... " + $FileGuid = New-Guid + $ExpandedSymbolsPath = Join-Path -Path $SymbolsPath -ChildPath $FileGuid + + $SymbolsOnMSDL = & $FirstMatchingSymbolDescriptionOrDefault ` + -FullPath $FileName ` + -TargetServerParam '--microsoft-symbol-server' ` + -SymbolsPath "$ExpandedSymbolsPath-msdl" ` + -WindowsPdbVerificationParam $WindowsPdbVerificationParam + $SymbolsOnSymWeb = & $FirstMatchingSymbolDescriptionOrDefault ` + -FullPath $FileName ` + -TargetServerParam '--internal-server' ` + -SymbolsPath "$ExpandedSymbolsPath-symweb" ` + -WindowsPdbVerificationParam $WindowsPdbVerificationParam + + Write-Host -NoNewLine "`t Checking file " $FileName "... " - if ($SymbolsOnMSDL -ne $null -and $SymbolsOnSymWeb -ne $null) { - Write-Host "Symbols found on MSDL ($SymbolsOnMSDL) and SymWeb ($SymbolsOnSymWeb)" + if ($SymbolsOnMSDL -ne $null -and $SymbolsOnSymWeb -ne $null) { + Write-Host "Symbols found on MSDL ($SymbolsOnMSDL) and SymWeb ($SymbolsOnSymWeb)" + } + else { + $MissingSymbols++ + + if ($SymbolsOnMSDL -eq $null -and $SymbolsOnSymWeb -eq $null) { + Write-Host 'No symbols found on MSDL or SymWeb!' } else { - $MissingSymbols++ - - if ($SymbolsOnMSDL -eq $null -and $SymbolsOnSymWeb -eq $null) { - Write-Host 'No symbols found on MSDL or SymWeb!' + if ($SymbolsOnMSDL -eq $null) { + Write-Host 'No symbols found on MSDL!' } else { - if ($SymbolsOnMSDL -eq $null) { - Write-Host 'No symbols found on MSDL!' - } - else { - Write-Host 'No symbols found on SymWeb!' - } + Write-Host 'No symbols found on SymWeb!' } } } + } if ($using:Clean) { Remove-Item $ExtractPath -Recurse -Force @@ -168,16 +185,16 @@ $CountMissingSymbols = { Pop-Location return [pscustomobject]@{ - result = $MissingSymbols - packagePath = $PackagePath - } + result = $MissingSymbols + packagePath = $PackagePath + } } function CheckJobResult( - $result, - $packagePath, - [ref]$DupedSymbols, - [ref]$TotalFailures) { + $result, + $packagePath, + [ref]$DupedSymbols, + [ref]$TotalFailures) { if ($result -eq $ERROR_BADEXTRACT) { Write-PipelineTelemetryError -Category 'CheckSymbols' -Message "$packagePath has duplicated symbol files" $DupedSymbols.Value++ @@ -200,6 +217,7 @@ function CheckSymbolsAvailable { Remove-Item $ExtractPath -Force -Recurse -ErrorAction SilentlyContinue } + $TotalPackages = 0 $TotalFailures = 0 $DupedSymbols = 0 @@ -222,7 +240,9 @@ function CheckSymbolsAvailable { return } - Start-Job -ScriptBlock $CountMissingSymbols -ArgumentList $FullName | Out-Null + $TotalPackages++ + + Start-Job -ScriptBlock $CountMissingSymbols -ArgumentList @($FullName,$WindowsPdbVerificationParam) | Out-Null $NumJobs = @(Get-Job -State 'Running').Count @@ -247,11 +267,11 @@ function CheckSymbolsAvailable { if ($TotalFailures -gt 0 -or $DupedSymbols -gt 0) { if ($TotalFailures -gt 0) { - Write-PipelineTelemetryError -Category 'CheckSymbols' -Message "Symbols missing for $TotalFailures packages" + Write-PipelineTelemetryError -Category 'CheckSymbols' -Message "Symbols missing for $TotalFailures/$TotalPackages packages" } if ($DupedSymbols -gt 0) { - Write-PipelineTelemetryError -Category 'CheckSymbols' -Message "$DupedSymbols packages had duplicated symbol files" + Write-PipelineTelemetryError -Category 'CheckSymbols' -Message "$DupedSymbols/$TotalPackages packages had duplicated symbol files and could not be extracted" } ExitWithExitCode 1 diff --git a/eng/common/sdk-task.ps1 b/eng/common/sdk-task.ps1 index f55c43c6f..65f1d75f3 100644 --- a/eng/common/sdk-task.ps1 +++ b/eng/common/sdk-task.ps1 @@ -53,7 +53,7 @@ try { } if ($task -eq "") { - Write-PipelineTelemetryError -Category 'Build' -Message "Missing required parameter '-task '" -ForegroundColor Red + Write-PipelineTelemetryError -Category 'Build' -Message "Missing required parameter '-task '" Print-Usage ExitWithExitCode 1 } @@ -78,7 +78,7 @@ try { $taskProject = GetSdkTaskProject $task if (!(Test-Path $taskProject)) { - Write-PipelineTelemetryError -Category 'Build' -Message "Unknown task: $task" -ForegroundColor Red + Write-PipelineTelemetryError -Category 'Build' -Message "Unknown task: $task" ExitWithExitCode 1 } diff --git a/eng/common/templates/job/onelocbuild.yml b/eng/common/templates/job/onelocbuild.yml index 928a70cda..2acdd5256 100644 --- a/eng/common/templates/job/onelocbuild.yml +++ b/eng/common/templates/job/onelocbuild.yml @@ -11,11 +11,14 @@ parameters: SourcesDirectory: $(Build.SourcesDirectory) CreatePr: true + AutoCompletePr: false + UseLfLineEndings: true UseCheckedInLocProjectJson: false LanguageSet: VS_Main_Languages LclSource: lclFilesInRepo LclPackageId: '' RepoType: gitHub + condition: '' jobs: - job: OneLocBuild @@ -43,23 +46,27 @@ jobs: filePath: $(Build.SourcesDirectory)/eng/common/generate-locproject.ps1 arguments: $(_GenerateLocProjectArguments) displayName: Generate LocProject.json + condition: ${{ parameters.condition }} - task: OneLocBuild@2 displayName: OneLocBuild env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) inputs: - locProj: Localize/LocProject.json + locProj: eng/Localize/LocProject.json outDir: $(Build.ArtifactStagingDirectory) lclSource: ${{ parameters.LclSource }} lclPackageId: ${{ parameters.LclPackageId }} isCreatePrSelected: ${{ parameters.CreatePr }} + ${{ if eq(parameters.CreatePr, true) }}: + isAutoCompletePrSelected: ${{ parameters.AutoCompletePr }} + isUseLfLineEndingsSelected: ${{ parameters.UseLfLineEndings }} packageSourceAuth: patAuth patVariable: ${{ parameters.CeapexPat }} ${{ if eq(parameters.RepoType, 'gitHub') }}: repoType: ${{ parameters.RepoType }} gitHubPatVariable: "${{ parameters.GithubPat }}" - condition: always() + condition: ${{ parameters.condition }} - task: PublishBuildArtifacts@1 displayName: Publish Localization Files @@ -67,12 +74,12 @@ jobs: PathtoPublish: '$(Build.ArtifactStagingDirectory)/loc' PublishLocation: Container ArtifactName: Loc - condition: always() + condition: ${{ parameters.condition }} - task: PublishBuildArtifacts@1 displayName: Publish LocProject.json inputs: - PathtoPublish: '$(Build.SourcesDirectory)/Localize/' + PathtoPublish: '$(Build.SourcesDirectory)/eng/Localize/' PublishLocation: Container ArtifactName: Loc - condition: always() \ No newline at end of file + condition: ${{ parameters.condition }} \ No newline at end of file diff --git a/eng/common/templates/job/source-build.yml b/eng/common/templates/job/source-build.yml index aad414649..5023d36dc 100644 --- a/eng/common/templates/job/source-build.yml +++ b/eng/common/templates/job/source-build.yml @@ -15,6 +15,9 @@ parameters: # nonPortable: false # Enables non-portable mode. This means a more specific RID (e.g. fedora.32-x64 rather than # linux-x64), and compiling against distro-provided packages rather than portable ones. + # skipPublishValidation: false + # Disables publishing validation. By default, a check is performed to ensure no packages are + # published by source-build. # container: '' # A container to use. Runs in docker. # pool: {} diff --git a/eng/common/templates/job/source-index-stage1.yml b/eng/common/templates/job/source-index-stage1.yml index c002a2b1b..a649d2b59 100644 --- a/eng/common/templates/job/source-index-stage1.yml +++ b/eng/common/templates/job/source-index-stage1.yml @@ -1,6 +1,6 @@ parameters: runAsPublic: false - sourceIndexPackageVersion: 1.0.1-20210225.1 + sourceIndexPackageVersion: 1.0.1-20210421.1 sourceIndexPackageSource: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json sourceIndexBuildCommand: powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "eng/common/build.ps1 -restore -build -binarylog -ci" preSteps: [] diff --git a/eng/common/templates/post-build/channels/generic-internal-channel.yml b/eng/common/templates/post-build/channels/generic-internal-channel.yml index 58fa9a35b..8990dfc8c 100644 --- a/eng/common/templates/post-build/channels/generic-internal-channel.yml +++ b/eng/common/templates/post-build/channels/generic-internal-channel.yml @@ -40,6 +40,9 @@ stages: pool: vmImage: 'windows-2019' steps: + - script: echo "##vso[task.logissue type=warning]Going forward, v2 Arcade publishing is no longer supported. Please read https://github.com/dotnet/arcade/blob/main/Documentation/CorePackages/Publishing.md for details, then contact dnceng if you have further questions." + displayName: Warn about v2 Arcade Publishing Usage + # This is necessary whenever we want to publish/restore to an AzDO private feed - task: NuGetAuthenticate@0 displayName: 'Authenticate to AzDO Feeds' @@ -110,6 +113,9 @@ stages: pool: vmImage: 'windows-2019' steps: + - script: echo "##vso[task.logissue type=warning]Going forward, v2 Arcade publishing is no longer supported. Please read https://github.com/dotnet/arcade/blob/main/Documentation/CorePackages/Publishing.md for details, then contact dnceng if you have further questions." + displayName: Warn about v2 Arcade Publishing Usage + - task: DownloadBuildArtifacts@0 displayName: Download Build Assets continueOnError: true diff --git a/eng/common/templates/post-build/channels/generic-public-channel.yml b/eng/common/templates/post-build/channels/generic-public-channel.yml index b50c0b3bd..3220c6a4f 100644 --- a/eng/common/templates/post-build/channels/generic-public-channel.yml +++ b/eng/common/templates/post-build/channels/generic-public-channel.yml @@ -42,6 +42,9 @@ stages: pool: vmImage: 'windows-2019' steps: + - script: echo "##vso[task.logissue type=warning]Going forward, v2 Arcade publishing is no longer supported. Please read https://github.com/dotnet/arcade/blob/main/Documentation/CorePackages/Publishing.md for details, then contact dnceng if you have further questions." + displayName: Warn about v2 Arcade Publishing Usage + - task: DownloadBuildArtifacts@0 displayName: Download Build Assets continueOnError: true @@ -109,6 +112,9 @@ stages: pool: vmImage: 'windows-2019' steps: + - script: echo "##vso[task.logissue type=warning]Going forward, v2 Arcade publishing is no longer supported. Please read https://github.com/dotnet/arcade/blob/main/Documentation/CorePackages/Publishing.md for details, then contact dnceng if you have further questions." + displayName: Warn about v2 Arcade Publishing Usage + - task: DownloadBuildArtifacts@0 displayName: Download Build Assets continueOnError: true diff --git a/eng/common/templates/steps/source-build.yml b/eng/common/templates/steps/source-build.yml index 65ee5992b..e20637ed6 100644 --- a/eng/common/templates/steps/source-build.yml +++ b/eng/common/templates/steps/source-build.yml @@ -34,9 +34,14 @@ steps: targetRidArgs='/p:TargetRid=${{ parameters.platform.targetRID }}' fi + publishArgs= + if [ '${{ parameters.platform.skipPublishValidation }}' != 'true' ]; then + publishArgs='--publish' + fi + ${{ coalesce(parameters.platform.buildScript, './build.sh') }} --ci \ --configuration $buildConfig \ - --restore --build --pack --publish -bl \ + --restore --build --pack $publishArgs -bl \ $officialBuildArgs \ $targetRidArgs \ /p:SourceBuildNonPortable=${{ parameters.platform.nonPortable }} \ diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1 index d52467eea..2d8a74f7d 100644 --- a/eng/common/tools.ps1 +++ b/eng/common/tools.ps1 @@ -48,6 +48,9 @@ # True to use global NuGet cache instead of restoring packages to repository-local directory. [bool]$useGlobalNuGetCache = if (Test-Path variable:useGlobalNuGetCache) { $useGlobalNuGetCache } else { !$ci } +# True to exclude prerelease versions Visual Studio during build +[bool]$excludePrereleaseVS = if (Test-Path variable:excludePrereleaseVS) { $excludePrereleaseVS } else { $false } + # An array of names of processes to stop on script exit if prepareMachine is true. $processesToStopOnExit = if (Test-Path variable:processesToStopOnExit) { $processesToStopOnExit } else { @('msbuild', 'dotnet', 'vbcscompiler') } @@ -463,7 +466,11 @@ function LocateVisualStudio([object]$vsRequirements = $null){ } if (!$vsRequirements) { $vsRequirements = $GlobalJson.tools.vs } - $args = @('-latest', '-prerelease', '-format', 'json', '-requires', 'Microsoft.Component.MSBuild', '-products', '*') + $args = @('-latest', '-format', 'json', '-requires', 'Microsoft.Component.MSBuild', '-products', '*') + + if (!$excludePrereleaseVS) { + $args += '-prerelease' + } if (Get-Member -InputObject $vsRequirements -Name 'version') { $args += '-version' @@ -489,7 +496,13 @@ function LocateVisualStudio([object]$vsRequirements = $null){ function InitializeBuildTool() { if (Test-Path variable:global:_BuildTool) { - return $global:_BuildTool + # If the requested msbuild parameters do not match, clear the cached variables. + if($global:_BuildTool.Contains('ExcludePrereleaseVS') -and $global:_BuildTool.ExcludePrereleaseVS -ne $excludePrereleaseVS) { + Remove-Item variable:global:_BuildTool + Remove-Item variable:global:_MSBuildExe + } else { + return $global:_BuildTool + } } if (-not $msbuildEngine) { @@ -517,7 +530,7 @@ function InitializeBuildTool() { ExitWithExitCode 1 } - $buildTool = @{ Path = $msbuildPath; Command = ""; Tool = "vs"; Framework = "net472" } + $buildTool = @{ Path = $msbuildPath; Command = ""; Tool = "vs"; Framework = "net472"; ExcludePrereleaseVS = $excludePrereleaseVS } } else { Write-PipelineTelemetryError -Category 'InitializeToolset' -Message "Unexpected value of -msbuildEngine: '$msbuildEngine'." ExitWithExitCode 1 diff --git a/global.json b/global.json index 7b55f2186..382a76cc4 100644 --- a/global.json +++ b/global.json @@ -1,11 +1,11 @@ { "sdk": { - "version": "6.0.100-preview.2.21155.3", + "version": "6.0.100-preview.3.21202.5", "allowPrerelease": true, "rollForward": "major" }, "tools": { - "dotnet": "6.0.100-preview.2.21155.3", + "dotnet": "6.0.100-preview.3.21202.5", "runtimes": { "dotnet": [ "3.1.0" @@ -17,6 +17,6 @@ "xcopy-msbuild": "16.8.0-preview2.1" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "6.0.0-beta.21212.6" + "Microsoft.DotNet.Arcade.Sdk": "6.0.0-beta.21304.1" } } diff --git a/samples/CSharp/SourceGenerators/GeneratedDemo/GeneratedDemo.csproj b/samples/CSharp/SourceGenerators/GeneratedDemo/CSharpGeneratedDemo.csproj similarity index 82% rename from samples/CSharp/SourceGenerators/GeneratedDemo/GeneratedDemo.csproj rename to samples/CSharp/SourceGenerators/GeneratedDemo/CSharpGeneratedDemo.csproj index b95bde39c..4b3054e9f 100644 --- a/samples/CSharp/SourceGenerators/GeneratedDemo/GeneratedDemo.csproj +++ b/samples/CSharp/SourceGenerators/GeneratedDemo/CSharpGeneratedDemo.csproj @@ -13,7 +13,7 @@ - + diff --git a/samples/CSharp/SourceGenerators/README.md b/samples/CSharp/SourceGenerators/README.md index fe06c349e..958cf03f6 100644 --- a/samples/CSharp/SourceGenerators/README.md +++ b/samples/CSharp/SourceGenerators/README.md @@ -28,7 +28,7 @@ You can add the sample generators to your own project by adding an item group co ```xml - + ``` diff --git a/samples/CSharp/SourceGenerators/SourceGeneratorSamples/SourceGeneratorSamples.csproj b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/CSharpSourceGeneratorSamples.csproj similarity index 100% rename from samples/CSharp/SourceGenerators/SourceGeneratorSamples/SourceGeneratorSamples.csproj rename to samples/CSharp/SourceGenerators/SourceGeneratorSamples/CSharpSourceGeneratorSamples.csproj diff --git a/samples/CSharp/SourceGenerators/SourceGenerators.sln b/samples/CSharp/SourceGenerators/SourceGenerators.sln index 1e5b737dc..1732a9c41 100644 --- a/samples/CSharp/SourceGenerators/SourceGenerators.sln +++ b/samples/CSharp/SourceGenerators/SourceGenerators.sln @@ -3,9 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30022.13 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceGeneratorSamples", "SourceGeneratorSamples\SourceGeneratorSamples.csproj", "{B452269D-856C-4FE6-8900-3D81461AF864}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceGeneratorSamples", "SourceGeneratorSamples\CSharpSourceGeneratorSamples.csproj", "{B452269D-856C-4FE6-8900-3D81461AF864}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeneratedDemo", "GeneratedDemo\GeneratedDemo.csproj", "{DB138C8B-7C34-4AC7-A443-A0B29D1CE8A5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeneratedDemo", "GeneratedDemo\CSharpGeneratedDemo.csproj", "{DB138C8B-7C34-4AC7-A443-A0B29D1CE8A5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/samples/VisualBasic/SourceGenerators/GeneratedDemo/Cars.csv b/samples/VisualBasic/SourceGenerators/GeneratedDemo/Cars.csv new file mode 100644 index 000000000..26a5d2051 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/GeneratedDemo/Cars.csv @@ -0,0 +1,4 @@ +Brand, Model, Year, cc, Favorite +Fiat, Punto, 2008, 12.3, No +Ford, Wagon, 1956, 20.3, No +BMW, "335", 2014, 20.3, Yes \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/GeneratedDemo/MainSettings.xmlsettings b/samples/VisualBasic/SourceGenerators/GeneratedDemo/MainSettings.xmlsettings new file mode 100644 index 000000000..96b96f5e5 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/GeneratedDemo/MainSettings.xmlsettings @@ -0,0 +1,6 @@ + + + False + 1234 + Hello World! + \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/GeneratedDemo/People.csv b/samples/VisualBasic/SourceGenerators/GeneratedDemo/People.csv new file mode 100644 index 000000000..5179c3539 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/GeneratedDemo/People.csv @@ -0,0 +1,3 @@ +Name, address, 11Age +"Luca Bol", "23 Bell Street", 90 +"john doe", "32 Carl street", 45 \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/GeneratedDemo/Program.vb b/samples/VisualBasic/SourceGenerators/GeneratedDemo/Program.vb new file mode 100644 index 000000000..fd209d06b --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/GeneratedDemo/Program.vb @@ -0,0 +1,33 @@ +Option Explicit On +Option Strict On +Option Infer On + +Module Program + + Public Sub Main() + + Console.WriteLine("Running HelloWorld: +") + UseHelloWorldGenerator.Run() + + Console.WriteLine(" + +Running AutoNotify: +") + UseAutoNotifyGenerator.Run() + + Console.WriteLine(" + +Running XmlSettings: +") + UseXmlSettingsGenerator.Run() + + Console.WriteLine(" + +Running CsvGenerator: +") + UseCsvGenerator.Run() + + End Sub + +End Module diff --git a/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseAutoNotifyGenerator.vb b/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseAutoNotifyGenerator.vb new file mode 100644 index 000000000..f810bd4a6 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseAutoNotifyGenerator.vb @@ -0,0 +1,42 @@ +Option Explicit On +Option Strict On +Option Infer On + +Imports AutoNotify + +' The view model we'd like to augment +Partial Public Class ExampleViewModel + + + Private _text As String = "private field text" + + + Private _amount As Integer = 5 + +End Class + +Public Module UseAutoNotifyGenerator + + Public Sub Run() + + Dim vm As New ExampleViewModel() + + ' we didn't explicitly create the 'Text' property, it was generated for us + Dim text = vm.Text + Console.WriteLine($"Text = {text}") + + ' Properties can have differnt names generated based on the PropertyName argument of the attribute + Dim count = vm.Count + Console.WriteLine($"Count = {count}") + + ' the viewmodel will automatically implement INotifyPropertyChanged + AddHandler vm.PropertyChanged, Sub(o, e) Console.WriteLine($"Property {e.PropertyName} was changed") + vm.Text = "abc" + vm.Count = 123 + + ' Try adding fields to the ExampleViewModel class above and tagging them with the attribute + ' You'll see the matching generated properties visibile in IntelliSense in realtime + + End Sub + +End Module diff --git a/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseCsvGenerator.vb b/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseCsvGenerator.vb new file mode 100644 index 000000000..f4c055835 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseCsvGenerator.vb @@ -0,0 +1,18 @@ +Option Explicit On +Option Strict On +Option Infer On + +Imports CSV + +Friend Class UseCsvGenerator + + Public Shared Sub Run() + + Console.WriteLine("## CARS") + Cars.All.ToList().ForEach(Sub(c) Console.WriteLine(c.Brand & vbTab & c.Model & vbTab & c.Year & vbTab & c.Cc & vbTab & c.Favorite)) + Console.WriteLine(vbCr & "## PEOPLE") + People.All.ToList().ForEach(Sub(p) Console.WriteLine(p.Name & vbTab & p.Address & vbTab & p._11Age)) + + End Sub + +End Class diff --git a/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseHelloWorldGenerator.vb b/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseHelloWorldGenerator.vb new file mode 100644 index 000000000..89fa11b47 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseHelloWorldGenerator.vb @@ -0,0 +1,8 @@ +Public Module UseHelloWorldGenerator + + Public Sub Run() + ' The static call below is generated at build time, and will list the syntax trees used in the compilation + HelloWorldGenerated.HelloWorld.SayHello() + End Sub + +End Module diff --git a/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseXmlSettingsGenerator.vb b/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseXmlSettingsGenerator.vb new file mode 100644 index 000000000..eb9a8e730 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/GeneratedDemo/UseXmlSettingsGenerator.vb @@ -0,0 +1,25 @@ +Imports AutoSettings + +Public Module UseXmlSettingsGenerator + + Public Sub Run() + + ' This XmlSettings generator makes a static property in the XmlSettings class for each .xmlsettings file + + ' here we have the 'Main' settings file from MainSettings.xmlsettings + ' the name is determined by the 'name' attribute of the root settings element + Dim main As XmlSettings.MainSettings = XmlSettings.Main + Console.WriteLine($"Reading settings from {main.GetLocation()}") + + ' settings are strongly typed and can be read directly from the static instance + Dim firstRun As Boolean = XmlSettings.Main.FirstRun + Console.WriteLine($"Setting firstRun = {firstRun}") + + Dim cacheSize As Integer = XmlSettings.Main.CacheSize + Console.WriteLine($"Setting cacheSize = {cacheSize}") + + ' Try adding some keys to the settings file and see the settings become available to read from + + End Sub + +End Module diff --git a/samples/VisualBasic/SourceGenerators/GeneratedDemo/VisualBasicGeneratedDemo.vbproj b/samples/VisualBasic/SourceGenerators/GeneratedDemo/VisualBasicGeneratedDemo.vbproj new file mode 100644 index 000000000..6df6a390c --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/GeneratedDemo/VisualBasicGeneratedDemo.vbproj @@ -0,0 +1,23 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/README.md b/samples/VisualBasic/SourceGenerators/README.md new file mode 100644 index 000000000..63d0029eb --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/README.md @@ -0,0 +1,35 @@ +🚧 Work In Progress +======== + +These samples are for an in-progress feature of Roslyn. As such they may change or break as the feature is developed, and no level of support is implied. + +For more information on the Source Generators feature, see the [design document](https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.md). + +Prerequisites +----- + +These samples require **Visual Studio 16.9.0 Preview 2.0** or higher. + +Building the samples +----- +Open `SourceGenerators.sln` in Visual Studio or run `dotnet build` from the `\SourceGenerators` directory. + +Running the samples +----- + +The generators must be run as part of another build, as they inject source into the project being built. This repo contains a sample project `GeneratorDemo` that relies of the sample generators to add code to it's compilation. + +Run `GeneratedDemo` in Visual studio or run `dotnet run` from the `GeneratorDemo` directory. + +Using the samples in your project +----- + +You can add the sample generators to your own project by adding an item group containing an analyzer reference: + +```xml + + + +``` + +You will most likely need to close and reopen the solution in Visual Studio for any changes made to the generators to take effect. diff --git a/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/AutoNotifyGenerator.vb b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/AutoNotifyGenerator.vb new file mode 100644 index 000000000..4c1ca4af8 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/AutoNotifyGenerator.vb @@ -0,0 +1,201 @@ +Option Explicit On +Option Infer On +Option Strict On + +Imports System.Text + +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.Text +Imports Microsoft.CodeAnalysis.VisualBasic +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax + +Namespace SourceGeneratorSamples + + + Public Class AutoNotifyGenerator + Implements ISourceGenerator + + Private Const ATTRIBUTE_TEXT As String = " +Imports System + +Namespace Global.AutoNotify + + Friend NotInheritable Class AutoNotifyAttribute + Inherits Attribute + + Public Sub New() + End Sub + + Public Property PropertyName As String + End Class +End Namespace +" + + Public Sub Initialize(context As GeneratorInitializationContext) Implements ISourceGenerator.Initialize + ' Register a syntax receiver that will be created for each generation pass + context.RegisterForSyntaxNotifications(Function() As ISyntaxReceiver + Return New SyntaxReceiver + End Function) + End Sub + + Public Sub Execute(context As GeneratorExecutionContext) Implements ISourceGenerator.Execute + + ' add the attribute text + context.AddSource("AutoNotifyAttribute", SourceText.From(ATTRIBUTE_TEXT, Encoding.UTF8)) + + ' retreive the populated receiver + Dim tempVar = TypeOf context.SyntaxReceiver Is SyntaxReceiver + Dim receiver = TryCast(context.SyntaxReceiver, SyntaxReceiver) + If Not tempVar Then + Return + End If + + ' we're going to create a new compilation that contains the attribute. + ' TODO: we should allow source generators to provide source during initialize, so that this step isn't required. + Dim options1 = context.Compilation.SyntaxTrees.First().Options + Dim compilation1 = context.Compilation.AddSyntaxTrees(VisualBasicSyntaxTree.ParseText(SourceText.From(ATTRIBUTE_TEXT, Encoding.UTF8), CType(options1, VisualBasicParseOptions))) + + ' get the newly bound attribute, and INotifyPropertyChanged + Dim attributeSymbol = compilation1.GetTypeByMetadataName("AutoNotify.AutoNotifyAttribute") + Dim notifySymbol = compilation1.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged") + + ' loop over the candidate fields, and keep the ones that are actually annotated + Dim fieldSymbols As New List(Of IFieldSymbol) + + For Each field In receiver.CandidateFields + Dim model = compilation1.GetSemanticModel(field.SyntaxTree) + For Each variable In field.Declarators + For Each name In variable.Names + ' Get the symbol being decleared by the field, and keep it if its annotated + Dim fieldSymbol = TryCast(model.GetDeclaredSymbol(name), IFieldSymbol) + If fieldSymbol.GetAttributes().Any(Function(ad) ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.[Default])) Then + fieldSymbols.Add(fieldSymbol) + End If + Next + Next + Next + + ' group the fields by class, and generate the source + For Each group In fieldSymbols.GroupBy(Function(f) f.ContainingType, SymbolEqualityComparer.Default) + Dim classSource = ProcessClass(CType(group.Key, INamedTypeSymbol), group.ToList(), attributeSymbol, notifySymbol) + context.AddSource($"{group.Key.Name}_AutoNotify.vb", SourceText.From(classSource, Encoding.UTF8)) + Next + + End Sub + + Private Function ProcessClass(classSymbol As INamedTypeSymbol, fields As List(Of IFieldSymbol), attributeSymbol As ISymbol, notifySymbol As ISymbol) As String + + If Not classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.[Default]) Then + Return Nothing 'TODO: issue a diagnostic that it must be top level + End If + + Dim namespaceName = classSymbol.ContainingNamespace.ToDisplayString() + + ' begin building the generated source + Dim source = New StringBuilder($"Option Explicit On +Option Strict On +Option Infer On + +Namespace Global.{namespaceName} + + Partial Public Class {classSymbol.Name} + Implements {notifySymbol.ToDisplayString()} + +") + + ' if the class doesn't implement INotifyPropertyChanged already, add it + If Not classSymbol.Interfaces.Contains(CType(notifySymbol, INamedTypeSymbol)) Then + source.Append(" Public Event PropertyChanged As System.ComponentModel.PropertyChangedEventHandler Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged +") + End If + + ' create properties for each field + For Each fieldSymbol In fields + ProcessField(source, fieldSymbol, attributeSymbol) + Next + + source.Append(" + End Class + +End Namespace") + + Return source.ToString() + + End Function + + Private Sub ProcessField(source As StringBuilder, fieldSymbol As IFieldSymbol, attributeSymbol As ISymbol) + + Dim chooseName As Func(Of String, TypedConstant, String) = + Function(fieldName1 As String, overridenNameOpt1 As TypedConstant) As String + + If Not overridenNameOpt1.IsNull Then + Return overridenNameOpt1.Value.ToString() + End If + + fieldName1 = fieldName1.TrimStart("_"c) + If fieldName1.Length = 0 Then + Return String.Empty + End If + + If fieldName1.Length = 1 Then + Return fieldName1.ToUpper() + End If + + Return fieldName1.Substring(0, 1).ToUpper() & fieldName1.Substring(1) + + End Function + + ' get the name and type of the field + Dim fieldName = fieldSymbol.Name + Dim fieldType = fieldSymbol.Type + + ' get the AutoNotify attribute from the field, and any associated data + Dim attributeData = fieldSymbol.GetAttributes().[Single](Function(ad) ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.[Default])) + Dim overridenNameOpt = attributeData.NamedArguments.SingleOrDefault(Function(kvp) kvp.Key = "PropertyName").Value + + Dim propertyName = chooseName(fieldName, overridenNameOpt) + If propertyName.Length = 0 OrElse propertyName = fieldName Then + 'TODO: issue a diagnostic that we can't process this field + Return + End If + + source.Append($" + Public Property {propertyName} As {fieldType} + Get + Return Me.{fieldName} + End Get + Set(value As {fieldType}) + Me.{fieldName} = value + RaiseEvent PropertyChanged(Me, New System.ComponentModel.PropertyChangedEventArgs(NameOf({propertyName}))) + End Set + End Property +") + + End Sub + + ''' + ''' Created on demand before each generation pass + ''' + Class SyntaxReceiver + Implements ISyntaxReceiver + + Public ReadOnly Property CandidateFields As List(Of FieldDeclarationSyntax) = New List(Of FieldDeclarationSyntax) + + ''' + ''' Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation + ''' + Public Sub OnVisitSyntaxNode(syntaxNode As SyntaxNode) Implements ISyntaxReceiver.OnVisitSyntaxNode + ' any field with at least one attribute is a candidate for property generation + If TypeOf syntaxNode Is FieldDeclarationSyntax Then + Dim fieldDeclarationSyntax = TryCast(syntaxNode, FieldDeclarationSyntax) + If fieldDeclarationSyntax.AttributeLists.Count > 0 Then + CandidateFields.Add(fieldDeclarationSyntax) + End If + End If + End Sub + + End Class + + End Class + +End Namespace diff --git a/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/CsvGenerator.props b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/CsvGenerator.props new file mode 100644 index 000000000..0741032bb --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/CsvGenerator.props @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/CsvGenerator.vb b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/CsvGenerator.vb new file mode 100644 index 000000000..fa7d13b90 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/CsvGenerator.vb @@ -0,0 +1,208 @@ +Option Explicit On +Option Infer On +Option Strict On + +Imports System.IO +Imports System.Text + +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.Text + +Imports NotVisualBasic.FileIO + +' CsvTextFileParser from https://github.com/22222/CsvTextFieldParser adding suppression rules for default VS config + +Namespace SourceGeneratorSamples + + + Public Class CsvGenerator + Implements ISourceGenerator + + Public Enum CsvLoadType + Startup + OnDemand + End Enum + + Public Sub Initialize(context As GeneratorInitializationContext) Implements Microsoft.CodeAnalysis.ISourceGenerator.Initialize + + End Sub + + Public Sub Execute(context As GeneratorExecutionContext) Implements ISourceGenerator.Execute + Dim options As IEnumerable(Of (CsvLoadType, Boolean, AdditionalText)) = GetLoadOptions(context) + Dim nameCodeSequence As IEnumerable(Of (Name As String, Code As String)) = SourceFilesFromAdditionalFiles(options) + For Each entry In nameCodeSequence + context.AddSource($"Csv_{entry.Name}", SourceText.From(entry.Code, Encoding.UTF8)) + Next + End Sub + + ' Guesses type of property for the object from the value of a csv field + Public Shared Function GetCsvFieldType(exemplar As String) As String + Dim garbageBoolean As Boolean + Dim garbageInteger As Integer + Dim garbageDouble As Double + Select Case True + Case Boolean.TryParse(exemplar, garbageBoolean) : Return "Boolean" + Case Integer.TryParse(exemplar, garbageInteger) : Return "Integer" + Case Double.TryParse(exemplar, garbageDouble) : Return "Double" + Case Else : Return "String" + End Select + End Function + + ' Examines the header row and the first row in the csv file to gather all header types and names + ' Also it returns the first row of data, because it must be read to figure out the types, + ' As the CsvTextFieldParser cannot 'Peek' ahead of one line. If there is no first line, + ' it consider all properties as strings. The generator returns an empty list of properly + ' typed objects in such case. If the file is completely empty, an error is generated. + Public Shared Function ExtractProperties(parser As CsvTextFieldParser) As (Types As String(), Names As String(), Fields As String()) + + Dim headerFields = parser.ReadFields() + If headerFields Is Nothing Then + Throw New Exception("Empty csv file!") + End If + + Dim firstLineFields = parser.ReadFields() + If firstLineFields Is Nothing Then + Return (Enumerable.Repeat("String", headerFields.Length).ToArray(), headerFields, firstLineFields) + Else + Return (firstLineFields.[Select](Function(field) GetCsvFieldType(field)).ToArray(), headerFields.[Select](New Func(Of String, String)(AddressOf StringToValidPropertyName)).ToArray(), firstLineFields) + End If + + End Function + + ' Adds a class to the `CSV` namespace for each `csv` file passed in. The class has a static property + ' named `All` that returns the list of strongly typed objects generated on demand at first access. + ' There is the slight chance of a race condition in a multi-thread program, but the result is relatively benign + ' , loading the collection multiple times instead of once. Measures could be taken to avoid that. + Public Shared Function GenerateClassFile(className As String, csvText As String, loadTime As CsvLoadType, cacheObjects As Boolean) As String + + Dim sb As New StringBuilder + Dim parser As New CsvTextFieldParser(New StringReader(csvText)) + + ''' Imports + sb.Append("Option Explicit On +Option Strict On +Option Infer On + +Imports System.Collections.Generic + +Namespace Global.CSV +") + + ''' Class Definition + sb.Append($" + Public Class {className} + +") + + If loadTime = CsvLoadType.Startup Then + sb.Append($" Shared Sub New() + Dim x = All + End Sub + +") + End If + + Dim tupleTemp = ExtractProperties(parser) : Dim types = tupleTemp.Types, names = tupleTemp.Names, fields = tupleTemp.Fields + Dim minLen = Math.Min(types.Length, names.Length) + + For i = 0 To minLen - 1 + sb.AppendLine($" Public Property {StringToValidPropertyName(names(i))} As {types(i)}") + Next + + ''' Loading data + sb.Append($" + Private Shared m_all As IEnumerable(Of {className}) + + Public Shared ReadOnly Property All As IEnumerable(Of {className}) + Get +") + + If cacheObjects Then + sb.Append(" If m_all IsNot Nothing Then + Return m_all + End If +") + End If + + sb.Append($" Dim l As New List(Of {className})() + Dim c As {className} +") + + Do + + If fields Is Nothing Then + Continue Do + End If + If fields.Length < minLen Then + Throw New Exception("Not enough fields in CSV file.") + End If + + sb.AppendLine($" c = New {className}()") + + Dim value As String '= "" + For i As Integer = 0 To minLen - 1 + ' Wrap strings in quotes. + value = If(GetCsvFieldType(fields(i)) = "String", $"""{fields(i).Trim().Trim(New Char() {""""c})}""", fields(i)) + sb.AppendLine($" c.{names(i)} = {value}") + Next + + sb.AppendLine(" l.Add(c)") + + fields = parser.ReadFields() + + Loop While fields IsNot Nothing + + sb.Append($" m_all = l + Return l +") + + ' Close things (property, class, namespace) + sb.Append(" End Get + End Property + + End Class + +End Namespace") + + Return sb.ToString() + + End Function + + Private Shared Function StringToValidPropertyName(s As String) As String + s = s.Trim() + s = If(Char.IsLetter(s(0)), Char.ToUpper(s(0)) & s.Substring(1), s) + s = If(Char.IsDigit(s.Trim()(0)), "_" & s, s) + s = New String(s.[Select](Function(ch) If(Char.IsDigit(ch) OrElse Char.IsLetter(ch), ch, "_"c)).ToArray()) + Return s + End Function + + Private Shared Function SourceFilesFromAdditionalFile(loadType As CsvLoadType, cacheObjects As Boolean, file As AdditionalText) As IEnumerable(Of (Name As String, Code As String)) + Dim className = Path.GetFileNameWithoutExtension(file.Path) + Dim csvText = file.GetText().ToString() + Return New(String, String)() {(className, GenerateClassFile(className, csvText, loadType, cacheObjects))} + End Function + + Private Shared Function SourceFilesFromAdditionalFiles(pathsData As IEnumerable(Of (LoadType As CsvLoadType, CacheObjects As Boolean, File As AdditionalText))) As IEnumerable(Of (Name As String, Code As String)) + Return pathsData.SelectMany(Function(d) SourceFilesFromAdditionalFile(d.LoadType, d.CacheObjects, d.File)) + End Function + + Private Shared Iterator Function GetLoadOptions(context As GeneratorExecutionContext) As IEnumerable(Of (LoadType As CsvLoadType, CacheObjects As Boolean, File As AdditionalText)) + For Each file In context.AdditionalFiles + If Path.GetExtension(file.Path).Equals(".csv", StringComparison.OrdinalIgnoreCase) Then + ' are there any options for it? + Dim loadTimeString As String = Nothing + context.AnalyzerConfigOptions.GetOptions(file).TryGetValue("build_metadata.additionalfiles.CsvLoadType", loadTimeString) + Dim loadType As CsvLoadType = Nothing + [Enum].TryParse(loadTimeString, ignoreCase:=True, loadType) + Dim cacheObjectsString As String = Nothing + context.AnalyzerConfigOptions.GetOptions(file).TryGetValue("build_metadata.additionalfiles.CacheObjects", cacheObjectsString) + Dim cacheObjects As Boolean = Nothing + Boolean.TryParse(cacheObjectsString, cacheObjects) + Yield (loadType, cacheObjects, file) + End If + Next + End Function + + End Class + +End Namespace diff --git a/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/HelloWorldGenerator.vb b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/HelloWorldGenerator.vb new file mode 100644 index 000000000..ff4c51ba2 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/HelloWorldGenerator.vb @@ -0,0 +1,66 @@ +Option Explicit On +Option Infer On +Option Strict On + +Imports System.Text + +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.Text + +Namespace SourceGeneratorSamples + + + Public Class HelloWorldGenerator + Implements ISourceGenerator + + Public Sub Initialize(context As GeneratorInitializationContext) Implements ISourceGenerator.Initialize + ' No initialization required + End Sub + + Public Sub Execute(context As GeneratorExecutionContext) Implements ISourceGenerator.Execute + + ' begin creating the source we'll inject into the users compilation + + Dim sourceBuilder = New StringBuilder("Option Explicit On +Option Strict On +Option Infer On + +Namespace Global.HelloWorldGenerated + + Public Module HelloWorld + + Public Sub SayHello() + + Console.WriteLine(""Hello from generated code!"") + Console.WriteLine(""The following syntax trees existed in the compilation that created this program:"") +") + + ' for testing... let's include a comment with the current date/time. + sourceBuilder.AppendLine($" ' Generated at {DateTime.Now}") + + ' using the context, get a list of syntax trees in the users compilation + ' add the filepath of each tree to the class we're building + + For Each tree In context.Compilation.SyntaxTrees + sourceBuilder.AppendLine($" Console.WriteLine("" - {tree.FilePath}"")") + Next + + ' finish creating the source to inject + + sourceBuilder.Append(" + + End Sub + + End Module + +End Namespace") + + ' inject the created source into the users compilation + + context.AddSource("HelloWorldGenerated", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8)) + + End Sub + + End Class + +End Namespace diff --git a/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/SettingsXmlGenerator.vb b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/SettingsXmlGenerator.vb new file mode 100644 index 000000000..d1ecd628f --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/SettingsXmlGenerator.vb @@ -0,0 +1,102 @@ +Option Explicit On +Option Infer On +Option Strict On + +Imports System.IO +Imports System.Text +Imports System.Xml + +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.Text + +Namespace SourceGeneratorSamples + + + Public Class SettingsXmlGenerator + Implements ISourceGenerator + + Public Sub Initialize(context As GeneratorInitializationContext) Implements ISourceGenerator.Initialize + + End Sub + + Public Sub Execute(context As GeneratorExecutionContext) Implements ISourceGenerator.Execute + ' Using the context, get any additional files that end in .xmlsettings + For Each settingsFile In context.AdditionalFiles.Where(Function(at) at.Path.EndsWith(".xmlsettings")) + ProcessSettingsFile(settingsFile, context) + Next + End Sub + + Private Sub ProcessSettingsFile(xmlFile As AdditionalText, context As GeneratorExecutionContext) + + ' try and load the settings file + Dim xmlDoc As New XmlDocument + Dim text = xmlFile.GetText(context.CancellationToken).ToString() + Try + xmlDoc.LoadXml(text) + Catch + 'TODO: issue a diagnostic that says we couldn't parse it + Return + End Try + + ' create a class in the XmlSetting class that represnts this entry, and a static field that contains a singleton instance. + Dim fileName = Path.GetFileName(xmlFile.Path) + Dim name = xmlDoc.DocumentElement.GetAttribute("name") + + Dim sb = New StringBuilder($"Option Explicit On +Option Strict On +Option Infer On + +Imports System.Xml + +Namespace Global.AutoSettings + + Partial Public Class XmlSettings + + Public Shared ReadOnly Property {name} As {name}Settings = New {name}Settings(""{fileName}"") + + Public Class {name}Settings + + Private m_xmlDoc As New XmlDocument() + + Private m_fileName As String + + Friend Sub New(fileName As String) + m_fileName = fileName + m_xmlDoc.Load(m_fileName) + End Sub + + Public Function GetLocation() As String + Return m_fileName + End Function") + + For i = 0 To xmlDoc.DocumentElement.ChildNodes.Count - 1 + + Dim setting = CType(xmlDoc.DocumentElement.ChildNodes(i), XmlElement) + Dim settingName = setting.GetAttribute("name") + Dim settingType = setting.GetAttribute("type") + + sb.Append($" + + Public ReadOnly Property {settingName} As {settingType} + Get + Return DirectCast(Convert.ChangeType(DirectCast(m_xmlDoc.DocumentElement.ChildNodes({i}), XmlElement).InnerText, GetType({settingType})), {settingType}) + End Get + End Property") + + Next + + sb.Append(" + + End Class + + End Class + +End Namespace") + + context.AddSource($"Settings_{name}", SourceText.From(sb.ToString(), Encoding.UTF8)) + + End Sub + + End Class + +End Namespace diff --git a/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/VisualBasicSourceGeneratorSamples.vbproj b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/VisualBasicSourceGeneratorSamples.vbproj new file mode 100644 index 000000000..9e9f12565 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/SourceGeneratorSamples/VisualBasicSourceGeneratorSamples.vbproj @@ -0,0 +1,27 @@ + + + + netstandard2.0 + + + + + + + + + + + + + + $(GetTargetPathDependsOn);GetDependencyTargetPaths + + + + + + + + + \ No newline at end of file diff --git a/samples/VisualBasic/SourceGenerators/SourceGenerators.sln b/samples/VisualBasic/SourceGenerators/SourceGenerators.sln new file mode 100644 index 000000000..6b7947985 --- /dev/null +++ b/samples/VisualBasic/SourceGenerators/SourceGenerators.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30022.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "GeneratedDemo", "GeneratedDemo\GeneratedDemo.vbproj", "{08612C19-D039-44D1-9030-D192CEAF05BB}" +EndProject +Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "SourceGeneratorSamples", "SourceGeneratorSamples\SourceGeneratorSamples.vbproj", "{90BDB1C3-E353-448C-8A29-E5B2EF10670B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {08612C19-D039-44D1-9030-D192CEAF05BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08612C19-D039-44D1-9030-D192CEAF05BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08612C19-D039-44D1-9030-D192CEAF05BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08612C19-D039-44D1-9030-D192CEAF05BB}.Release|Any CPU.Build.0 = Release|Any CPU + {90BDB1C3-E353-448C-8A29-E5B2EF10670B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90BDB1C3-E353-448C-8A29-E5B2EF10670B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90BDB1C3-E353-448C-8A29-E5B2EF10670B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90BDB1C3-E353-448C-8A29-E5B2EF10670B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {623C84B6-B8A4-4F29-8E68-4ED37D4529D5} + EndGlobalSection +EndGlobal diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/AnalyzerTest`1.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/AnalyzerTest`1.cs index 770326c4f..6410ab7e1 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/AnalyzerTest`1.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/AnalyzerTest`1.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -165,12 +166,34 @@ public string TestCode /// protected TimeSpan MatchDiagnosticsTimeout { get; set; } = TimeSpan.FromSeconds(2); + private readonly ConcurrentBag _workspaces = new ConcurrentBag(); + /// /// Runs the test. /// /// The that the operation will observe. /// A representing the asynchronous operation. - public virtual async Task RunAsync(CancellationToken cancellationToken = default) + public async Task RunAsync(CancellationToken cancellationToken = default) + { + try + { + await RunImplAsync(cancellationToken); + } + finally + { + while (_workspaces.TryTake(out var workspace)) + { + workspace.Dispose(); + } + } + } + + /// + /// Runs the test. + /// + /// The that the operation will observe. + /// A representing the asynchronous operation. + protected virtual async Task RunImplAsync(CancellationToken cancellationToken) { Verify.NotEmpty($"{nameof(TestState)}.{nameof(SolutionState.Sources)}", TestState.Sources); @@ -319,19 +342,19 @@ private async Task VerifySuppressionDiagnosticsAsync(ImmutableArrayA collection of s describing the expected /// diagnostics for the sources. /// The verifier to use for test assertions. - private void VerifyDiagnosticResults(IEnumerable actualResults, ImmutableArray analyzers, DiagnosticResult[] expectedResults, IVerifier verifier) + private void VerifyDiagnosticResults(IEnumerable<(Project project, Diagnostic diagnostic)> actualResults, ImmutableArray analyzers, DiagnosticResult[] expectedResults, IVerifier verifier) { var matchedDiagnostics = MatchDiagnostics(actualResults.ToArray(), expectedResults); verifier.Equal(actualResults.Count(), matchedDiagnostics.Count(x => x.actual is object), $"{nameof(MatchDiagnostics)} failed to include all actual diagnostics in the result"); verifier.Equal(expectedResults.Length, matchedDiagnostics.Count(x => x.expected is object), $"{nameof(MatchDiagnostics)} failed to include all expected diagnostics in the result"); - actualResults = matchedDiagnostics.Select(x => x.actual).WhereNotNull(); + actualResults = matchedDiagnostics.Select(x => x.actual).Where(x => x is { }).Select(x => x!.Value); expectedResults = matchedDiagnostics.Where(x => x.expected is object).Select(x => x.expected.GetValueOrDefault()).ToArray(); var expectedCount = expectedResults.Length; var actualCount = actualResults.Count(); - var diagnosticsOutput = actualResults.Any() ? FormatDiagnostics(analyzers, DefaultFilePath, actualResults.ToArray()) : " NONE."; + var diagnosticsOutput = actualResults.Any() ? FormatDiagnostics(analyzers, DefaultFilePath, actualResults.Select(result => result.diagnostic).ToArray()) : " NONE."; var message = $"Mismatch between number of diagnostics returned, expected \"{expectedCount}\" actual \"{actualCount}\"\r\n\r\nDiagnostics:\r\n{diagnosticsOutput}\r\n"; verifier.Equal(expectedCount, actualCount, message); @@ -342,51 +365,51 @@ private void VerifyDiagnosticResults(IEnumerable actualResults, Immu if (!expected.HasLocation) { - message = FormatVerifierMessage(analyzers, actual, expected, "Expected a project diagnostic with no location:"); - verifier.Equal(Location.None, actual.Location, message); + message = FormatVerifierMessage(analyzers, actual.diagnostic, expected, "Expected a project diagnostic with no location:"); + verifier.Equal(Location.None, actual.diagnostic.Location, message); } else { - VerifyDiagnosticLocation(analyzers, actual, expected, actual.Location, expected.Spans[0], verifier); + VerifyDiagnosticLocation(analyzers, actual.diagnostic, expected, actual.diagnostic.Location, expected.Spans[0], verifier); if (!expected.Options.HasFlag(DiagnosticOptions.IgnoreAdditionalLocations)) { - var additionalLocations = actual.AdditionalLocations.ToArray(); + var additionalLocations = actual.diagnostic.AdditionalLocations.ToArray(); - message = FormatVerifierMessage(analyzers, actual, expected, $"Expected {expected.Spans.Length - 1} additional locations but got {additionalLocations.Length} for Diagnostic:"); + message = FormatVerifierMessage(analyzers, actual.diagnostic, expected, $"Expected {expected.Spans.Length - 1} additional locations but got {additionalLocations.Length} for Diagnostic:"); verifier.Equal(expected.Spans.Length - 1, additionalLocations.Length, message); for (var j = 0; j < additionalLocations.Length; ++j) { - VerifyDiagnosticLocation(analyzers, actual, expected, additionalLocations[j], expected.Spans[j + 1], verifier); + VerifyDiagnosticLocation(analyzers, actual.diagnostic, expected, additionalLocations[j], expected.Spans[j + 1], verifier); } } } - message = FormatVerifierMessage(analyzers, actual, expected, $"Expected diagnostic id to be \"{expected.Id}\" was \"{actual.Id}\""); - verifier.Equal(expected.Id, actual.Id, message); + message = FormatVerifierMessage(analyzers, actual.diagnostic, expected, $"Expected diagnostic id to be \"{expected.Id}\" was \"{actual.diagnostic.Id}\""); + verifier.Equal(expected.Id, actual.diagnostic.Id, message); if (!expected.Options.HasFlag(DiagnosticOptions.IgnoreSeverity)) { - message = FormatVerifierMessage(analyzers, actual, expected, $"Expected diagnostic severity to be \"{expected.Severity}\" was \"{actual.Severity}\""); - verifier.Equal(expected.Severity, actual.Severity, message); + message = FormatVerifierMessage(analyzers, actual.diagnostic, expected, $"Expected diagnostic severity to be \"{expected.Severity}\" was \"{actual.diagnostic.Severity}\""); + verifier.Equal(expected.Severity, actual.diagnostic.Severity, message); } if (expected.Message != null) { - message = FormatVerifierMessage(analyzers, actual, expected, $"Expected diagnostic message to be \"{expected.Message}\" was \"{actual.GetMessage()}\""); - verifier.Equal(expected.Message, actual.GetMessage(), message); + message = FormatVerifierMessage(analyzers, actual.diagnostic, expected, $"Expected diagnostic message to be \"{expected.Message}\" was \"{actual.diagnostic.GetMessage()}\""); + verifier.Equal(expected.Message, actual.diagnostic.GetMessage(), message); } else if (expected.MessageArguments?.Length > 0) { - message = FormatVerifierMessage(analyzers, actual, expected, $"Expected diagnostic message arguments to match"); + message = FormatVerifierMessage(analyzers, actual.diagnostic, expected, $"Expected diagnostic message arguments to match"); verifier.SequenceEqual( expected.MessageArguments.Select(argument => argument?.ToString() ?? string.Empty), - GetArguments(actual).Select(argument => argument?.ToString() ?? string.Empty), + GetArguments(actual.diagnostic).Select(argument => argument?.ToString() ?? string.Empty), StringComparer.Ordinal, message); } - DiagnosticVerifier?.Invoke(actual, expected, verifier); + DiagnosticVerifier?.Invoke(actual.diagnostic, expected, verifier); } } @@ -418,11 +441,11 @@ private void VerifyDiagnosticResults(IEnumerable actualResults, Immu /// the total number of mismatched pairs. /// /// - private ImmutableArray<(Diagnostic? actual, DiagnosticResult? expected)> MatchDiagnostics(Diagnostic[] actualResults, DiagnosticResult[] expectedResults) + private ImmutableArray<((Project project, Diagnostic diagnostic)? actual, DiagnosticResult? expected)> MatchDiagnostics((Project project, Diagnostic diagnostic)[] actualResults, DiagnosticResult[] expectedResults) { - var actualIds = actualResults.Select(result => result.Id).ToImmutableArray(); - var actualResultLocations = actualResults.Select(result => (location: result.Location.GetLineSpan(), additionalLocations: result.AdditionalLocations.Select(location => location.GetLineSpan()).ToImmutableArray())).ToImmutableArray(); - var actualArguments = actualResults.Select(actual => GetArguments(actual).Select(argument => argument?.ToString() ?? string.Empty).ToImmutableArray()).ToImmutableArray(); + var actualIds = actualResults.Select(result => result.diagnostic.Id).ToImmutableArray(); + var actualResultLocations = actualResults.Select(result => (location: result.diagnostic.Location.GetLineSpan(), additionalLocations: result.diagnostic.AdditionalLocations.Select(location => location.GetLineSpan()).ToImmutableArray())).ToImmutableArray(); + var actualArguments = actualResults.Select(actual => GetArguments(actual.diagnostic).Select(argument => argument?.ToString() ?? string.Empty).ToImmutableArray()).ToImmutableArray(); expectedResults = expectedResults.ToOrderedArray(); var expectedArguments = expectedResults.Select(expected => expected.MessageArguments?.Select(argument => argument?.ToString() ?? string.Empty).ToImmutableArray() ?? ImmutableArray.Empty).ToImmutableArray(); @@ -430,9 +453,9 @@ private void VerifyDiagnosticResults(IEnumerable actualResults, Immu // Initialize the best match to a trivial result where everything is unmatched. This will be updated if/when // better matches are found. var bestMatchCount = MatchQuality.RemainingUnmatched(actualResults.Length + expectedResults.Length); - var bestMatch = actualResults.Select(result => ((Diagnostic?)result, default(DiagnosticResult?))).Concat(expectedResults.Select(result => (default(Diagnostic?), (DiagnosticResult?)result))).ToImmutableArray(); + var bestMatch = actualResults.Select(result => (((Project project, Diagnostic diagnostic)?)result, default(DiagnosticResult?))).Concat(expectedResults.Select(result => (default((Project project, Diagnostic diagnostic)?), (DiagnosticResult?)result))).ToImmutableArray(); - var builder = ImmutableArray.CreateBuilder<(Diagnostic? actual, DiagnosticResult? expected)>(); + var builder = ImmutableArray.CreateBuilder<((Project project, Diagnostic diagnostic)? actual, DiagnosticResult? expected)>(); var usedExpected = new bool[expectedResults.Length]; // The recursive match algorithm is not optimized, so use a timeout to ensure it completes in a reasonable @@ -508,7 +531,7 @@ MatchQuality RecursiveMatch(int firstActualIndex, int remainingActualItems, int } var (lineSpan, additionalLineSpans) = actualResultLocations[firstActualIndex]; - var matchValue = GetMatchValue(actualResults[firstActualIndex], actualIds[firstActualIndex], lineSpan, additionalLineSpans, actualArguments[firstActualIndex], expectedResults[i], expectedArguments[i]); + var matchValue = GetMatchValue(actualResults[firstActualIndex].diagnostic, actualIds[firstActualIndex], lineSpan, additionalLineSpans, actualArguments[firstActualIndex], expectedResults[i], expectedArguments[i]); if (matchValue == MatchQuality.None) { continue; @@ -969,13 +992,15 @@ private static bool IsInSourceFile(DiagnosticResult result, (string filename, So /// The that the task will observe. /// A collection of s that surfaced in the source code, sorted by /// . - private async Task> GetSortedDiagnosticsAsync(EvaluatedProjectState primaryProject, ImmutableArray additionalProjects, ImmutableArray analyzers, IVerifier verifier, CancellationToken cancellationToken) + private async Task> GetSortedDiagnosticsAsync(EvaluatedProjectState primaryProject, ImmutableArray additionalProjects, ImmutableArray analyzers, IVerifier verifier, CancellationToken cancellationToken) { var solution = await GetSolutionAsync(primaryProject, additionalProjects, verifier, cancellationToken); - var additionalDiagnostics = primaryProject.AdditionalDiagnostics; - foreach (var project in additionalProjects) + var primaryProjectInSolution = solution.Projects.Single(project => project.Name == DefaultTestProjectName); + var additionalDiagnostics = primaryProject.AdditionalDiagnostics.Select(diagnostic => (primaryProjectInSolution, diagnostic)).ToImmutableArray(); + foreach (var additionalProject in additionalProjects) { - additionalDiagnostics = additionalDiagnostics.AddRange(project.AdditionalDiagnostics); + var additionalProjectInSolution = solution.Projects.Single(project => project.Name == additionalProject.Name); + additionalDiagnostics = additionalDiagnostics.AddRange(additionalProject.AdditionalDiagnostics.Select(diagnostic => (additionalProjectInSolution, diagnostic))); } return await GetSortedDiagnosticsAsync(solution, analyzers, additionalDiagnostics, CompilerDiagnostics, verifier, cancellationToken); @@ -993,21 +1018,21 @@ private async Task> GetSortedDiagnosticsAsync(Evaluat /// The that the task will observe. /// A collection of s that surfaced in the source code, sorted by /// . - protected async Task> GetSortedDiagnosticsAsync(Solution solution, ImmutableArray analyzers, ImmutableArray additionalDiagnostics, CompilerDiagnostics compilerDiagnostics, IVerifier verifier, CancellationToken cancellationToken) + protected async Task> GetSortedDiagnosticsAsync(Solution solution, ImmutableArray analyzers, ImmutableArray<(Project project, Diagnostic diagnostic)> additionalDiagnostics, CompilerDiagnostics compilerDiagnostics, IVerifier verifier, CancellationToken cancellationToken) { if (analyzers.IsEmpty) { analyzers = ImmutableArray.Create(new EmptyDiagnosticAnalyzer()); } - var diagnostics = ImmutableArray.CreateBuilder(); + var diagnostics = ImmutableArray.CreateBuilder<(Project project, Diagnostic diagnostic)>(); foreach (var project in solution.Projects) { var compilation = await GetProjectCompilationAsync(project, verifier, cancellationToken).ConfigureAwait(false); var compilationWithAnalyzers = compilation.WithAnalyzers(analyzers, GetAnalyzerOptions(project), cancellationToken); var allDiagnostics = await compilationWithAnalyzers.GetAllDiagnosticsAsync().ConfigureAwait(false); - diagnostics.AddRange(allDiagnostics.Where(diagnostic => !IsCompilerDiagnostic(diagnostic) || IsCompilerDiagnosticIncluded(diagnostic, compilerDiagnostics))); + diagnostics.AddRange(allDiagnostics.Where(diagnostic => !IsCompilerDiagnostic(diagnostic) || IsCompilerDiagnosticIncluded(diagnostic, compilerDiagnostics)).Select(diagnostic => (project, diagnostic))); } diagnostics.AddRange(additionalDiagnostics); @@ -1310,7 +1335,14 @@ protected virtual Project ApplyCompilationOptions(Project project) return solution.GetProject(project.Id); } - public virtual AdhocWorkspace CreateWorkspace() + public Workspace CreateWorkspace() + { + var workspace = CreateWorkspaceImpl(); + _workspaces.Add(workspace); + return workspace; + } + + protected virtual Workspace CreateWorkspaceImpl() { var exportProvider = ExportProviderFactory.Value.CreateExportProvider(); var host = MefHostServices.Create(exportProvider.AsCompositionContext()); @@ -1327,14 +1359,14 @@ public virtual AdhocWorkspace CreateWorkspace() /// A collection of s to be sorted. /// A collection containing the input , sorted by /// and . - private static Diagnostic[] SortDistinctDiagnostics(IEnumerable diagnostics) + private static (Project project, Diagnostic diagnostic)[] SortDistinctDiagnostics(IEnumerable<(Project project, Diagnostic diagnostic)> diagnostics) { return diagnostics - .OrderBy(d => d.Location.GetLineSpan().Path, StringComparer.Ordinal) - .ThenBy(d => d.Location.SourceSpan.Start) - .ThenBy(d => d.Location.SourceSpan.End) - .ThenBy(d => d.Id) - .ThenBy(d => GetArguments(d), LexicographicComparer.Instance).ToArray(); + .OrderBy(d => d.diagnostic.Location.GetLineSpan().Path, StringComparer.Ordinal) + .ThenBy(d => d.diagnostic.Location.SourceSpan.Start) + .ThenBy(d => d.diagnostic.Location.SourceSpan.End) + .ThenBy(d => d.diagnostic.Id) + .ThenBy(d => GetArguments(d.diagnostic), LexicographicComparer.Instance).ToArray(); } private static IReadOnlyList GetArguments(Diagnostic diagnostic) diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/CodeActionTest`1.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/CodeActionTest`1.cs index 459395457..e3542db49 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/CodeActionTest`1.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/CodeActionTest`1.cs @@ -70,24 +70,62 @@ protected static bool CodeActionExpected(SolutionState state) || state.AdditionalFilesFactories.Any(); } - protected static bool HasAnyChange(SolutionState oldState, SolutionState newState) + protected static bool HasAnyChange(ProjectState oldState, ProjectState newState, bool recursive) { - return !oldState.Sources.SequenceEqual(newState.Sources, SourceFileEqualityComparer.Instance) + if (!oldState.Sources.SequenceEqual(newState.Sources, SourceFileEqualityComparer.Instance) || !oldState.GeneratedSources.SequenceEqual(newState.GeneratedSources, SourceFileEqualityComparer.Instance) || !oldState.AdditionalFiles.SequenceEqual(newState.AdditionalFiles, SourceFileEqualityComparer.Instance) - || !oldState.AnalyzerConfigFiles.SequenceEqual(newState.AnalyzerConfigFiles, SourceFileEqualityComparer.Instance); + || !oldState.AnalyzerConfigFiles.SequenceEqual(newState.AnalyzerConfigFiles, SourceFileEqualityComparer.Instance)) + { + return true; + } + + if (!recursive) + { + return false; + } + + if (oldState is SolutionState oldSolutionState) + { + if (!(newState is SolutionState newSolutionState)) + { + throw new ArgumentException("Unexpected mismatch of SolutionState with ProjectState."); + } + + if (oldSolutionState.AdditionalProjects.Count != newSolutionState.AdditionalProjects.Count) + { + return true; + } + + foreach (var oldAdditionalState in oldSolutionState.AdditionalProjects) + { + if (!newSolutionState.AdditionalProjects.TryGetValue(oldAdditionalState.Key, out var newAdditionalState) + || HasAnyChange(oldAdditionalState.Value, newAdditionalState, recursive: true)) + { + return true; + } + } + } + + return false; } - protected static CodeAction? TryGetCodeActionToApply(ImmutableArray actions, int? codeActionIndex, string? codeActionEquivalenceKey, Action? codeActionVerifier, IVerifier verifier) + protected static CodeAction? TryGetCodeActionToApply(int iteration, ImmutableArray actions, int? codeActionIndex, string? codeActionEquivalenceKey, Action? codeActionVerifier, IVerifier verifier) { CodeAction? result; if (codeActionIndex.HasValue && codeActionEquivalenceKey != null) { - if (actions.Length <= codeActionIndex) + var expectedAction = actions.FirstOrDefault(action => action.EquivalenceKey == codeActionEquivalenceKey); + if (expectedAction is null && iteration > 0) { + // No matching code action was found. This is acceptable if this is not the first iteration. return null; } + verifier.True( + actions.Length > codeActionIndex, + $"Expected to find a code action at index '{codeActionIndex}', but only '{actions.Length}' code actions were found."); + verifier.Equal( codeActionEquivalenceKey, actions[codeActionIndex.Value].EquivalenceKey, diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Extensions/IVerifierExtensions.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Extensions/IVerifierExtensions.cs index 0bffc225c..7e0f3d551 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Extensions/IVerifierExtensions.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Extensions/IVerifierExtensions.cs @@ -2,8 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Linq; using System.Text; using DiffPlex; +using DiffPlex.Chunkers; using DiffPlex.DiffBuilder; using DiffPlex.DiffBuilder.Model; @@ -14,6 +16,10 @@ namespace Microsoft.CodeAnalysis.Testing /// public static class IVerifierExtensions { + private static readonly IChunker s_lineChunker = new LineChunker(); + private static readonly IChunker s_lineEndingsPreservingChunker = new LineEndingsPreservingChunker(); + private static readonly InlineDiffBuilder s_diffBuilder = new InlineDiffBuilder(new Differ()); + /// /// Asserts that two strings are equal, and prints a diff between the two if they are not. /// @@ -27,14 +33,19 @@ public static void EqualOrDiff(this IVerifier verifier, string expected, string if (expected != actual) { - var diffBuilder = new InlineDiffBuilder(new Differ()); - var diff = diffBuilder.BuildDiffModel(expected, actual, ignoreWhitespace: false); + var diff = s_diffBuilder.BuildDiffModel(expected, actual, ignoreWhitespace: false, ignoreCase: false, s_lineChunker); var messageBuilder = new StringBuilder(); messageBuilder.AppendLine( string.IsNullOrEmpty(message) ? "Actual and expected values differ. Expected shown in baseline of diff:" : message); + if (!diff.Lines.Any(line => line.Type == ChangeType.Inserted || line.Type == ChangeType.Deleted)) + { + // We have a failure only caused by line ending differences; recalculate with line endings visible + diff = s_diffBuilder.BuildDiffModel(expected, actual, ignoreWhitespace: false, ignoreCase: false, s_lineEndingsPreservingChunker); + } + foreach (var line in diff.Lines) { switch (line.Type) @@ -50,7 +61,7 @@ public static void EqualOrDiff(this IVerifier verifier, string expected, string break; } - messageBuilder.AppendLine(line.Text); + messageBuilder.AppendLine(line.Text.Replace("\r", "").Replace("\n", "")); } verifier.Fail(messageBuilder.ToString()); diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.csproj b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.csproj index d108fdc13..91f8237e9 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.csproj +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.csproj @@ -38,6 +38,7 @@ + diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/ProjectCollection.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/ProjectCollection.cs index 07ee2603d..12b69c14e 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/ProjectCollection.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/ProjectCollection.cs @@ -35,7 +35,22 @@ public ProjectCollection(string defaultLanguage, string defaultExtension) { get { - var project = this.GetOrAdd(projectName, () => new ProjectState(projectName, _defaultLanguage, $"/{projectName}/Test", _defaultExtension)); + string extension; + if (language == _defaultLanguage) + { + extension = _defaultExtension; + } + else + { + extension = language switch + { + LanguageNames.CSharp => "cs", + LanguageNames.VisualBasic => "vb", + _ => throw new ArgumentOutOfRangeException(nameof(language)), + }; + } + + var project = this.GetOrAdd(projectName, () => new ProjectState(projectName, language, $"/{projectName}/Test", extension)); if (project.Language != language) { throw new InvalidOperationException(); diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/PublicAPI.Unshipped.txt b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/PublicAPI.Unshipped.txt index cd1037a9e..7c946dcb0 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/PublicAPI.Unshipped.txt +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/PublicAPI.Unshipped.txt @@ -3,12 +3,13 @@ Microsoft.CodeAnalysis.Testing.AnalyzerTest.AnalyzerTest() -> void Microsoft.CodeAnalysis.Testing.AnalyzerTest.CompilerDiagnostics.get -> Microsoft.CodeAnalysis.Testing.CompilerDiagnostics Microsoft.CodeAnalysis.Testing.AnalyzerTest.CompilerDiagnostics.set -> void Microsoft.CodeAnalysis.Testing.AnalyzerTest.CreateProjectAsync(Microsoft.CodeAnalysis.Testing.Model.EvaluatedProjectState primaryProject, System.Collections.Immutable.ImmutableArray additionalProjects, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +Microsoft.CodeAnalysis.Testing.AnalyzerTest.CreateWorkspace() -> Microsoft.CodeAnalysis.Workspace Microsoft.CodeAnalysis.Testing.AnalyzerTest.DiagnosticVerifier.get -> System.Action Microsoft.CodeAnalysis.Testing.AnalyzerTest.DiagnosticVerifier.set -> void Microsoft.CodeAnalysis.Testing.AnalyzerTest.DisabledDiagnostics.get -> System.Collections.Generic.List Microsoft.CodeAnalysis.Testing.AnalyzerTest.ExpectedDiagnostics.get -> System.Collections.Generic.List Microsoft.CodeAnalysis.Testing.AnalyzerTest.FormatVerifierMessage(System.Collections.Immutable.ImmutableArray analyzers, Microsoft.CodeAnalysis.Diagnostic actual, Microsoft.CodeAnalysis.Testing.DiagnosticResult expected, string message) -> string -Microsoft.CodeAnalysis.Testing.AnalyzerTest.GetSortedDiagnosticsAsync(Microsoft.CodeAnalysis.Solution solution, System.Collections.Immutable.ImmutableArray analyzers, System.Collections.Immutable.ImmutableArray additionalDiagnostics, Microsoft.CodeAnalysis.Testing.CompilerDiagnostics compilerDiagnostics, Microsoft.CodeAnalysis.Testing.IVerifier verifier, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> +Microsoft.CodeAnalysis.Testing.AnalyzerTest.GetSortedDiagnosticsAsync(Microsoft.CodeAnalysis.Solution solution, System.Collections.Immutable.ImmutableArray analyzers, System.Collections.Immutable.ImmutableArray<(Microsoft.CodeAnalysis.Project project, Microsoft.CodeAnalysis.Diagnostic diagnostic)> additionalDiagnostics, Microsoft.CodeAnalysis.Testing.CompilerDiagnostics compilerDiagnostics, Microsoft.CodeAnalysis.Testing.IVerifier verifier, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task> Microsoft.CodeAnalysis.Testing.AnalyzerTest.MarkupOptions.get -> Microsoft.CodeAnalysis.Testing.MarkupOptions Microsoft.CodeAnalysis.Testing.AnalyzerTest.MarkupOptions.set -> void Microsoft.CodeAnalysis.Testing.AnalyzerTest.MatchDiagnosticsTimeout.get -> System.TimeSpan @@ -16,6 +17,7 @@ Microsoft.CodeAnalysis.Testing.AnalyzerTest.MatchDiagnosticsTimeout.s Microsoft.CodeAnalysis.Testing.AnalyzerTest.OptionsTransforms.get -> System.Collections.Generic.List> Microsoft.CodeAnalysis.Testing.AnalyzerTest.ReferenceAssemblies.get -> Microsoft.CodeAnalysis.Testing.ReferenceAssemblies Microsoft.CodeAnalysis.Testing.AnalyzerTest.ReferenceAssemblies.set -> void +Microsoft.CodeAnalysis.Testing.AnalyzerTest.RunAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task Microsoft.CodeAnalysis.Testing.AnalyzerTest.SolutionTransforms.get -> System.Collections.Generic.List> Microsoft.CodeAnalysis.Testing.AnalyzerTest.TestBehaviors.get -> Microsoft.CodeAnalysis.Testing.TestBehaviors Microsoft.CodeAnalysis.Testing.AnalyzerTest.TestBehaviors.set -> void @@ -243,13 +245,14 @@ static Microsoft.CodeAnalysis.Testing.AnalyzerVerifier.Diagnostic(string diagnosticId) -> Microsoft.CodeAnalysis.Testing.DiagnosticResult static Microsoft.CodeAnalysis.Testing.AnalyzerVerifier.VerifyAnalyzerAsync(string source, params Microsoft.CodeAnalysis.Testing.DiagnosticResult[] expected) -> System.Threading.Tasks.Task static Microsoft.CodeAnalysis.Testing.CodeActionTest.CodeActionExpected(Microsoft.CodeAnalysis.Testing.SolutionState state) -> bool -static Microsoft.CodeAnalysis.Testing.CodeActionTest.HasAnyChange(Microsoft.CodeAnalysis.Testing.SolutionState oldState, Microsoft.CodeAnalysis.Testing.SolutionState newState) -> bool -static Microsoft.CodeAnalysis.Testing.CodeActionTest.TryGetCodeActionToApply(System.Collections.Immutable.ImmutableArray actions, int? codeActionIndex, string codeActionEquivalenceKey, System.Action codeActionVerifier, Microsoft.CodeAnalysis.Testing.IVerifier verifier) -> Microsoft.CodeAnalysis.CodeActions.CodeAction +static Microsoft.CodeAnalysis.Testing.CodeActionTest.HasAnyChange(Microsoft.CodeAnalysis.Testing.ProjectState oldState, Microsoft.CodeAnalysis.Testing.ProjectState newState, bool recursive) -> bool +static Microsoft.CodeAnalysis.Testing.CodeActionTest.TryGetCodeActionToApply(int iteration, System.Collections.Immutable.ImmutableArray actions, int? codeActionIndex, string codeActionEquivalenceKey, System.Action codeActionVerifier, Microsoft.CodeAnalysis.Testing.IVerifier verifier) -> Microsoft.CodeAnalysis.CodeActions.CodeAction static Microsoft.CodeAnalysis.Testing.DiagnosticResult.CompilerError(string identifier) -> Microsoft.CodeAnalysis.Testing.DiagnosticResult static Microsoft.CodeAnalysis.Testing.DiagnosticResult.CompilerWarning(string identifier) -> Microsoft.CodeAnalysis.Testing.DiagnosticResult static Microsoft.CodeAnalysis.Testing.IVerifierExtensions.EqualOrDiff(this Microsoft.CodeAnalysis.Testing.IVerifier verifier, string expected, string actual, string message = null) -> void static Microsoft.CodeAnalysis.Testing.ReferenceAssemblies.Default.get -> Microsoft.CodeAnalysis.Testing.ReferenceAssemblies static Microsoft.CodeAnalysis.Testing.ReferenceAssemblies.Net.Net50.get -> Microsoft.CodeAnalysis.Testing.ReferenceAssemblies +static Microsoft.CodeAnalysis.Testing.ReferenceAssemblies.Net.Net60.get -> Microsoft.CodeAnalysis.Testing.ReferenceAssemblies static Microsoft.CodeAnalysis.Testing.ReferenceAssemblies.NetCore.NetCoreApp10.get -> Microsoft.CodeAnalysis.Testing.ReferenceAssemblies static Microsoft.CodeAnalysis.Testing.ReferenceAssemblies.NetCore.NetCoreApp11.get -> Microsoft.CodeAnalysis.Testing.ReferenceAssemblies static Microsoft.CodeAnalysis.Testing.ReferenceAssemblies.NetCore.NetCoreApp20.get -> Microsoft.CodeAnalysis.Testing.ReferenceAssemblies @@ -320,7 +323,7 @@ static readonly Microsoft.CodeAnalysis.Testing.DiagnosticResult.EmptyDiagnosticR virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.ApplyCompilationOptions(Microsoft.CodeAnalysis.Project project) -> Microsoft.CodeAnalysis.Project virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.CreateProjectImplAsync(Microsoft.CodeAnalysis.Testing.Model.EvaluatedProjectState primaryProject, System.Collections.Immutable.ImmutableArray additionalProjects, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.CreateSolutionAsync(Microsoft.CodeAnalysis.ProjectId projectId, Microsoft.CodeAnalysis.Testing.Model.EvaluatedProjectState projectState, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task -virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.CreateWorkspace() -> Microsoft.CodeAnalysis.AdhocWorkspace +virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.CreateWorkspaceImpl() -> Microsoft.CodeAnalysis.Workspace virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.DefaultFilePath.get -> string virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.DefaultFilePathPrefix.get -> string virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.DefaultTestProjectName.get -> string @@ -328,7 +331,7 @@ virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.GetAnalyzerOption virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.GetDefaultDiagnostic(Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer[] analyzers) -> Microsoft.CodeAnalysis.DiagnosticDescriptor virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.GetProjectCompilationAsync(Microsoft.CodeAnalysis.Project project, Microsoft.CodeAnalysis.Testing.IVerifier verifier, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.IsCompilerDiagnosticIncluded(Microsoft.CodeAnalysis.Diagnostic diagnostic, Microsoft.CodeAnalysis.Testing.CompilerDiagnostics compilerDiagnostics) -> bool -virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.RunAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +virtual Microsoft.CodeAnalysis.Testing.AnalyzerTest.RunImplAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task virtual Microsoft.CodeAnalysis.Testing.CodeActionTest.FilterCodeActions(System.Collections.Immutable.ImmutableArray actions) -> System.Collections.Immutable.ImmutableArray virtual Microsoft.CodeAnalysis.Testing.DefaultVerifier.CreateMessage(string message) -> string virtual Microsoft.CodeAnalysis.Testing.DefaultVerifier.Empty(string collectionName, System.Collections.Generic.IEnumerable collection) -> void diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/ReferenceAssemblies.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/ReferenceAssemblies.cs index 8e085d213..2ab950247 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/ReferenceAssemblies.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/ReferenceAssemblies.cs @@ -865,7 +865,26 @@ public static class Net Path.Combine("ref", "net5.0")); }); + private static readonly Lazy _lazyNet60 = + new Lazy(() => + { + if (!NuGetFramework.Parse("net6.0").IsPackageBased) + { + // The NuGet version provided at runtime does not recognize the 'net6.0' target framework + throw new NotSupportedException("The 'net6.0' target framework is not supported by this version of NuGet."); + } + + return new ReferenceAssemblies( + "net6.0", + new PackageIdentity( + "Microsoft.NETCore.App.Ref", + "6.0.0-preview.6.21352.12"), + Path.Combine("ref", "net6.0")); + }); + public static ReferenceAssemblies Net50 => _lazyNet50.Value; + + public static ReferenceAssemblies Net60 => _lazyNet60.Value; } public static class NetStandard diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/CodeFixTest`1.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/CodeFixTest`1.cs index 0ee162819..0bc34d839 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/CodeFixTest`1.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/CodeFixTest`1.cs @@ -122,7 +122,8 @@ public string BatchFixedCode /// /// /// If the expected Fix All output equals the input sources, the default value is treated as 0. - /// Otherwise, the default value is treated as 1. + /// If all projects in the solution have the same , the default value is treated as 1. + /// Otherwise, the default value is treated as new negative of the number of languages represented by projects in the solution. /// /// /// @@ -158,6 +159,31 @@ public string BatchFixedCode /// public int? NumberOfFixAllInDocumentIterations { get; set; } + /// + /// Gets or sets the number of code fix iterations expected during code fix testing for Fix All in Project + /// scenarios. + /// + /// + /// See the property for an overview of the behavior of this + /// property. If the number of Fix All in Project iterations is not specified, the value is automatically + /// selected according to the current test configuration: + /// + /// + /// If a value has been explicitly provided for , the value is used as-is. + /// If the expected Fix All output equals the input sources, the default value is treated as 0. + /// Otherwise, the default value is treated as the negative of the number of distinct projects containing fixable diagnostics (typically -1). + /// + /// + /// + /// The default value for this property can be interpreted as "Fix All in Project operations are expected + /// to complete after at most one operation for each fixable project in the input source has been applied. + /// Completing in fewer iterations is acceptable." + /// + /// + /// + /// + public int? NumberOfFixAllInProjectIterations { get; set; } + /// /// Gets or sets the code fix test behaviors applying to this test. The default value is /// . @@ -216,7 +242,7 @@ bool CodeFixProvidersHandleDiagnostic(Diagnostic localDiagnostic) } } - public override async Task RunAsync(CancellationToken cancellationToken = default) + protected override async Task RunImplAsync(CancellationToken cancellationToken) { Verify.NotEmpty($"{nameof(TestState)}.{nameof(SolutionState.Sources)}", TestState.Sources); @@ -266,8 +292,12 @@ private bool CodeFixExpected() /// A representing the asynchronous operation. protected async Task VerifyFixAsync(SolutionState testState, SolutionState fixedState, SolutionState batchFixedState, IVerifier verifier, CancellationToken cancellationToken) { + var fixers = GetCodeFixProviders().ToImmutableArray(); + var fixableDiagnostics = testState.ExpectedDiagnostics.Where(diagnostic => fixers.Any(fixer => fixer.FixableDiagnosticIds.Contains(diagnostic.Id))).ToImmutableArray(); + int numberOfIncrementalIterations; int numberOfFixAllIterations; + int numberOfFixAllInProjectIterations; int numberOfFixAllInDocumentIterations; if (NumberOfIncrementalIterations != null) { @@ -275,16 +305,14 @@ protected async Task VerifyFixAsync(SolutionState testState, SolutionState fixed } else { - if (!HasAnyChange(testState, fixedState)) + if (!HasAnyChange(testState, fixedState, recursive: true)) { numberOfIncrementalIterations = 0; } else { // Expect at most one iteration per fixable diagnostic - var fixers = GetCodeFixProviders().ToArray(); - var fixableExpectedDiagnostics = testState.ExpectedDiagnostics.Count(diagnostic => fixers.Any(fixer => fixer.FixableDiagnosticIds.Contains(diagnostic.Id))); - numberOfIncrementalIterations = -fixableExpectedDiagnostics; + numberOfIncrementalIterations = -fixableDiagnostics.Count(); } } @@ -294,13 +322,44 @@ protected async Task VerifyFixAsync(SolutionState testState, SolutionState fixed } else { - if (!HasAnyChange(testState, batchFixedState)) + if (!HasAnyChange(testState, batchFixedState, recursive: true)) { numberOfFixAllIterations = 0; } else { - numberOfFixAllIterations = 1; + // Expect at most one iteration per language with fixable diagnostics. Since we can't tell the + // language from ExpectedDiagnostic, use a conservative value from the number of project languages + // present. + numberOfFixAllIterations = -Enumerable.Repeat(testState.Language, 1).Concat(testState.AdditionalProjects.Select(p => p.Value.Language)).Distinct().Count(); + } + } + + if (NumberOfFixAllInProjectIterations != null) + { + numberOfFixAllInProjectIterations = NumberOfFixAllInProjectIterations.Value; + } + else if (NumberOfFixAllIterations != null) + { + numberOfFixAllInProjectIterations = NumberOfFixAllIterations.Value; + } + else + { + numberOfFixAllInProjectIterations = 0; + if (HasAnyChange(testState, batchFixedState, recursive: false)) + { + // Expect at most one iteration for a fixable primary project + numberOfFixAllInProjectIterations--; + } + + foreach (var (name, state) in testState.AdditionalProjects) + { + if (!batchFixedState.AdditionalProjects.TryGetValue(name, out var expected) + || HasAnyChange(state, expected, recursive: true)) + { + // Expect at most one iteration for each fixable additional project + numberOfFixAllInProjectIterations--; + } } } @@ -314,15 +373,13 @@ protected async Task VerifyFixAsync(SolutionState testState, SolutionState fixed } else { - if (!HasAnyChange(testState, batchFixedState)) + if (!HasAnyChange(testState, batchFixedState, recursive: false)) { numberOfFixAllInDocumentIterations = 0; } else { // Expect at most one iteration per fixable document - var fixers = GetCodeFixProviders().ToArray(); - var fixableDiagnostics = testState.ExpectedDiagnostics.Where(diagnostic => fixers.Any(fixer => fixer.FixableDiagnosticIds.Contains(diagnostic.Id))); numberOfFixAllInDocumentIterations = -fixableDiagnostics.GroupBy(diagnostic => diagnostic.Spans.FirstOrDefault().Span.Path).Count(); } } @@ -352,7 +409,7 @@ protected async Task VerifyFixAsync(SolutionState testState, SolutionState fixed var t3 = CodeFixTestBehaviors.HasFlag(CodeFixTestBehaviors.SkipFixAllInProjectCheck) ? ((Task)Task.FromResult(true)).ConfigureAwait(false) - : VerifyFixAsync(Language, GetDiagnosticAnalyzers().ToImmutableArray(), GetCodeFixProviders().ToImmutableArray(), testState, batchFixedState, numberOfFixAllIterations, FixAllAnalyzerDiagnosticsInProjectAsync, verifier.PushContext("Fix all in project"), cancellationToken).ConfigureAwait(false); + : VerifyFixAsync(Language, GetDiagnosticAnalyzers().ToImmutableArray(), GetCodeFixProviders().ToImmutableArray(), testState, batchFixedState, numberOfFixAllInProjectIterations, FixAllAnalyzerDiagnosticsInProjectAsync, verifier.PushContext("Fix all in project"), cancellationToken).ConfigureAwait(false); if (Debugger.IsAttached) { await t3; @@ -404,6 +461,21 @@ private async Task VerifyFixAsync( ExceptionDispatchInfo? iterationCountFailure; (project, iterationCountFailure) = await getFixedProject(analyzers, codeFixProviders, CodeActionIndex, CodeActionEquivalenceKey, CodeActionVerifier, project, numberOfIterations, verifier, cancellationToken).ConfigureAwait(false); + // After applying all of the code fixes, compare the resulting string to the inputted one + await VerifyProjectAsync(newState, project, verifier, cancellationToken).ConfigureAwait(false); + + foreach (var additionalProject in newState.AdditionalProjects) + { + var actualProject = project.Solution.Projects.Single(p => p.Name == additionalProject.Key); + await VerifyProjectAsync(additionalProject.Value, actualProject, verifier, cancellationToken); + } + + // Validate the iteration counts after validating the content + iterationCountFailure?.Throw(); + } + + private async Task VerifyProjectAsync(ProjectState newState, Project project, IVerifier verifier, CancellationToken cancellationToken) + { // After applying all of the code fixes, compare the resulting string to the inputted one var updatedDocuments = project.Documents.ToArray(); @@ -443,25 +515,32 @@ private async Task VerifyFixAsync( verifier.Equal(newState.AnalyzerConfigFiles[i].content.ChecksumAlgorithm, actual.ChecksumAlgorithm, $"checksum algorithm of '{newState.AnalyzerConfigFiles[i].filename}' was expected to be '{newState.AnalyzerConfigFiles[i].content.ChecksumAlgorithm}' but was '{actual.ChecksumAlgorithm}'"); verifier.Equal(newState.AnalyzerConfigFiles[i].filename, updatedAnalyzerConfigDocuments[i].Name, $"file name was expected to be '{newState.AnalyzerConfigFiles[i].filename}' but was '{updatedAnalyzerConfigDocuments[i].Name}'"); } - - // Validate the iteration counts after validating the content - iterationCountFailure?.Throw(); } private async Task<(Project project, ExceptionDispatchInfo? iterationCountFailure)> FixEachAnalyzerDiagnosticAsync(ImmutableArray analyzers, ImmutableArray codeFixProviders, int? codeFixIndex, string? codeFixEquivalenceKey, Action? codeActionVerifier, Project project, int numberOfIterations, IVerifier verifier, CancellationToken cancellationToken) { + if (numberOfIterations == -1) + { + // For better error messages, use '==' instead of '<=' for iteration comparison when the right hand + // side is 1. + numberOfIterations = 1; + } + var expectedNumberOfIterations = numberOfIterations; if (numberOfIterations < 0) { numberOfIterations = -numberOfIterations; } - var previousDiagnostics = ImmutableArray.Create(); + var previousDiagnostics = ImmutableArray.Create<(Project project, Diagnostic diagnostic)>(); + var currentIteration = -1; bool done; do { - var analyzerDiagnostics = await GetSortedDiagnosticsAsync(project.Solution, analyzers, additionalDiagnostics: ImmutableArray.Empty, CompilerDiagnostics, verifier, cancellationToken).ConfigureAwait(false); + currentIteration++; + + var analyzerDiagnostics = await GetSortedDiagnosticsAsync(project.Solution, analyzers, additionalDiagnostics: ImmutableArray<(Project project, Diagnostic diagnostic)>.Empty, CompilerDiagnostics, verifier, cancellationToken).ConfigureAwait(false); if (analyzerDiagnostics.Length == 0) { break; @@ -484,22 +563,23 @@ private async Task VerifyFixAsync( previousDiagnostics = analyzerDiagnostics; var fixableDiagnostics = analyzerDiagnostics - .Where(diagnostic => codeFixProviders.Any(provider => provider.FixableDiagnosticIds.Contains(diagnostic.Id))) - .Where(diagnostic => project.GetDocument(diagnostic.Location.SourceTree) is object) + .Where(diagnostic => codeFixProviders.Any(provider => provider.FixableDiagnosticIds.Contains(diagnostic.diagnostic.Id))) + .Where(diagnostic => project.Solution.GetDocument(diagnostic.diagnostic.Location.SourceTree) is object) .ToImmutableArray(); if (CodeFixTestBehaviors.HasFlag(CodeFixTestBehaviors.FixOne)) { - var diagnosticToFix = TrySelectDiagnosticToFix(fixableDiagnostics); - fixableDiagnostics = diagnosticToFix is object ? ImmutableArray.Create(diagnosticToFix) : ImmutableArray.Empty; + var diagnosticToFix = TrySelectDiagnosticToFix(fixableDiagnostics.Select(x => x.diagnostic).ToImmutableArray()); + fixableDiagnostics = diagnosticToFix is object ? ImmutableArray.Create(fixableDiagnostics.Single(x => x.diagnostic == diagnosticToFix)) : ImmutableArray<(Project project, Diagnostic diagnostic)>.Empty; } done = true; var anyActions = false; - foreach (var diagnostic in fixableDiagnostics) + foreach (var (_, diagnostic) in fixableDiagnostics) { var actions = ImmutableArray.CreateBuilder(); + var fixableDocument = project.Solution.GetDocument(diagnostic.Location.SourceTree); foreach (var codeFixProvider in codeFixProviders) { if (!codeFixProvider.FixableDiagnosticIds.Contains(diagnostic.Id)) @@ -508,21 +588,22 @@ private async Task VerifyFixAsync( continue; } - var context = new CodeFixContext(project.GetDocument(diagnostic.Location.SourceTree), diagnostic, (a, d) => actions.Add(a), cancellationToken); + var context = new CodeFixContext(fixableDocument, diagnostic, (a, d) => actions.Add(a), cancellationToken); await codeFixProvider.RegisterCodeFixesAsync(context).ConfigureAwait(false); } var filteredActions = FilterCodeActions(actions.ToImmutable()); - var actionToApply = TryGetCodeActionToApply(filteredActions, codeFixIndex, codeFixEquivalenceKey, codeActionVerifier, verifier); + var actionToApply = TryGetCodeActionToApply(currentIteration, filteredActions, codeFixIndex, codeFixEquivalenceKey, codeActionVerifier, verifier); if (actionToApply != null) { anyActions = true; - var fixedProject = await ApplyCodeActionAsync(project, actionToApply, verifier, cancellationToken).ConfigureAwait(false); - if (fixedProject != project) + var originalProjectId = project.Id; + var fixedProject = await ApplyCodeActionAsync(fixableDocument.Project, actionToApply, verifier, cancellationToken).ConfigureAwait(false); + if (fixedProject != fixableDocument.Project) { done = false; - project = fixedProject; + project = fixedProject.Solution.GetProject(originalProjectId); break; } } @@ -579,18 +660,28 @@ private async Task VerifyFixAsync( private async Task<(Project project, ExceptionDispatchInfo? iterationCountFailure)> FixAllAnalyerDiagnosticsInScopeAsync(FixAllScope scope, ImmutableArray analyzers, ImmutableArray codeFixProviders, int? codeFixIndex, string? codeFixEquivalenceKey, Action? codeActionVerifier, Project project, int numberOfIterations, IVerifier verifier, CancellationToken cancellationToken) { + if (numberOfIterations == -1) + { + // For better error messages, use '==' instead of '<=' for iteration comparison when the right hand + // side is 1. + numberOfIterations = 1; + } + var expectedNumberOfIterations = numberOfIterations; if (numberOfIterations < 0) { numberOfIterations = -numberOfIterations; } - var previousDiagnostics = ImmutableArray.Create(); + var previousDiagnostics = ImmutableArray.Create<(Project project, Diagnostic diagnostic)>(); + var currentIteration = -1; bool done; do { - var analyzerDiagnostics = await GetSortedDiagnosticsAsync(project.Solution, analyzers, additionalDiagnostics: ImmutableArray.Empty, CompilerDiagnostics, verifier, cancellationToken).ConfigureAwait(false); + currentIteration++; + + var analyzerDiagnostics = await GetSortedDiagnosticsAsync(project.Solution, analyzers, additionalDiagnostics: ImmutableArray<(Project project, Diagnostic diagnostic)>.Empty, CompilerDiagnostics, verifier, cancellationToken).ConfigureAwait(false); if (analyzerDiagnostics.Length == 0) { break; @@ -610,29 +701,40 @@ private async Task VerifyFixAsync( return (project, ExceptionDispatchInfo.Capture(ex)); } + var fixableDiagnostics = analyzerDiagnostics + .Where(diagnostic => codeFixProviders.Any(provider => provider.FixableDiagnosticIds.Contains(diagnostic.diagnostic.Id))) + .Where(diagnostic => project.Solution.GetDocument(diagnostic.diagnostic.Location.SourceTree) is object) + .ToImmutableArray(); + + if (CodeFixTestBehaviors.HasFlag(CodeFixTestBehaviors.FixOne)) + { + var diagnosticToFix = TrySelectDiagnosticToFix(fixableDiagnostics.Select(x => x.diagnostic).ToImmutableArray()); + fixableDiagnostics = diagnosticToFix is object ? ImmutableArray.Create(fixableDiagnostics.Single(x => x.diagnostic == diagnosticToFix)) : ImmutableArray<(Project project, Diagnostic diagnostic)>.Empty; + } + Diagnostic? firstDiagnostic = null; CodeFixProvider? effectiveCodeFixProvider = null; string? equivalenceKey = null; - foreach (var diagnostic in analyzerDiagnostics) + foreach (var (_, diagnostic) in fixableDiagnostics) { var actions = new List<(CodeAction, CodeFixProvider)>(); + var diagnosticDocument = project.Solution.GetDocument(diagnostic.Location.SourceTree); foreach (var codeFixProvider in codeFixProviders) { - if (!codeFixProvider.FixableDiagnosticIds.Contains(diagnostic.Id) - || !(project.GetDocument(diagnostic.Location.SourceTree) is { } document)) + if (!codeFixProvider.FixableDiagnosticIds.Contains(diagnostic.Id)) { // do not pass unsupported diagnostics to a code fix provider continue; } var actionsBuilder = ImmutableArray.CreateBuilder(); - var context = new CodeFixContext(document, diagnostic, (a, d) => actionsBuilder.Add(a), cancellationToken); + var context = new CodeFixContext(diagnosticDocument, diagnostic, (a, d) => actionsBuilder.Add(a), cancellationToken); await codeFixProvider.RegisterCodeFixesAsync(context).ConfigureAwait(false); actions.AddRange(FilterCodeActions(actionsBuilder.ToImmutable()).Select(action => (action, codeFixProvider))); } - var actionToApply = TryGetCodeActionToApply(actions.Select(a => a.Item1).ToImmutableArray(), codeFixIndex, codeFixEquivalenceKey, codeActionVerifier, verifier); + var actionToApply = TryGetCodeActionToApply(currentIteration, actions.Select(a => a.Item1).ToImmutableArray(), codeFixIndex, codeFixEquivalenceKey, codeActionVerifier, verifier); if (actionToApply != null) { firstDiagnostic = diagnostic; @@ -655,11 +757,12 @@ private async Task VerifyFixAsync( FixAllContext.DiagnosticProvider fixAllDiagnosticProvider = TestDiagnosticProvider.Create(analyzerDiagnostics); + var fixableDocument = project.Solution.GetDocument(firstDiagnostic.Location.SourceTree); var analyzerDiagnosticIds = analyzers.SelectMany(x => x.SupportedDiagnostics).Select(x => x.Id); var compilerDiagnosticIds = codeFixProviders.SelectMany(codeFixProvider => codeFixProvider.FixableDiagnosticIds).Where(x => x.StartsWith("CS", StringComparison.Ordinal) || x.StartsWith("BC", StringComparison.Ordinal)); var disabledDiagnosticIds = project.CompilationOptions.SpecificDiagnosticOptions.Where(x => x.Value == ReportDiagnostic.Suppress).Select(x => x.Key); var relevantIds = analyzerDiagnosticIds.Concat(compilerDiagnosticIds).Except(disabledDiagnosticIds).Distinct(); - var fixAllContext = new FixAllContext(project.GetDocument(firstDiagnostic.Location.SourceTree), effectiveCodeFixProvider, scope, equivalenceKey, relevantIds, fixAllDiagnosticProvider, cancellationToken); + var fixAllContext = new FixAllContext(fixableDocument, effectiveCodeFixProvider, scope, equivalenceKey, relevantIds, fixAllDiagnosticProvider, cancellationToken); var action = await fixAllProvider.GetFixAsync(fixAllContext).ConfigureAwait(false); if (action == null) @@ -667,11 +770,12 @@ private async Task VerifyFixAsync( return (project, null); } - var fixedProject = await ApplyCodeActionAsync(project, action, verifier, cancellationToken).ConfigureAwait(false); - if (fixedProject != project) + var originalProjectId = project.Id; + var fixedProject = await ApplyCodeActionAsync(fixableDocument.Project, action, verifier, cancellationToken).ConfigureAwait(false); + if (fixedProject != fixableDocument.Project) { done = false; - project = fixedProject; + project = fixedProject.Solution.GetProject(originalProjectId); } if (CodeFixTestBehaviors.HasFlag(CodeFixTestBehaviors.FixOne)) @@ -732,7 +836,7 @@ private static async Task GetSourceTextFromDocumentAsync(Document do return await formatted.GetTextAsync(cancellationToken).ConfigureAwait(false); } - private static bool AreDiagnosticsDifferent(ImmutableArray analyzerDiagnostics, ImmutableArray previousDiagnostics) + private static bool AreDiagnosticsDifferent(ImmutableArray<(Project project, Diagnostic diagnostic)> analyzerDiagnostics, ImmutableArray<(Project project, Diagnostic diagnostic)> previousDiagnostics) { if (analyzerDiagnostics.Length != previousDiagnostics.Length) { @@ -741,8 +845,9 @@ private static bool AreDiagnosticsDifferent(ImmutableArray analyzerD for (var i = 0; i < analyzerDiagnostics.Length; i++) { - if ((analyzerDiagnostics[i].Id != previousDiagnostics[i].Id) - || (analyzerDiagnostics[i].Location.SourceSpan != previousDiagnostics[i].Location.SourceSpan)) + if ((analyzerDiagnostics[i].project.Id != previousDiagnostics[i].project.Id) + || (analyzerDiagnostics[i].diagnostic.Id != previousDiagnostics[i].diagnostic.Id) + || (analyzerDiagnostics[i].diagnostic.Location.SourceSpan != previousDiagnostics[i].diagnostic.Location.SourceSpan)) { return true; } diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/PublicAPI.Unshipped.txt b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/PublicAPI.Unshipped.txt index 6ec73c7a2..1552a9842 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/PublicAPI.Unshipped.txt +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/PublicAPI.Unshipped.txt @@ -14,6 +14,8 @@ Microsoft.CodeAnalysis.Testing.CodeFixTest.FixedCode.set -> void Microsoft.CodeAnalysis.Testing.CodeFixTest.FixedState.get -> Microsoft.CodeAnalysis.Testing.SolutionState Microsoft.CodeAnalysis.Testing.CodeFixTest.NumberOfFixAllInDocumentIterations.get -> int? Microsoft.CodeAnalysis.Testing.CodeFixTest.NumberOfFixAllInDocumentIterations.set -> void +Microsoft.CodeAnalysis.Testing.CodeFixTest.NumberOfFixAllInProjectIterations.get -> int? +Microsoft.CodeAnalysis.Testing.CodeFixTest.NumberOfFixAllInProjectIterations.set -> void Microsoft.CodeAnalysis.Testing.CodeFixTest.NumberOfFixAllIterations.get -> int? Microsoft.CodeAnalysis.Testing.CodeFixTest.NumberOfFixAllIterations.set -> void Microsoft.CodeAnalysis.Testing.CodeFixTest.NumberOfIncrementalIterations.get -> int? @@ -36,7 +38,7 @@ Microsoft.CodeAnalysis.Testing.EmptyCodeFixProvider Microsoft.CodeAnalysis.Testing.EmptyCodeFixProvider.EmptyCodeFixProvider() -> void abstract Microsoft.CodeAnalysis.Testing.CodeFixTest.GetCodeFixProviders() -> System.Collections.Generic.IEnumerable override Microsoft.CodeAnalysis.Testing.CodeFixTest.IsCompilerDiagnosticIncluded(Microsoft.CodeAnalysis.Diagnostic diagnostic, Microsoft.CodeAnalysis.Testing.CompilerDiagnostics compilerDiagnostics) -> bool -override Microsoft.CodeAnalysis.Testing.CodeFixTest.RunAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +override Microsoft.CodeAnalysis.Testing.CodeFixTest.RunImplAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task override Microsoft.CodeAnalysis.Testing.EmptyCodeFixProvider.FixableDiagnosticIds.get -> System.Collections.Immutable.ImmutableArray override Microsoft.CodeAnalysis.Testing.EmptyCodeFixProvider.RegisterCodeFixesAsync(Microsoft.CodeAnalysis.CodeFixes.CodeFixContext context) -> System.Threading.Tasks.Task static Microsoft.CodeAnalysis.Testing.CodeFixVerifier.Diagnostic() -> Microsoft.CodeAnalysis.Testing.DiagnosticResult diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/TestDiagnosticProvider.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/TestDiagnosticProvider.cs index fb63c4122..07940362d 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/TestDiagnosticProvider.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing/TestDiagnosticProvider.cs @@ -13,22 +13,22 @@ namespace Microsoft.CodeAnalysis.Testing { internal sealed class TestDiagnosticProvider : FixAllContext.DiagnosticProvider { - private readonly ImmutableArray _diagnostics; + private readonly ImmutableArray<(Project project, Diagnostic diagnostic)> _diagnostics; - private TestDiagnosticProvider(ImmutableArray diagnostics) + private TestDiagnosticProvider(ImmutableArray<(Project project, Diagnostic diagnostic)> diagnostics) { _diagnostics = diagnostics; } public override Task> GetAllDiagnosticsAsync(Project project, CancellationToken cancellationToken) - => Task.FromResult>(_diagnostics); + => Task.FromResult>(_diagnostics.Where(diagnostic => diagnostic.project.Id == project.Id).Select(diagnostic => diagnostic.diagnostic)); public override Task> GetDocumentDiagnosticsAsync(Document document, CancellationToken cancellationToken) - => Task.FromResult(_diagnostics.Where(i => i.Location.GetLineSpan().Path == document.Name)); + => Task.FromResult(_diagnostics.Where(i => i.diagnostic.Location.GetLineSpan().Path == document.Name).Where(diagnostic => diagnostic.project.Id == document.Project.Id).Select(diagnostic => diagnostic.diagnostic)); public override Task> GetProjectDiagnosticsAsync(Project project, CancellationToken cancellationToken) - => Task.FromResult(_diagnostics.Where(i => !i.Location.IsInSource)); + => Task.FromResult(_diagnostics.Where(i => !i.diagnostic.Location.IsInSource).Where(diagnostic => diagnostic.project.Id == project.Id).Select(diagnostic => diagnostic.diagnostic)); - internal static TestDiagnosticProvider Create(ImmutableArray diagnostics) => new TestDiagnosticProvider(diagnostics); + internal static TestDiagnosticProvider Create(ImmutableArray<(Project project, Diagnostic diagnostic)> diagnostics) => new TestDiagnosticProvider(diagnostics); } } diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeRefactoring.Testing/CodeRefactoringTest`1.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeRefactoring.Testing/CodeRefactoringTest`1.cs index 441845d59..4aa1ca2bb 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeRefactoring.Testing/CodeRefactoringTest`1.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeRefactoring.Testing/CodeRefactoringTest`1.cs @@ -64,7 +64,7 @@ protected override IEnumerable GetDiagnosticAnalyzers() /// The to be used. protected abstract IEnumerable GetCodeRefactoringProviders(); - public override async Task RunAsync(CancellationToken cancellationToken = default) + protected override async Task RunImplAsync(CancellationToken cancellationToken) { Verify.NotEmpty($"{nameof(TestState)}.{nameof(SolutionState.Sources)}", TestState.Sources); @@ -114,7 +114,7 @@ private bool CodeActionExpected() return CodeActionExpected(FixedState); } - protected override DiagnosticDescriptor? GetDefaultDiagnostic(DiagnosticAnalyzer[] analyzers) + protected internal override DiagnosticDescriptor? GetDefaultDiagnostic(DiagnosticAnalyzer[] analyzers) { if (base.GetDefaultDiagnostic(analyzers) is { } descriptor) { @@ -135,7 +135,7 @@ private bool CodeActionExpected() /// A representing the asynchronous operation. protected async Task VerifyRefactoringAsync(SolutionState testState, SolutionState fixedState, DiagnosticResult triggerSpan, IVerifier verifier, CancellationToken cancellationToken) { - var numberOfIncrementalIterations = OffersEmptyRefactoring || HasAnyChange(testState, fixedState) ? 1 : 0; + var numberOfIncrementalIterations = OffersEmptyRefactoring || HasAnyChange(testState, fixedState, recursive: true) ? 1 : 0; await VerifyRefactoringAsync(Language, triggerSpan, GetCodeRefactoringProviders().ToImmutableArray(), testState, fixedState, numberOfIncrementalIterations, ApplyRefactoringAsync, verifier.PushContext("Code refactoring application"), cancellationToken); } @@ -156,6 +156,21 @@ private async Task VerifyRefactoringAsync( ExceptionDispatchInfo? iterationCountFailure; (project, iterationCountFailure) = await getFixedProject(triggerSpan, codeRefactoringProviders, CodeActionIndex, CodeActionEquivalenceKey, CodeActionVerifier, project, numberOfIterations, verifier, cancellationToken).ConfigureAwait(false); + // After applying the refactoring, compare the resulting string to the inputted one + await VerifyProjectAsync(newState, project, verifier, cancellationToken).ConfigureAwait(false); + + foreach (var additionalProject in newState.AdditionalProjects) + { + var actualProject = project.Solution.Projects.Single(p => p.Name == additionalProject.Key); + await VerifyProjectAsync(additionalProject.Value, actualProject, verifier, cancellationToken); + } + + // Validate the iteration counts after validating the content + iterationCountFailure?.Throw(); + } + + private async Task VerifyProjectAsync(ProjectState newState, Project project, IVerifier verifier, CancellationToken cancellationToken) + { // After applying the refactoring, compare the resulting string to the inputted one var updatedDocuments = project.Documents.ToArray(); @@ -183,21 +198,41 @@ private async Task VerifyRefactoringAsync( verifier.Equal(newState.AdditionalFiles[i].filename, updatedAdditionalDocuments[i].Name, $"file name was expected to be '{newState.AdditionalFiles[i].filename}' but was '{updatedAdditionalDocuments[i].Name}'"); } - // Validate the iteration counts after validating the content - iterationCountFailure?.Throw(); + var updatedAnalyzerConfigDocuments = project.AnalyzerConfigDocuments().ToArray(); + + verifier.Equal(newState.AnalyzerConfigFiles.Count, updatedAnalyzerConfigDocuments.Length, $"expected '{nameof(newState)}.{nameof(SolutionState.AnalyzerConfigFiles)}' and '{nameof(updatedAnalyzerConfigDocuments)}' to be equal but '{nameof(newState)}.{nameof(SolutionState.AnalyzerConfigFiles)}' contains '{newState.AnalyzerConfigFiles.Count}' documents and '{nameof(updatedAnalyzerConfigDocuments)}' contains '{updatedAnalyzerConfigDocuments.Length}' documents"); + + for (var i = 0; i < updatedAnalyzerConfigDocuments.Length; i++) + { + var actual = await updatedAnalyzerConfigDocuments[i].GetTextAsync(cancellationToken).ConfigureAwait(false); + verifier.EqualOrDiff(newState.AnalyzerConfigFiles[i].content.ToString(), actual.ToString(), $"content of '{newState.AnalyzerConfigFiles[i].filename}' did not match. Diff shown with expected as baseline:"); + verifier.Equal(newState.AnalyzerConfigFiles[i].content.Encoding, actual.Encoding, $"encoding of '{newState.AnalyzerConfigFiles[i].filename}' was expected to be '{newState.AnalyzerConfigFiles[i].content.Encoding?.WebName}' but was '{actual.Encoding?.WebName}'"); + verifier.Equal(newState.AnalyzerConfigFiles[i].content.ChecksumAlgorithm, actual.ChecksumAlgorithm, $"checksum algorithm of '{newState.AnalyzerConfigFiles[i].filename}' was expected to be '{newState.AnalyzerConfigFiles[i].content.ChecksumAlgorithm}' but was '{actual.ChecksumAlgorithm}'"); + verifier.Equal(newState.AnalyzerConfigFiles[i].filename, updatedAnalyzerConfigDocuments[i].Name, $"file name was expected to be '{newState.AnalyzerConfigFiles[i].filename}' but was '{updatedAnalyzerConfigDocuments[i].Name}'"); + } } private async Task<(Project project, ExceptionDispatchInfo? iterationCountFailure)> ApplyRefactoringAsync(DiagnosticResult triggerSpan, ImmutableArray codeRefactoringProviders, int? codeActionIndex, string? codeActionEquivalenceKey, Action? codeActionVerifier, Project project, int numberOfIterations, IVerifier verifier, CancellationToken cancellationToken) { + if (numberOfIterations == -1) + { + // For better error messages, use '==' instead of '<=' for iteration comparison when the right hand + // side is 1. + numberOfIterations = 1; + } + var expectedNumberOfIterations = numberOfIterations; if (numberOfIterations < 0) { numberOfIterations = -numberOfIterations; } + var currentIteration = -1; bool done; do { + currentIteration++; + try { verifier.True(--numberOfIterations >= -1, "The upper limit for the number of code fix iterations was exceeded"); @@ -212,24 +247,26 @@ private async Task VerifyRefactoringAsync( var actions = ImmutableArray.CreateBuilder(); var location = await GetTriggerLocationAsync(); + var triggerDocument = project.Solution.GetDocument(location.SourceTree); foreach (var codeRefactoringProvider in codeRefactoringProviders) { - var context = new CodeRefactoringContext(project.GetDocument(location.SourceTree), location.SourceSpan, actions.Add, cancellationToken); + var context = new CodeRefactoringContext(triggerDocument, location.SourceSpan, actions.Add, cancellationToken); await codeRefactoringProvider.ComputeRefactoringsAsync(context).ConfigureAwait(false); } var filteredActions = FilterCodeActions(actions.ToImmutable()); - var actionToApply = TryGetCodeActionToApply(filteredActions, codeActionIndex, codeActionEquivalenceKey, codeActionVerifier, verifier); + var actionToApply = TryGetCodeActionToApply(currentIteration, filteredActions, codeActionIndex, codeActionEquivalenceKey, codeActionVerifier, verifier); if (actionToApply != null) { anyActions = true; - var fixedProject = await ApplyCodeActionAsync(project, actionToApply, verifier, cancellationToken).ConfigureAwait(false); - if (fixedProject != project) + var originalProjectId = project.Id; + var fixedProject = await ApplyCodeActionAsync(triggerDocument.Project, actionToApply, verifier, cancellationToken).ConfigureAwait(false); + if (fixedProject != triggerDocument.Project) { done = false; - project = fixedProject; + project = fixedProject.Solution.GetProject(originalProjectId); break; } } diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeRefactoring.Testing/PublicAPI.Unshipped.txt b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeRefactoring.Testing/PublicAPI.Unshipped.txt index 74ed5b386..f2aaec429 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeRefactoring.Testing/PublicAPI.Unshipped.txt +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeRefactoring.Testing/PublicAPI.Unshipped.txt @@ -12,7 +12,7 @@ Microsoft.CodeAnalysis.Testing.EmptyCodeRefactoringProvider.EmptyCodeRefactoring abstract Microsoft.CodeAnalysis.Testing.CodeRefactoringTest.GetCodeRefactoringProviders() -> System.Collections.Generic.IEnumerable override Microsoft.CodeAnalysis.Testing.CodeRefactoringTest.GetDefaultDiagnostic(Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer[] analyzers) -> Microsoft.CodeAnalysis.DiagnosticDescriptor override Microsoft.CodeAnalysis.Testing.CodeRefactoringTest.GetDiagnosticAnalyzers() -> System.Collections.Generic.IEnumerable -override Microsoft.CodeAnalysis.Testing.CodeRefactoringTest.RunAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +override Microsoft.CodeAnalysis.Testing.CodeRefactoringTest.RunImplAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task override Microsoft.CodeAnalysis.Testing.EmptyCodeRefactoringProvider.ComputeRefactoringsAsync(Microsoft.CodeAnalysis.CodeRefactorings.CodeRefactoringContext context) -> System.Threading.Tasks.Task static Microsoft.CodeAnalysis.Testing.CodeRefactoringTest.TriggerSpanDescriptor.get -> Microsoft.CodeAnalysis.DiagnosticDescriptor static Microsoft.CodeAnalysis.Testing.CodeRefactoringVerifier.VerifyRefactoringAsync(string source, Microsoft.CodeAnalysis.Testing.DiagnosticResult expected, string fixedSource) -> System.Threading.Tasks.Task diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.SourceGenerators.Testing/PublicAPI.Unshipped.txt b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.SourceGenerators.Testing/PublicAPI.Unshipped.txt index c5004ff9d..09cd1e21f 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.SourceGenerators.Testing/PublicAPI.Unshipped.txt +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.SourceGenerators.Testing/PublicAPI.Unshipped.txt @@ -11,4 +11,4 @@ abstract Microsoft.CodeAnalysis.Testing.SourceGeneratorTest.CreateGen abstract Microsoft.CodeAnalysis.Testing.SourceGeneratorTest.GetSourceGenerators() -> System.Collections.Generic.IEnumerable override Microsoft.CodeAnalysis.Testing.SourceGeneratorTest.GetDiagnosticAnalyzers() -> System.Collections.Generic.IEnumerable override Microsoft.CodeAnalysis.Testing.SourceGeneratorTest.GetProjectCompilationAsync(Microsoft.CodeAnalysis.Project project, Microsoft.CodeAnalysis.Testing.IVerifier verifier, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task -override Microsoft.CodeAnalysis.Testing.SourceGeneratorTest.RunAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +override Microsoft.CodeAnalysis.Testing.SourceGeneratorTest.RunImplAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task diff --git a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.SourceGenerators.Testing/SourceGeneratorTest`1.cs b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.SourceGenerators.Testing/SourceGeneratorTest`1.cs index f7ac4fb3f..394b9bae3 100644 --- a/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.SourceGenerators.Testing/SourceGeneratorTest`1.cs +++ b/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.SourceGenerators.Testing/SourceGeneratorTest`1.cs @@ -33,7 +33,7 @@ protected override IEnumerable GetDiagnosticAnalyzers() protected abstract GeneratorDriver CreateGeneratorDriver(Project project, ImmutableArray sourceGenerators); - public override async Task RunAsync(CancellationToken cancellationToken = default) + protected override async Task RunImplAsync(CancellationToken cancellationToken) { var analyzers = GetDiagnosticAnalyzers().ToArray(); var defaultDiagnostic = GetDefaultDiagnostic(analyzers); diff --git a/src/VisualStudio.Roslyn.SDK/AssemblyVersionGenerator/AssemblyVersionGenerator.cs b/src/VisualStudio.Roslyn.SDK/AssemblyVersionGenerator/AssemblyVersionGenerator.cs new file mode 100644 index 000000000..84d961638 --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/AssemblyVersionGenerator/AssemblyVersionGenerator.cs @@ -0,0 +1,24 @@ +using System; +using System.Diagnostics; +using Microsoft.CodeAnalysis; + +namespace AssemblyVersionGenerator +{ + [Generator] + public class AssemblyVersionGenerator : ISourceGenerator + { + public void Execute(GeneratorExecutionContext context) + { + context.AddSource("assemblyversion.g.cs", $@" + +internal class AssemblyVersion +{{ + public const string Version = ""{context.Compilation.Assembly.Identity.Version}""; +}}"); + } + + public void Initialize(GeneratorInitializationContext context) + { + } + } +} diff --git a/src/VisualStudio.Roslyn.SDK/AssemblyVersionGenerator/AssemblyVersionGenerator.csproj b/src/VisualStudio.Roslyn.SDK/AssemblyVersionGenerator/AssemblyVersionGenerator.csproj new file mode 100644 index 000000000..6d0e593f4 --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/AssemblyVersionGenerator/AssemblyVersionGenerator.csproj @@ -0,0 +1,13 @@ + + + + netstandard2.0 + enable + true + + + + + + + diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/ComponentDebuggerLaunchProfile.xaml b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/ComponentDebuggerLaunchProfile.xaml new file mode 100644 index 000000000..656b12969 --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/ComponentDebuggerLaunchProfile.xaml @@ -0,0 +1,43 @@ + + + + + DebugRoslynComponent + + + AE27A6B0-E345-4288-96DF-5EAF394EE369 + + + 3644 + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/DebuggerOptions.xaml b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/DebuggerOptions.xaml deleted file mode 100644 index b431658e4..000000000 --- a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/DebuggerOptions.xaml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/DebuggerOptions.xaml.cs b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/DebuggerOptions.xaml.cs deleted file mode 100644 index 56459afeb..000000000 --- a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/DebuggerOptions.xaml.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace Roslyn.ComponentDebugger -{ - partial class DebuggerOptions - { - public DebuggerOptions() - { - InitializeComponent(); - } - } -} diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/DebuggerOptionsViewModel.cs b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/DebuggerOptionsViewModel.cs deleted file mode 100644 index 180012a97..000000000 --- a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/DebuggerOptionsViewModel.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.ComponentModel; -using System.Linq; -using System.Runtime.CompilerServices; - -namespace Roslyn.ComponentDebugger -{ - internal sealed class DebuggerOptionsViewModel : INotifyPropertyChanged - { - private readonly Action _indexChanged; - - private IEnumerable _projectNames = ImmutableArray.Empty; - - private int _selectedProjectIndex = -1; - - public event PropertyChangedEventHandler? PropertyChanged; - - public DebuggerOptionsViewModel(Action indexChanged) - { - _indexChanged = indexChanged; - } - - public IEnumerable ProjectNames - { - get => _projectNames; - set - { - if (!_projectNames.SequenceEqual(value)) - { - _projectNames = value; - NotifyPropertyChanged(); - } - } - } - - public int SelectedProjectIndex - { - get => _selectedProjectIndex; - set - { - if (_selectedProjectIndex != value) - { - _selectedProjectIndex = value; - NotifyPropertyChanged(); - _indexChanged?.Invoke(value); - } - } - } - - private void NotifyPropertyChanged([CallerMemberName]string propertyName = "") - { - this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - } -} diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/LaunchSettingsManager.cs b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/LaunchSettingsManager.cs index 6d9c97236..44a5d49a2 100644 --- a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/LaunchSettingsManager.cs +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/LaunchSettingsManager.cs @@ -38,22 +38,5 @@ public LaunchSettingsManager(UnconfiguredProject owningProject, IDebugTokenRepla } return targetProject; } - - public void WriteProjectForLaunch(IWritableLaunchProfile profile, UnconfiguredProject targetProject) - { - if (profile is null) - { - throw new System.ArgumentNullException(nameof(profile)); - } - - if (targetProject is null) - { - throw new System.ArgumentNullException(nameof(targetProject)); - } - - var rootedPath = _owningProject.MakeRelative(targetProject.FullPath); - profile.OtherSettings[Constants.TargetProjectKeyName] = rootedPath; - } - } } diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/LaunchSettingsProvider.cs b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/LaunchSettingsProvider.cs deleted file mode 100644 index 43b54a2d7..000000000 --- a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/LaunchSettingsProvider.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Immutable; -using System.ComponentModel.Composition; -using System.IO; -using System.Linq; -using System.Windows.Controls; -using Microsoft.VisualStudio.ProjectSystem; -using Microsoft.VisualStudio.ProjectSystem.Debug; -using Microsoft.VisualStudio.Utilities; -using Task = System.Threading.Tasks.Task; - -namespace Roslyn.ComponentDebugger -{ - [Export(typeof(ILaunchSettingsUIProvider))] - [AppliesTo(Constants.RoslynComponentCapability)] - public class LaunchSettingsProvider : ILaunchSettingsUIProvider - { - private readonly IProjectThreadingService _threadingService; - private readonly UnconfiguredProject _unconfiguredProject; - private readonly LaunchSettingsManager _launchSettingsManager; - private readonly DebuggerOptionsViewModel _viewModel; - - private ImmutableArray _projects; - private IWritableLaunchProfile? _launchProfile; - - [ImportingConstructor] - [Obsolete("This exported object must be obtained through the MEF export provider.", error: true)] - public LaunchSettingsProvider(IProjectThreadingService threadingService, UnconfiguredProject unconfiguredProject, LaunchSettingsManager launchSettingsManager) - { - _threadingService = threadingService; - _unconfiguredProject = unconfiguredProject; - _launchSettingsManager = launchSettingsManager; - _viewModel = new DebuggerOptionsViewModel(IndexChanged); - } - - public string CommandName { get => Constants.CommandName; } - - // https://github.com/dotnet/roslyn-sdk/issues/730 : localization - public string FriendlyName { get => "Roslyn Component"; } - - public UserControl? CustomUI { get => new DebuggerOptions() { DataContext = _viewModel }; } - - public void ProfileSelected(IWritableLaunchSettings curSettings) - { - _launchProfile = curSettings?.ActiveProfile; - _threadingService.ExecuteSynchronously(UpdateViewModelAsync); - } - - public bool ShouldEnableProperty(string propertyName) - { - // we disable all the default options for a debugger. - // in the future we might want to enable env vars and (potentially) the exe to allow - // customization of the compiler used? - return false; - } - - private async Task UpdateViewModelAsync() - { - var targetProjects = ArrayBuilder.GetInstance(); - - // get the output assembly for this project - var projectArgs = await _unconfiguredProject.GetCompilationArgumentsAsync().ConfigureAwait(false); - var targetArg = projectArgs.LastOrDefault(a => a.StartsWith("/out:", StringComparison.OrdinalIgnoreCase)); - var target = Path.GetFileName(targetArg); - - var projectService = _unconfiguredProject.Services.ProjectService; - foreach (var targetProjectUnconfigured in projectService.LoadedUnconfiguredProjects) - { - // check if the args contain the project as an analyzer ref - foreach (var arg in await targetProjectUnconfigured.GetCompilationArgumentsAsync().ConfigureAwait(false)) - { - if (arg.StartsWith("/analyzer:", StringComparison.OrdinalIgnoreCase) - && arg.EndsWith(target, StringComparison.OrdinalIgnoreCase)) - { - targetProjects.Add(targetProjectUnconfigured); - } - } - } - _projects = targetProjects.ToImmutableAndFree(); - - var launchTargetProject = await _launchSettingsManager.TryGetProjectForLaunchAsync(_launchProfile?.ToLaunchProfile()).ConfigureAwait(true); - var index = _projects.IndexOf(launchTargetProject!); - - _viewModel.ProjectNames = _projects.Select(p => Path.GetFileNameWithoutExtension(p.FullPath)); - _viewModel.SelectedProjectIndex = index; - } - - private void IndexChanged(int newIndex) - { - if (_launchProfile is object && !_projects.IsDefaultOrEmpty && newIndex >= 0 && newIndex < _projects.Length) - { - var project = _projects[newIndex]; - _launchSettingsManager.WriteProjectForLaunch(_launchProfile, project); - } - } - } -} diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/ProjectUtilities.cs b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/ProjectUtilities.cs index b20cc3115..4c2a4a769 100644 --- a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/ProjectUtilities.cs +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/ProjectUtilities.cs @@ -4,13 +4,41 @@ using System; using System.Collections.Immutable; +using System.IO; +using System.Linq; using System.Threading.Tasks; using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.Utilities; namespace Roslyn.ComponentDebugger { public static class ProjectUtilities { + public static async Task> GetComponentReferencingProjectsAsync(this UnconfiguredProject unconfiguredProject) + { + var targetProjects = ArrayBuilder.GetInstance(); + + // get the output assembly for this project + var projectArgs = await unconfiguredProject.GetCompilationArgumentsAsync().ConfigureAwait(false); + var targetArg = projectArgs.LastOrDefault(a => a.StartsWith("/out:", StringComparison.OrdinalIgnoreCase)); + var target = Path.GetFileName(targetArg); + + var projectService = unconfiguredProject.Services.ProjectService; + foreach (var targetProjectUnconfigured in projectService.LoadedUnconfiguredProjects) + { + // check if the args contain the project as an analyzer ref + foreach (var arg in await targetProjectUnconfigured.GetCompilationArgumentsAsync().ConfigureAwait(false)) + { + if (arg.StartsWith("/analyzer:", StringComparison.OrdinalIgnoreCase) + && arg.EndsWith(target, StringComparison.OrdinalIgnoreCase)) + { + targetProjects.Add(targetProjectUnconfigured); + } + } + } + return targetProjects.ToImmutableAndFree(); + } + public static Task> GetCompilationArgumentsAsync(this UnconfiguredProject project) { if (project is null) diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/Roslyn.ComponentDebugger.csproj b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/Roslyn.ComponentDebugger.csproj index 41199d84d..28f17264f 100644 --- a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/Roslyn.ComponentDebugger.csproj +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/Roslyn.ComponentDebugger.csproj @@ -9,13 +9,27 @@ true $(NoWarn);NU1603;NU1605 + + + + Never + MSBuild:GenerateRuleSourceFromXaml + Designer + None + None + + + + + + - + \ No newline at end of file diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/RuleExporter.cs b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/RuleExporter.cs new file mode 100644 index 000000000..38382da9f --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/RuleExporter.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.ProjectSystem.Properties; + +namespace Roslyn.ComponentDebugger +{ + internal static class RuleExporter + { + /// + /// Used to export the XAML rule via MEF + /// + [ExportPropertyXamlRuleDefinition( + xamlResourceAssemblyName: "Roslyn.ComponentDebugger, Version=" + AssemblyVersion.Version + ", Culture=neutral, PublicKeyToken=31bf3856ad364e35", + xamlResourceStreamName: "XamlRuleToCode:ComponentDebuggerLaunchProfile.xaml", + context: PropertyPageContexts.Project)] + [AppliesTo(Constants.RoslynComponentCapability)] +#pragma warning disable CS0649 + public static int LaunchProfileRule; +#pragma warning restore CS0649 + } +} diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/TargetProjectEnumProvider.cs b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/TargetProjectEnumProvider.cs new file mode 100644 index 000000000..23582bd2a --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/TargetProjectEnumProvider.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Build.Framework.XamlTypes; +using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.ProjectSystem.Properties; + +namespace Roslyn.ComponentDebugger +{ + [ExportDynamicEnumValuesProvider(nameof(TargetProjectEnumProvider))] + [AppliesTo(Constants.RoslynComponentCapability)] + public class TargetProjectEnumProvider : IDynamicEnumValuesProvider + { + private readonly UnconfiguredProject _unconfiguredProject; + private readonly LaunchSettingsManager _launchSettingsManager; + + [ImportingConstructor] + public TargetProjectEnumProvider(UnconfiguredProject unconfiguredProject, LaunchSettingsManager launchSettingsManager) + { + _unconfiguredProject = unconfiguredProject; + _launchSettingsManager = launchSettingsManager; + } + + public async Task GetProviderAsync(IList? options) + { + // get the targets for this project + var projects = await _unconfiguredProject.GetComponentReferencingProjectsAsync().ConfigureAwait(false); + + // convert to display values of friendly name + relative path + var displayValues = projects.Select(p => (Path.GetFileNameWithoutExtension(p.FullPath), _unconfiguredProject.MakeRelative(p.FullPath))).ToImmutableArray(); + + return new TargetProjectEnumValuesGenerator(displayValues); + } + + private class TargetProjectEnumValuesGenerator : IDynamicEnumValuesGenerator + { + private readonly ImmutableArray<(string display, string path)> _referencingProjects; + + public bool AllowCustomValues => false; + + public TargetProjectEnumValuesGenerator(ImmutableArray<(string display, string path)> referencingProjects) + { + _referencingProjects = referencingProjects; + } + + public Task> GetListedValuesAsync() + { + var values = _referencingProjects.Select(p => new PageEnumValue(new EnumValue() { DisplayName = p.display, Name = p.path})).Cast().ToImmutableArray(); + return Task.FromResult>(values); + } + + /// + /// The user can't add arbitrary projects from the UI, so this is unsupported + /// + public Task TryCreateEnumValueAsync(string userSuppliedValue) => Task.FromResult(null); + } + } +} diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.cs.xlf b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.cs.xlf new file mode 100644 index 000000000..70107d3d7 --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.cs.xlf @@ -0,0 +1,32 @@ + + + + + + A project that uses this component, whose compilation will be debugged. + A project that uses this component, whose compilation will be debugged. + + + + Target Project + Target Project + + + + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + + + + Roslyn Component + Roslyn Component + + + + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + + + + + \ No newline at end of file diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.de.xlf b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.de.xlf new file mode 100644 index 000000000..3f73ba68a --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.de.xlf @@ -0,0 +1,32 @@ + + + + + + A project that uses this component, whose compilation will be debugged. + A project that uses this component, whose compilation will be debugged. + + + + Target Project + Target Project + + + + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + + + + Roslyn Component + Roslyn Component + + + + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + + + + + \ No newline at end of file diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.es.xlf b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.es.xlf new file mode 100644 index 000000000..49696eefb --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.es.xlf @@ -0,0 +1,32 @@ + + + + + + A project that uses this component, whose compilation will be debugged. + A project that uses this component, whose compilation will be debugged. + + + + Target Project + Target Project + + + + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + + + + Roslyn Component + Roslyn Component + + + + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + + + + + \ No newline at end of file diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.fr.xlf b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.fr.xlf new file mode 100644 index 000000000..b0ad7e720 --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.fr.xlf @@ -0,0 +1,32 @@ + + + + + + A project that uses this component, whose compilation will be debugged. + A project that uses this component, whose compilation will be debugged. + + + + Target Project + Target Project + + + + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + + + + Roslyn Component + Roslyn Component + + + + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + + + + + \ No newline at end of file diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.it.xlf b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.it.xlf new file mode 100644 index 000000000..6e9993b18 --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.it.xlf @@ -0,0 +1,32 @@ + + + + + + A project that uses this component, whose compilation will be debugged. + A project that uses this component, whose compilation will be debugged. + + + + Target Project + Target Project + + + + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + + + + Roslyn Component + Roslyn Component + + + + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + + + + + \ No newline at end of file diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.ja.xlf b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.ja.xlf new file mode 100644 index 000000000..38b0fc455 --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.ja.xlf @@ -0,0 +1,32 @@ + + + + + + A project that uses this component, whose compilation will be debugged. + A project that uses this component, whose compilation will be debugged. + + + + Target Project + Target Project + + + + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + + + + Roslyn Component + Roslyn Component + + + + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + + + + + \ No newline at end of file diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.ko.xlf b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.ko.xlf new file mode 100644 index 000000000..55caf820f --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.ko.xlf @@ -0,0 +1,32 @@ + + + + + + A project that uses this component, whose compilation will be debugged. + A project that uses this component, whose compilation will be debugged. + + + + Target Project + Target Project + + + + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + + + + Roslyn Component + Roslyn Component + + + + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + + + + + \ No newline at end of file diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.pl.xlf b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.pl.xlf new file mode 100644 index 000000000..d623a3953 --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.pl.xlf @@ -0,0 +1,32 @@ + + + + + + A project that uses this component, whose compilation will be debugged. + A project that uses this component, whose compilation will be debugged. + + + + Target Project + Target Project + + + + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + + + + Roslyn Component + Roslyn Component + + + + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + + + + + \ No newline at end of file diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.pt-BR.xlf b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.pt-BR.xlf new file mode 100644 index 000000000..d93d99696 --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.pt-BR.xlf @@ -0,0 +1,32 @@ + + + + + + A project that uses this component, whose compilation will be debugged. + A project that uses this component, whose compilation will be debugged. + + + + Target Project + Target Project + + + + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + + + + Roslyn Component + Roslyn Component + + + + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + + + + + \ No newline at end of file diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.ru.xlf b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.ru.xlf new file mode 100644 index 000000000..5890416c8 --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.ru.xlf @@ -0,0 +1,32 @@ + + + + + + A project that uses this component, whose compilation will be debugged. + A project that uses this component, whose compilation will be debugged. + + + + Target Project + Target Project + + + + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + + + + Roslyn Component + Roslyn Component + + + + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + + + + + \ No newline at end of file diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.tr.xlf b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.tr.xlf new file mode 100644 index 000000000..8c8db3e8f --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.tr.xlf @@ -0,0 +1,32 @@ + + + + + + A project that uses this component, whose compilation will be debugged. + A project that uses this component, whose compilation will be debugged. + + + + Target Project + Target Project + + + + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + + + + Roslyn Component + Roslyn Component + + + + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + + + + + \ No newline at end of file diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.zh-Hans.xlf b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.zh-Hans.xlf new file mode 100644 index 000000000..1e3273ef3 --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.zh-Hans.xlf @@ -0,0 +1,32 @@ + + + + + + A project that uses this component, whose compilation will be debugged. + A project that uses this component, whose compilation will be debugged. + + + + Target Project + Target Project + + + + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + + + + Roslyn Component + Roslyn Component + + + + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + + + + + \ No newline at end of file diff --git a/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.zh-Hant.xlf b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.zh-Hant.xlf new file mode 100644 index 000000000..5720b1a0c --- /dev/null +++ b/src/VisualStudio.Roslyn.SDK/ComponentDebugger/xlf/ComponentDebuggerLaunchProfile.xaml.zh-Hant.xlf @@ -0,0 +1,32 @@ + + + + + + A project that uses this component, whose compilation will be debugged. + A project that uses this component, whose compilation will be debugged. + + + + Target Project + Target Project + + + + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + Allows a user to debug a Roslyn Component by running it in the context of another projects build. + + + + Roslyn Component + Roslyn Component + + + + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + A Roslyn Component can be debugged in the context of compiling a second project that uses it. Ensure your target project is referencing this component for it to appear in the list. + + + + + \ No newline at end of file diff --git a/src/VisualStudio.Roslyn.SDK/Roslyn.SDK.Template.Wizard/RoslynSDKRootTemplateWizard.cs b/src/VisualStudio.Roslyn.SDK/Roslyn.SDK.Template.Wizard/RoslynSDKRootTemplateWizard.cs index 9875b5f8d..a74df4b69 100644 --- a/src/VisualStudio.Roslyn.SDK/Roslyn.SDK.Template.Wizard/RoslynSDKRootTemplateWizard.cs +++ b/src/VisualStudio.Roslyn.SDK/Roslyn.SDK.Template.Wizard/RoslynSDKRootTemplateWizard.cs @@ -9,19 +9,6 @@ public partial class RoslynSDKRootTemplateWizard { - private const string NuGetConfig = @" - - - - - - - - - - -"; - public static Dictionary GlobalDictionary = new Dictionary(); private void OnRunStarted(DTE dte, Dictionary replacementsDictionary, WizardRunKind runKind, object[] customParams) @@ -31,22 +18,5 @@ private void OnRunStarted(DTE dte, Dictionary replacementsDictio // add the root project name (the name the user passed in) to the global replacement dictionary GlobalDictionary["$saferootprojectname$"] = replacementsDictionary["$safeprojectname$"]; GlobalDictionary["$saferootidentifiername$"] = replacementsDictionary["$safeprojectname$"].Replace(".",""); - - if (!replacementsDictionary.TryGetValue("$solutiondirectory$", out var solutionFolder)) - { - var solutionFile = dte.Solution.FullName; - if (string.IsNullOrEmpty(solutionFile)) - return; - - solutionFolder = Path.GetDirectoryName(solutionFile); - } - - if (Directory.Exists(solutionFolder)) - { - File.WriteAllText( - Path.Combine(solutionFolder, "NuGet.config"), - NuGetConfig, - Encoding.UTF8); - } } } diff --git a/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/ProjectTemplates/CSharp/Diagnostic/Test/Test.csproj b/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/ProjectTemplates/CSharp/Diagnostic/Test/Test.csproj index 9c07533da..3ff076d88 100644 --- a/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/ProjectTemplates/CSharp/Diagnostic/Test/Test.csproj +++ b/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/ProjectTemplates/CSharp/Diagnostic/Test/Test.csproj @@ -12,12 +12,12 @@ - - - - - - + + + + + + diff --git a/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/ProjectTemplates/VisualBasic/Diagnostic/Test/UnitTestProject.vbproj b/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/ProjectTemplates/VisualBasic/Diagnostic/Test/UnitTestProject.vbproj index 75b2bcd1f..570444648 100644 --- a/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/ProjectTemplates/VisualBasic/Diagnostic/Test/UnitTestProject.vbproj +++ b/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/ProjectTemplates/VisualBasic/Diagnostic/Test/UnitTestProject.vbproj @@ -12,12 +12,12 @@ - - - - - - + + + + + + diff --git a/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/Properties/AssemblyInfo.cs b/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/Properties/AssemblyInfo.cs index 6ccb8544f..710a6f0cb 100644 --- a/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/Properties/AssemblyInfo.cs +++ b/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/Properties/AssemblyInfo.cs @@ -5,3 +5,4 @@ using Microsoft.VisualStudio.Shell; [assembly: ProvideBindingRedirection(CodeBase = "Roslyn.SDK.Template.Wizard.dll", OldVersionLowerBound = "1.0.0.0")] +[assembly: ProvideCodeBase(AssemblyName = "Roslyn.ComponentDebugger", CodeBase = "$PackageFolder$\\Roslyn.ComponentDebugger.dll")] diff --git a/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/Roslyn.SDK.csproj b/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/Roslyn.SDK.csproj index 4a2d50555..8f82ad750 100644 --- a/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/Roslyn.SDK.csproj +++ b/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/Roslyn.SDK.csproj @@ -123,8 +123,8 @@ Roslyn.ComponentDebugger - BuiltProjectOutputGroup%3bGetCopyToOutputDirectoryItems%3b - DebugSymbolsProjectOutputGroup%3b + BuiltProjectOutputGroup%3bGetCopyToOutputDirectoryItems + DebugSymbolsProjectOutputGroup true false diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/MetadataReferenceTests.cs b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/MetadataReferenceTests.cs index 70f0a47d5..9a5f501f3 100644 --- a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/MetadataReferenceTests.cs +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/MetadataReferenceTests.cs @@ -450,6 +450,7 @@ public async Task ResolveReferenceAssemblies_Net50() [InlineData("netcoreapp3.1")] #if !(NETCOREAPP1_1 || NET46) [InlineData("net5.0")] + [InlineData("net6.0")] #endif [InlineData("netstandard1.0")] [InlineData("netstandard1.1")] @@ -500,6 +501,7 @@ internal static ReferenceAssemblies ReferenceAssembliesForTargetFramework(string "netcoreapp3.0" => ReferenceAssemblies.NetCore.NetCoreApp30, "netcoreapp3.1" => ReferenceAssemblies.NetCore.NetCoreApp31, "net5.0" => ReferenceAssemblies.Net.Net50, + "net6.0" => ReferenceAssemblies.Net.Net60, "netstandard1.0" => ReferenceAssemblies.NetStandard.NetStandard10, "netstandard1.1" => ReferenceAssemblies.NetStandard.NetStandard11, "netstandard1.2" => ReferenceAssemblies.NetStandard.NetStandard12, diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/MultipleProjectsTests.cs b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/MultipleProjectsTests.cs index 0ecfcd1e9..118ba2b01 100644 --- a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/MultipleProjectsTests.cs +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/MultipleProjectsTests.cs @@ -12,6 +12,8 @@ Microsoft.CodeAnalysis.Testing.TestAnalyzers.HighlightBracesAnalyzer, Microsoft.CodeAnalysis.Testing.TestAnalyzers.CSharpAnalyzerTest, Microsoft.CodeAnalysis.Testing.DefaultVerifier>; +using VisualBasicTest = Microsoft.CodeAnalysis.Testing.TestAnalyzers.VisualBasicAnalyzerTest< + Microsoft.CodeAnalysis.Testing.TestAnalyzers.HighlightBracesAnalyzer>; namespace Microsoft.CodeAnalysis.Testing { @@ -44,6 +46,87 @@ public async Task TwoCSharpProjects_Independent() }.RunAsync(); } + [Fact] + public async Task TwoVisualBasicProjects_Independent() + { + await new VisualBasicTest + { + TestState = + { + Sources = + { + @"Public Class Derived1 : Inherits {|BC30002:Base2|} : End Class", + @"Public Class Base1 : End Class", + }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"Public Class Derived2 : Inherits {|BC30002:Base1|} : End Class", + @"Public Class Base2 : End Class", + }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task OneCSharpProjectOneVisualBasicProject_Independent() + { + await new CSharpTest + { + TestState = + { + Sources = + { + @"public class Derived1 : {|CS0246:Base2|} [|{|] }", + @"public class Base1 [|{|] }", + }, + AdditionalProjects = + { + ["Secondary", LanguageNames.VisualBasic] = + { + Sources = + { + @"Public Class Derived2 : Inherits {|BC30002:Base1|} : End Class", + @"Public Class Base2 : End Class", + }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task OneVisualBasicProjectOneCSharpProject_Independent() + { + await new VisualBasicTest + { + TestState = + { + Sources = + { + @"Public Class Derived1 : Inherits {|BC30002:Base2|} : End Class", + @"Public Class Base1 : End Class", + }, + AdditionalProjects = + { + ["Secondary", LanguageNames.CSharp] = + { + Sources = + { + @"public class Derived2 : {|CS0246:Base1|} [|{|] }", + @"public class Base2 [|{|] }", + }, + }, + }, + }, + }.RunAsync(); + } + [Fact] public async Task TwoCSharpProjects_IndependentWithMarkupLocations() { @@ -120,6 +203,93 @@ public async Task TwoCSharpProjects_PrimaryReferencesSecondary() }.RunAsync(); } + [Fact] + public async Task TwoVisualBasicProjects_PrimaryReferencesSecondary() + { + // TestProject references Secondary + await new VisualBasicTest + { + TestState = + { + Sources = + { + @"Public Class Derived1 : Inherits Base2 : End Class", + @"Public Class Base1 : End Class", + }, + AdditionalProjectReferences = { "Secondary", }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"Public Class Derived2 : Inherits {|BC30002:Base1|} : End Class", + @"Public Class Base2 : End Class", + }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task OneCSharpProjectOneVisualBasicProject_PrimaryReferencesSecondary() + { + // TestProject references Secondary + await new CSharpTest + { + TestState = + { + Sources = + { + @"public class Type1 [|{|] object field = new Type3(); }", + @"public class Type2 [|{|] }", + }, + AdditionalProjectReferences = { "Secondary", }, + AdditionalProjects = + { + ["Secondary", LanguageNames.VisualBasic] = + { + Sources = + { + @"Public Class Type3 : Private field As Object = New {|BC30002:Type1|}() : End Class", + @"Public Class Type4 : End Class", + }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task OneVisualBasicProjectOneCSharpProject_PrimaryReferencesSecondary() + { + // TestProject references Secondary + await new VisualBasicTest + { + TestState = + { + Sources = + { + @"Public Class Type1 : Private field As Object = New Type3() : End Class", + @"Public Class Type2 : End Class", + }, + AdditionalProjectReferences = { "Secondary", }, + AdditionalProjects = + { + ["Secondary", LanguageNames.CSharp] = + { + Sources = + { + @"public class Type3 [|{|] object field = new {|CS0246:Type1|}(); }", + @"public class Type4 [|{|] }", + }, + }, + }, + }, + }.RunAsync(); + } + [Fact] public async Task TwoCSharpProjects_SecondaryReferencesPrimary() { @@ -149,6 +319,93 @@ public async Task TwoCSharpProjects_SecondaryReferencesPrimary() }.RunAsync(); } + [Fact] + public async Task TwoVisualBasicProjects_SecondaryReferencesPrimary() + { + // TestProject references Secondary + await new VisualBasicTest + { + TestState = + { + Sources = + { + @"Public Class Derived1 : Inherits {|BC30002:Base2|} : End Class", + @"Public Class Base1 : End Class", + }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"Public Class Derived2 : Inherits Base1 : End Class", + @"Public Class Base2 : End Class", + }, + AdditionalProjectReferences = { "TestProject" }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task OneCSharpProjectOneVisualBasicProject_SecondaryReferencesPrimary() + { + // TestProject references Secondary + await new CSharpTest + { + TestState = + { + Sources = + { + @"public class Type1 [|{|] object field = new {|CS0246:Type3|}(); }", + @"public class Type2 [|{|] }", + }, + AdditionalProjects = + { + ["Secondary", LanguageNames.VisualBasic] = + { + Sources = + { + @"Public Class Type3 : Private field As Object = New Type1() : End Class", + @"Public Class Type4 : End Class", + }, + AdditionalProjectReferences = { "TestProject" }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task OneVisualBasicProjectOneCSharpProject_SecondaryReferencesPrimary() + { + // TestProject references Secondary + await new VisualBasicTest + { + TestState = + { + Sources = + { + @"Public Class Type1 : Private field As Object = New {|BC30002:Type3|}() : End Class", + @"Public Class Type2 : End Class", + }, + AdditionalProjects = + { + ["Secondary", LanguageNames.CSharp] = + { + Sources = + { + @"public class Type3 [|{|] object field = new Type1(); }", + @"public class Type4 [|{|] }", + }, + AdditionalProjectReferences = { "TestProject" }, + }, + }, + }, + }.RunAsync(); + } + [Fact] public async Task TwoCSharpProjects_DefaultPaths() { diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/VerifierExtensionsTests.cs b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/VerifierExtensionsTests.cs new file mode 100644 index 000000000..eb850de69 --- /dev/null +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing.UnitTests/VerifierExtensionsTests.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Xunit; + +namespace Microsoft.CodeAnalysis.Testing +{ + public class VerifierExtensionsTests + { + [Fact] + [WorkItem(876, "https://github.com/dotnet/roslyn-sdk/issues/876")] + public void VerifyContentWithMixedLineEndings1() + { + var baseline = + "Line 1\r\n" + + "Line 2\r\n" + + "Line 3\r\n" + + "Line 4\r\n" + + "Line 5\r\n" + + "Line 6\r\n"; + var modified = + "Line 1\r" + + "Line 2\r\n" + + "Line 3\n" + + "Line 4\r" + + "Line 5\r\n" + + "Line 6\n"; + + var exception = Assert.Throws(() => new DefaultVerifier().EqualOrDiff(baseline, modified)); + Assert.Equal( + $"Actual and expected values differ. Expected shown in baseline of diff:{Environment.NewLine}" + + $"-Line 1{Environment.NewLine}" + + $"+Line 1{Environment.NewLine}" + + $" Line 2{Environment.NewLine}" + + $"-Line 3{Environment.NewLine}" + + $"-Line 4{Environment.NewLine}" + + $"+Line 3{Environment.NewLine}" + + $"+Line 4{Environment.NewLine}" + + $" Line 5{Environment.NewLine}" + + $"-Line 6{Environment.NewLine}" + + $"+Line 6{Environment.NewLine}", + exception.Message); + } + + [Fact] + [WorkItem(876, "https://github.com/dotnet/roslyn-sdk/issues/876")] + public void VerifyContentWithMixedLineEnding2() + { + var baseline = + "Line 1\n" + + "Line 2\n" + + "Line 3\n" + + "Line 4\n" + + "Line 5\n" + + "Line 6\n"; + var modified = + "Line 1\r" + + "Line 2\r\n" + + "Line 3\n" + + "Line 4\r" + + "Line 5\r\n" + + "Line 6\n"; + + var exception = Assert.Throws(() => new DefaultVerifier().EqualOrDiff(baseline, modified)); + Assert.Equal( + $"Actual and expected values differ. Expected shown in baseline of diff:{Environment.NewLine}" + + $"-Line 1{Environment.NewLine}" + + $"-Line 2{Environment.NewLine}" + + $"+Line 1{Environment.NewLine}" + + $"+Line 2{Environment.NewLine}" + + $" Line 3{Environment.NewLine}" + + $"-Line 4{Environment.NewLine}" + + $"-Line 5{Environment.NewLine}" + + $"+Line 4{Environment.NewLine}" + + $"+Line 5{Environment.NewLine}" + + $" Line 6{Environment.NewLine}", + exception.Message); + } + } +} diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing.UnitTests/CodeFixIterationTests.cs b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing.UnitTests/CodeFixIterationTests.cs index 43704cacf..e50674169 100644 --- a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing.UnitTests/CodeFixIterationTests.cs +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing.UnitTests/CodeFixIterationTests.cs @@ -3,18 +3,10 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Composition; -using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.CodeActions; -using Microsoft.CodeAnalysis.CodeFixes; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing.TestAnalyzers; using Microsoft.CodeAnalysis.Testing.TestFixes; -using Microsoft.CodeAnalysis.Text; using Xunit; namespace Microsoft.CodeAnalysis.Testing @@ -220,11 +212,11 @@ class TestClass { }.RunAsync(); }); - new DefaultVerifier().EqualOrDiff($"Context: Iterative code fix application{Environment.NewLine}The upper limit for the number of code fix iterations was exceeded", exception.Message); + new DefaultVerifier().EqualOrDiff($"Context: Iterative code fix application{Environment.NewLine}Expected '1' iterations but found '2' iterations.", exception.Message); } [Theory] - [InlineData(-1, "The upper limit for the number of code fix iterations was exceeded", " 5")] + [InlineData(-1, "Expected '1' iterations but found '2' iterations.", " 5")] [InlineData(0, "The upper limit for the number of code fix iterations was exceeded", " [|4|]")] [InlineData(1, "Expected '1' iterations but found '2' iterations.", " 5")] public async Task TestTwoIterationsRequiredButIncrementalDeclaredIncorrectly(int declaredIncrementalIterations, string message, string replacement) @@ -284,11 +276,11 @@ class TestClass { }.RunAsync(); }); - Assert.Equal($"Context: Fix all in document{Environment.NewLine}The upper limit for the number of code fix iterations was exceeded", exception.Message); + Assert.Equal($"Context: Fix all in document{Environment.NewLine}Expected '1' iterations but found '2' iterations.", exception.Message); } [Theory] - [InlineData(-1, "The upper limit for the number of code fix iterations was exceeded", " 5")] + [InlineData(-1, "Expected '1' iterations but found '2' iterations.", " 5")] [InlineData(0, "The upper limit for the number of fix all iterations was exceeded", " [|4|]")] [InlineData(1, "Expected '1' iterations but found '2' iterations.", " 5")] public async Task TestTwoIterationsRequiredButFixAllDeclaredIncorrectly(int declaredFixAllIterations, string message, string replacement) @@ -403,67 +395,65 @@ class TestClass2 { Assert.Equal($"Context: {context}{Environment.NewLine}Expected '2' iterations but found '1' iterations.", exception.Message); } - /// - /// Reports a diagnostic on any integer literal token with a value less than five. - /// - [DiagnosticAnalyzer(LanguageNames.CSharp)] - private class LiteralUnderFiveAnalyzer : DiagnosticAnalyzer + [Fact] + [WorkItem(874, "https://github.com/dotnet/roslyn-sdk/issues/874")] + public async Task TestTwoIterationsRequiredButOneApplied() { - internal static readonly DiagnosticDescriptor Descriptor = - new DiagnosticDescriptor("LiteralUnderFive", "title", "message", "category", DiagnosticSeverity.Warning, isEnabledByDefault: true); - - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); - - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - - context.RegisterSyntaxNodeAction(HandleNumericLiteralExpression, SyntaxKind.NumericLiteralExpression); - } + var testCode = @" +class TestClass { + int field = [|3|]; +} +"; + var fixedCode = @" +class TestClass { + int field = [|4|]; +} +"; - private void HandleNumericLiteralExpression(SyntaxNodeAnalysisContext context) + await new CSharpTest { - var node = (LiteralExpressionSyntax)context.Node; - if (int.TryParse(node.Token.ValueText, out var value) && value < 5) + TestCode = testCode, + FixedState = { - context.ReportDiagnostic(Diagnostic.Create(Descriptor, node.Token.GetLocation())); - } - } + Sources = { fixedCode }, + MarkupHandling = MarkupMode.Allow, + }, + CodeActionEquivalenceKey = "IncrementFix:4", + CodeActionIndex = 0, + }.RunAsync(); } - [ExportCodeFixProvider(LanguageNames.CSharp)] - [PartNotDiscoverable] - private class IncrementFix : CodeFixProvider + [Fact] + [WorkItem(874, "https://github.com/dotnet/roslyn-sdk/issues/874")] + public async Task TestTwoIterationsRequiredButNoneApplied() { - public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(LiteralUnderFiveAnalyzer.Descriptor.Id); - - public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + var testCode = @" +class TestClass { + int field = [|3|]; +} +"; + var fixedCode = @" +class TestClass { + int field = [|4|]; +} +"; - public override Task RegisterCodeFixesAsync(CodeFixContext context) + var exception = await Assert.ThrowsAsync(async () => { - foreach (var diagnostic in context.Diagnostics) + await new CSharpTest { - context.RegisterCodeFix( - CodeAction.Create( - "LiteralUnderFive", - cancellationToken => CreateChangedDocument(context.Document, diagnostic.Location.SourceSpan, cancellationToken), - nameof(IncrementFix)), - diagnostic); - } - - return Task.CompletedTask; - } + TestCode = testCode, + FixedState = + { + Sources = { fixedCode }, + MarkupHandling = MarkupMode.Allow, + }, + CodeActionEquivalenceKey = "IncrementFix:3", + CodeActionIndex = 0, + }.RunAsync(); + }); - private async Task CreateChangedDocument(Document document, TextSpan sourceSpan, CancellationToken cancellationToken) - { - var tree = (await document.GetSyntaxTreeAsync(cancellationToken))!; - var root = await tree.GetRootAsync(cancellationToken); - var token = root.FindToken(sourceSpan.Start); - var replacement = int.Parse(token.ValueText) + 1; - var newToken = SyntaxFactory.Literal(token.LeadingTrivia, " " + replacement.ToString(), replacement, token.TrailingTrivia); - return document.WithSyntaxRoot(root.ReplaceToken(token, newToken)); - } + new DefaultVerifier().EqualOrDiff($"Context: Iterative code fix application{Environment.NewLine}The code action equivalence key and index must be consistent when both are specified.", exception.Message); } private class CSharpTest : CSharpCodeFixTest diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing.UnitTests/FixMultipleProjectsTests.cs b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing.UnitTests/FixMultipleProjectsTests.cs new file mode 100644 index 000000000..35059a454 --- /dev/null +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeFix.Testing.UnitTests/FixMultipleProjectsTests.cs @@ -0,0 +1,306 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading.Tasks; +using Xunit; +using CSharpTest = Microsoft.CodeAnalysis.Testing.TestFixes.CSharpCodeFixTest< + Microsoft.CodeAnalysis.Testing.TestAnalyzers.LiteralUnderFiveAnalyzer, + Microsoft.CodeAnalysis.Testing.TestFixes.IncrementFix>; +using VisualBasicTest = Microsoft.CodeAnalysis.Testing.TestFixes.VisualBasicCodeFixTest< + Microsoft.CodeAnalysis.Testing.TestAnalyzers.LiteralUnderFiveAnalyzer, + Microsoft.CodeAnalysis.Testing.TestFixes.IncrementFix>; + +namespace Microsoft.CodeAnalysis.Testing +{ + public class FixMultipleProjectsTests + { + [Fact] + public async Task TwoCSharpProjects_Independent() + { + await new CSharpTest + { + TestState = + { + Sources = + { + @"public class Type1 { int field = [|4|]; }", + @"public class Type2 { int field = [|4|]; }", + }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"public class Type3 { int field = [|4|]; }", + @"public class Type4 { int field = [|4|]; }", + }, + }, + }, + }, + FixedState = + { + Sources = + { + @"public class Type1 { int field = 5; }", + @"public class Type2 { int field = 5; }", + }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"public class Type3 { int field = 5; }", + @"public class Type4 { int field = 5; }", + }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task TwoVisualBasicProjects_Independent() + { + await new VisualBasicTest + { + TestState = + { + Sources = + { + @"Public Class Type1 : Private field = [|4|] : End Class", + @"Public Class Type2 : Private field = [|4|] : End Class", + }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"Public Class Type3 : Private field = [|4|] : End Class", + @"Public Class Type4 : Private field = [|4|] : End Class", + }, + }, + }, + }, + FixedState = + { + Sources = + { + @"Public Class Type1 : Private field = 5 : End Class", + @"Public Class Type2 : Private field = 5 : End Class", + }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"Public Class Type3 : Private field = 5 : End Class", + @"Public Class Type4 : Private field = 5 : End Class", + }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task OneCSharpProjectOneVisualBasicProject_Independent() + { + await new CSharpTest + { + TestState = + { + Sources = + { + @"public class Type1 { int field = [|4|]; }", + @"public class Type2 { int field = [|4|]; }", + }, + AdditionalProjects = + { + ["Secondary", LanguageNames.VisualBasic] = + { + Sources = + { + @"Public Class Type3 : Private field = [|4|] : End Class", + @"Public Class Type4 : Private field = [|4|] : End Class", + }, + }, + }, + }, + FixedState = + { + Sources = + { + @"public class Type1 { int field = 5; }", + @"public class Type2 { int field = 5; }", + }, + AdditionalProjects = + { + ["Secondary", LanguageNames.VisualBasic] = + { + Sources = + { + @"Public Class Type3 : Private field = 5 : End Class", + @"Public Class Type4 : Private field = 5 : End Class", + }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task OneVisualBasicProjectOneCSharpProject_Independent() + { + await new VisualBasicTest + { + TestState = + { + Sources = + { + @"Public Class Type1 : Private field = [|4|] : End Class", + @"Public Class Type2 : Private field = [|4|] : End Class", + }, + AdditionalProjects = + { + ["Secondary", LanguageNames.CSharp] = + { + Sources = + { + @"public class Type3 { int field = [|4|]; }", + @"public class Type4 { int field = [|4|]; }", + }, + }, + }, + }, + FixedState = + { + Sources = + { + @"Public Class Type1 : Private field = 5 : End Class", + @"Public Class Type2 : Private field = 5 : End Class", + }, + AdditionalProjects = + { + ["Secondary", LanguageNames.CSharp] = + { + Sources = + { + @"public class Type3 { int field = 5; }", + @"public class Type4 { int field = 5; }", + }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + public async Task TwoCSharpProjects_Independent_UnexpectedDiagnostic() + { + var exception = await Assert.ThrowsAsync(async () => + { + await new CSharpTest + { + TestState = + { + Sources = + { + @"public class Type1 { int field = [|4|]; }", + @"public class Type2 { int field = [|4|]; }", + }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"public class Type3 { int field = [|4|]; }", + @"public class Type4 { int field = [|4|]; }", + }, + }, + }, + }, + FixedState = + { + Sources = + { + @"public class Type1 { int field = 5; }", + @"public class Type2 { int field = 5; }", + }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"public class Type3 { int field = [|5|]; }", + @"public class Type4 { int field = 5; }", + }, + }, + }, + MarkupHandling = MarkupMode.Allow, + }, + }.RunAsync(); + }); + + new DefaultVerifier().EqualOrDiff($"Context: Diagnostics of fixed state{Environment.NewLine}Mismatch between number of diagnostics returned, expected \"1\" actual \"0\"{Environment.NewLine}{Environment.NewLine}Diagnostics:{Environment.NewLine} NONE.{Environment.NewLine}", exception.Message); + } + + [Fact] + public async Task TwoCSharpProjects_Independent_UnexpectedContent() + { + var exception = await Assert.ThrowsAsync(async () => + { + await new CSharpTest + { + TestState = + { + Sources = + { + @"public class Type1 { int field = [|4|]; }", + @"public class Type2 { int field = [|4|]; }", + }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"public class Type3 { int field = [|4|]; }", + @"public class Type4 { int field = [|4|]; }", + }, + }, + }, + }, + FixedState = + { + Sources = + { + @"public class Type1 { int field = 5; }", + @"public class Type2 { int field = 5; }", + }, + AdditionalProjects = + { + ["Secondary"] = + { + Sources = + { + @"public class Type3 { int field = 5; }", + @"public class Type4 { int field = 5; }", + }, + }, + }, + }, + }.RunAsync(); + }); + + new DefaultVerifier().EqualOrDiff($"Context: Iterative code fix application{Environment.NewLine}content of '/Secondary/Test0.cs' did not match. Diff shown with expected as baseline:{Environment.NewLine}-public class Type3 {{ int field = 5; }}{Environment.NewLine}+public class Type3 {{ int field = 5; }}{Environment.NewLine}", exception.Message); + } + } +} diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeRefactoring.Testing.UnitTests/RefactoringValidationTests.cs b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeRefactoring.Testing.UnitTests/RefactoringValidationTests.cs index b47c232dc..03780afb8 100644 --- a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeRefactoring.Testing.UnitTests/RefactoringValidationTests.cs +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.CodeRefactoring.Testing.UnitTests/RefactoringValidationTests.cs @@ -290,6 +290,68 @@ public async Task TestNoValidationPassesFull() }.RunAsync(); } + [Fact] + [WorkItem(806, "https://github.com/dotnet/roslyn-sdk/issues/806")] + public async Task TestWithAdditionalProject_SameLanguage() + { + await new ReplaceThisWithBaseTest + { + TestState = + { + Sources = { "public class Ignored { }" }, + AdditionalProjects = + { + ["Additional"] = + { + Sources = { ReplaceThisWithBaseTestCode }, + }, + }, + }, + FixedState = + { + Sources = { "public class Ignored { }" }, + AdditionalProjects = + { + ["Additional"] = + { + Sources = { ReplaceThisWithBaseFixedCode }, + }, + }, + }, + }.RunAsync(); + } + + [Fact] + [WorkItem(806, "https://github.com/dotnet/roslyn-sdk/issues/806")] + public async Task TestWithAdditionalProject_DifferentLanguage() + { + await new ReplaceThisWithBaseTestVisualBasic + { + TestState = + { + Sources = { "Public Class Ignored : End Class" }, + AdditionalProjects = + { + ["Additional", LanguageNames.CSharp] = + { + Sources = { ReplaceThisWithBaseTestCode }, + }, + }, + }, + FixedState = + { + Sources = { "Public Class Ignored : End Class" }, + AdditionalProjects = + { + ["Additional", LanguageNames.CSharp] = + { + Sources = { ReplaceThisWithBaseFixedCode }, + }, + }, + }, + }.RunAsync(); + } + [ExportCodeRefactoringProvider(LanguageNames.CSharp)] [PartNotDiscoverable] private class ReplaceThisWithBaseTokenFix : CodeRefactoringProvider @@ -405,5 +467,30 @@ protected override IEnumerable GetCodeRefactoringProvid yield return new TCodeRefactoring(); } } + + private class ReplaceThisWithBaseTestVisualBasic : CodeRefactoringTest + where TCodeRefactoring : CodeRefactoringProvider, new() + { + public override string Language => LanguageNames.VisualBasic; + + public override Type SyntaxKindType => typeof(VisualBasic.SyntaxKind); + + protected override string DefaultFileExt => "vb"; + + protected override CompilationOptions CreateCompilationOptions() + { + return new VisualBasic.VisualBasicCompilationOptions(OutputKind.DynamicallyLinkedLibrary); + } + + protected override ParseOptions CreateParseOptions() + { + return new VisualBasic.VisualBasicParseOptions(VisualBasic.LanguageVersion.Default, DocumentationMode.Diagnose); + } + + protected override IEnumerable GetCodeRefactoringProviders() + { + yield return new TCodeRefactoring(); + } + } } } diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/Microsoft.CodeAnalysis.Testing.Utilities.csproj b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/Microsoft.CodeAnalysis.Testing.Utilities.csproj index 614d81005..184f5e0fe 100644 --- a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/Microsoft.CodeAnalysis.Testing.Utilities.csproj +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/Microsoft.CodeAnalysis.Testing.Utilities.csproj @@ -36,5 +36,6 @@ + diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/TestAnalyzers/LiteralUnderFiveAnalyzer.cs b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/TestAnalyzers/LiteralUnderFiveAnalyzer.cs new file mode 100644 index 000000000..e852b0a6a --- /dev/null +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/TestAnalyzers/LiteralUnderFiveAnalyzer.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.CodeAnalysis.Testing.TestAnalyzers +{ + /// + /// Reports a diagnostic on any integer literal with a value less than five. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] + public class LiteralUnderFiveAnalyzer : DiagnosticAnalyzer + { + internal const string CurrentValueProperty = nameof(CurrentValueProperty); + + internal static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor("LiteralUnderFive", "title", "message", "category", DiagnosticSeverity.Warning, isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterOperationAction(HandleLiteralOperation, OperationKind.Literal); + } + + private void HandleLiteralOperation(OperationAnalysisContext context) + { + var operation = (ILiteralOperation)context.Operation; + if (operation.ConstantValue.HasValue + && operation.ConstantValue.Value is int value + && value < 5) + { + var properties = ImmutableDictionary.Empty + .Add(CurrentValueProperty, value.ToString()); + context.ReportDiagnostic(Diagnostic.Create(Descriptor, operation.Syntax.GetLocation(), properties)); + } + } + } +} diff --git a/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/TestFixes/IncrementFix.cs b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/TestFixes/IncrementFix.cs new file mode 100644 index 000000000..f7d0765e8 --- /dev/null +++ b/tests/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Testing.Utilities/TestFixes/IncrementFix.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Testing.TestAnalyzers; +using Microsoft.CodeAnalysis.Text; +using Xunit; + +namespace Microsoft.CodeAnalysis.Testing.TestFixes +{ + [ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic)] + [PartNotDiscoverable] + public class IncrementFix : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(LiteralUnderFiveAnalyzer.Descriptor.Id); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + foreach (var diagnostic in context.Diagnostics) + { + context.RegisterCodeFix( + CodeAction.Create( + "LiteralUnderFive", + cancellationToken => CreateChangedDocument(context.Document, diagnostic.Location.SourceSpan, cancellationToken), + $"{nameof(IncrementFix)}:{int.Parse(diagnostic.Properties[LiteralUnderFiveAnalyzer.CurrentValueProperty]!) + 1}"), + diagnostic); + } + + return Task.CompletedTask; + } + + private async Task CreateChangedDocument(Document document, TextSpan sourceSpan, CancellationToken cancellationToken) + { + var tree = (await document.GetSyntaxTreeAsync(cancellationToken))!; + var root = await tree.GetRootAsync(cancellationToken); + var token = root.FindToken(sourceSpan.Start); + var replacement = int.Parse(token.ValueText) + 1; + var generator = SyntaxGenerator.GetGenerator(document); + + var originalLeadingTrivia = token.LeadingTrivia; + SyntaxTriviaList newLeadingTrivia; + Assert.Equal(0, originalLeadingTrivia.Count); + if (document.Project.Language == LanguageNames.CSharp) + { + newLeadingTrivia = CSharp.SyntaxFactory.TriviaList(CSharp.SyntaxFactory.Space); + } + else + { + newLeadingTrivia = VisualBasic.SyntaxFactory.TriviaList(VisualBasic.SyntaxFactory.Space); + } + + var newExpression = generator.LiteralExpression(replacement).WithLeadingTrivia(newLeadingTrivia).WithTrailingTrivia(token.TrailingTrivia); + return document.WithSyntaxRoot(root.ReplaceNode(token.Parent!, newExpression)); + } + } +}