├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github ├── actions │ └── dotnet-test │ │ └── action.yml ├── copilot-instructions.md ├── dependabot.yml └── workflows │ ├── dotnet.yml │ └── publish.yml ├── .gitignore ├── .vscode ├── settings.json └── tasks.json ├── BasicCompilerLog.sln ├── LICENSE ├── README.md ├── docs ├── images │ ├── debug-rsp-1.png │ └── debug-rsp-2.png └── investigating.md ├── notes.txt └── src ├── Basic.CompilerLog.UnitTests ├── AppDomainUtils.cs ├── AssertEx.cs ├── Basic.CompilerLog.UnitTests.csproj ├── BasicAnalyzerHostTests.cs ├── BinaryLogReaderTests.cs ├── BinaryLogUtilTests.cs ├── CodeAnalysisExtensionTests.cs ├── CommonUtilTests.cs ├── CompilationDataTests.cs ├── CompilerCallReaderUtilTests.cs ├── CompilerCallTests.cs ├── CompilerLogBuilderTests.cs ├── CompilerLogFixture.cs ├── CompilerLogReaderExTests.cs ├── CompilerLogReaderTests.cs ├── CompilerLogUtilTests.cs ├── ConditionalFacts.cs ├── ExportUtilTests.cs ├── Extensions.cs ├── ExtensionsTests.cs ├── FilterOptionSetTests.cs ├── FixtureBase.cs ├── InMemoryLoaderTests.cs ├── LibraryUtil.cs ├── LogReaderStateTests.cs ├── MetadataTests.cs ├── PathNormalizationUtilTests.cs ├── PathUtilTests.cs ├── PolyfillTests.cs ├── ProgramTests.cs ├── Properties.cs ├── ResilientDirectoryTests.cs ├── ResourceLoader.cs ├── Resources │ ├── Key.snk │ ├── MetadataVersion1 │ │ └── console.complog │ ├── MetadataVersion2 │ │ └── console.complog │ ├── linux-console.complog │ └── windows-console.complog ├── RoslynUtilTests.cs ├── SdkUtilTests.cs ├── SolutionFixture.cs ├── SolutionReaderTests.cs ├── StringStreamTests.cs ├── TempDir.cs ├── TestBase.cs ├── TestUtil.cs ├── UnitTestsTests.cs ├── UsingAllCompilerLogTests.cs └── xunit.runner.json ├── Basic.CompilerLog.Util ├── AssemblyIdentityData.cs ├── BannedSymbols.txt ├── Basic.CompilerLog.Util.csproj ├── BasicAnalyzerHost.cs ├── BinaryLogReader.cs ├── BinaryLogUtil.cs ├── CodeAnalysisExtensions.cs ├── CommonUtil.cs ├── CompilationData.cs ├── CompilerAssemblyData.cs ├── CompilerCall.cs ├── CompilerCallData.cs ├── CompilerCallReaderUtil.cs ├── CompilerLogBuilder.cs ├── CompilerLogException.cs ├── CompilerLogReader.cs ├── CompilerLogTextLoader.cs ├── CompilerLogUtil.cs ├── Data.cs ├── EmitData.cs ├── ExportUtil.cs ├── Extensions.cs ├── IBasicAnalyzerHostDataProvider.cs ├── ICompilerCallReader.cs ├── Impl │ ├── BasicAdditionalTextFile.cs │ ├── BasicAnalyzerConfigOptionsProvider.cs │ ├── BasicAnalyzerHostInMemory.cs │ ├── BasicAnalyzerHostNone.cs │ ├── BasicAnalyzerHostOnDisk.cs │ └── BasicSyntaxTreeOptionsProvider.cs ├── LogReaderState.cs ├── Metadata.cs ├── MiscDirectory.cs ├── PathNormalizationUtil.cs ├── Polyfill.cs ├── Properties.cs ├── RawCompilationData.cs ├── ReflectionUtil.cs ├── ResilientDirectory.cs ├── RoslynUtil.cs ├── SdkUtil.cs ├── Serialize │ ├── MessagePackTypes.cs │ └── MessagePackUtil.cs ├── SolutionReader.cs ├── SourceTextData.cs └── StringStream.cs ├── Basic.CompilerLog ├── Basic.CompilerLog.csproj ├── Constants.cs ├── Extensions.cs ├── FilterOptionSet.cs ├── Program.cs ├── Properties.cs └── Properties │ └── launchSettings.json ├── Directory.Build.props ├── Directory.Packages.props ├── Scratch ├── CompilerBenchmarks.cs ├── Properties │ └── launchSettings.json ├── Scratch.cs └── Scratch.csproj ├── Shared ├── DotnetUtil.cs ├── PathUtil.cs └── ProcessUtil.cs └── key.snk /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/dotnet:8.0 2 | 3 | RUN wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh && \ 4 | chmod +x dotnet-install.sh && \ 5 | ./dotnet-install.sh --channel 9.0 --version latest --install-dir /usr/share/dotnet && \ 6 | rm dotnet-install.sh 7 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/dotnet 3 | { 4 | "name": "C# (.NET)", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | // "image": "mcr.microsoft.com/devcontainers/dotnet:1-8.0-bookworm", 7 | "build": { 8 | "dockerfile": "Dockerfile" 9 | }, 10 | "features": { 11 | "ghcr.io/devcontainers-contrib/features/bash-command:1": {}, 12 | "ghcr.io/devcontainers/features/sshd:1": { 13 | "version": "latest" 14 | } 15 | }, 16 | 17 | // Features to add to the dev container. More info: https://containers.dev/features. 18 | // "features": {}, 19 | 20 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 21 | // "forwardPorts": [5000, 5001], 22 | // "portsAttributes": { 23 | // "5001": { 24 | // "protocol": "https" 25 | // } 26 | // } 27 | 28 | // Use 'postCreateCommand' to run commands after the container is created. 29 | // "postCreateCommand": "dotnet restore", 30 | 31 | // Configure tool-specific properties. 32 | "customizations": { 33 | "vscode": { 34 | "extensions": [ 35 | "ms-dotnettools.csharp", 36 | "ms-dotnettools.csdevkit" 37 | ] 38 | } 39 | } 40 | 41 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 42 | // "remoteUser": "root" 43 | } 44 | -------------------------------------------------------------------------------- /.github/actions/dotnet-test/action.yml: -------------------------------------------------------------------------------- 1 | name: 'dotnet test' 2 | description: 'Wrap dotnet test command with common arguments' 3 | author: 'Jared Parsons ' 4 | inputs: 5 | name: 6 | description: 'Name of the test run' 7 | required: true 8 | framework: 9 | description: 'Target framework to test' 10 | required: true 11 | test-results-dir: 12 | description: 'Path to store the test results' 13 | required: true 14 | test-coverage-dir: 15 | description: 'Path to store the test coverage' 16 | required: true 17 | 18 | runs: 19 | using: 'composite' 20 | steps: 21 | - name: Test $${ inputs.name } 22 | run: > 23 | dotnet test --no-build --framework ${{ inputs.framework }} 24 | --blame-hang --blame-hang-dump-type full --blame-hang-timeout 10m 25 | --results-directory ${{ inputs.test-results-dir }} 26 | --logger "console;verbosity=detailed" 27 | --logger "trx;LogFileName=TestResults-${{ inputs.name }}.trx" 28 | -p:CollectCoverage=true -p:CoverletOutputFormat=cobertura -p:CoverletOutput=${{ inputs.test-coverage-dir }}/coverage.${{ inputs.name }}.xml 29 | shell: pwsh 30 | env: 31 | DOTNET_DbgEnableMiniDump: 1 32 | DOTNET_DbgMiniDumptype: 2 33 | DOTNET_CreateDumpLogToFile: ${{ inputs.test-results-dir }}test-%e-%p.dmp 34 | 35 | -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | 2 | Use the following code styles: 3 | - Use `var` for local variables when the type of the initializer is intuitive 4 | - Use the suffix `Async` for `async` methods -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | groups: 13 | all-dependencies: 14 | patterns: 15 | - "*" # This groups all dependencies into a single PR 16 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: Build and Test 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | paths-ignore: 10 | - 'doc/**' 11 | - 'README.md' 12 | pull_request: 13 | branches: [ "main" ] 14 | paths-ignore: 15 | - 'doc/**' 16 | - 'README.md' 17 | 18 | permissions: 19 | checks: write 20 | 21 | jobs: 22 | build: 23 | strategy: 24 | matrix: 25 | os: [windows-latest, ubuntu-latest] 26 | include: 27 | - os: windows-latest 28 | artifact: windows.complog 29 | slash: \ 30 | test-results: test-results-windows 31 | - os: ubuntu-latest 32 | artifact: ubuntu.complog 33 | slash: / 34 | test-results: test-results-linux 35 | 36 | env: 37 | TEST_ARTIFACTS_PATH: ${{ github.workspace }}${{ matrix.slash }}artifacts${{ matrix.slash }}test-artifacts 38 | TEST_RESULTS_PATH: ${{ github.workspace }}${{ matrix.slash }}artifacts${{ matrix.slash }}test-results 39 | TEST_COVERAGE_PATH: ${{ github.workspace }}${{ matrix.slash }}artifacts${{ matrix.slash }}coverage 40 | 41 | timeout-minutes: 30 42 | 43 | name: Build and Test ${{ matrix.os }} 44 | runs-on: ${{ matrix.os }} 45 | 46 | steps: 47 | 48 | # Setup the output directories for usage 49 | - name: Create directories 50 | shell: pwsh 51 | run: New-Item -Type Directory -Path @("${{ env.TEST_ARTIFACTS_PATH }}", "${{ env.TEST_RESULTS_PATH }}", "${{ env.TEST_COVERAGE_PATH }}") 52 | 53 | - uses: actions/checkout@v3 54 | - name: Setup .NET 55 | uses: actions/setup-dotnet@v3 56 | with: 57 | dotnet-version: | 58 | 8.0.x 59 | 9.0.x 60 | 61 | - name: List .NET Runtimes 62 | run: dotnet --list-runtimes 63 | 64 | - name: Restore dependencies 65 | run: dotnet restore 66 | - name: Build 67 | run: dotnet build --no-restore -bl 68 | 69 | - name: Test Linux 70 | uses: ./.github/actions/dotnet-test 71 | with: 72 | name: 'Test-Linux' 73 | framework: 'net9.0' 74 | test-results-dir: ${{ env.TEST_RESULTS_PATH }} 75 | test-coverage-dir: ${{ env.TEST_COVERAGE_PATH }} 76 | if: matrix.os == 'ubuntu-latest' 77 | 78 | - name: Test Windows .NET Core 79 | uses: ./.github/actions/dotnet-test 80 | with: 81 | name: 'Test-Windows-Core' 82 | framework: 'net9.0' 83 | test-results-dir: ${{ env.TEST_RESULTS_PATH }} 84 | test-coverage-dir: ${{ env.TEST_COVERAGE_PATH }} 85 | if: matrix.os == 'windows-latest' 86 | 87 | - name: Test Windows Framework 88 | uses: ./.github/actions/dotnet-test 89 | with: 90 | name: 'Test-Windows-Framework' 91 | framework: 'net472' 92 | test-results-dir: ${{ env.TEST_RESULTS_PATH }} 93 | test-coverage-dir: ${{ env.TEST_COVERAGE_PATH }} 94 | if: matrix.os == 'windows-latest' 95 | 96 | - name: Create Compiler Log 97 | run: dotnet run --framework net8.0 --project src/Basic.CompilerLog/Basic.CompilerLog.csproj create msbuild.binlog 98 | 99 | - name: Publish Compiler Log 100 | uses: actions/upload-artifact@v4 101 | with: 102 | name: ${{ matrix.artifact }} 103 | path: msbuild.complog 104 | 105 | - name: Upload coverage reports to Codecov 106 | uses: codecov/codecov-action@v3 107 | with: 108 | token: ${{ secrets.CODECOV_TOKEN }} 109 | directory: ${{ env.TEST_COVERAGE_PATH }} 110 | 111 | - name: Publish Test Results 112 | uses: actions/upload-artifact@v4 113 | if: always() 114 | with: 115 | name: ${{ matrix.test-results }} 116 | path: ${{ env.TEST_RESULTS_PATH }} 117 | 118 | - name: Publish Test Artifacts 119 | uses: actions/upload-artifact@v4 120 | if: always() 121 | with: 122 | name: Test Artifacts ${{ matrix.os }} 123 | path: ${{ env.TEST_ARTIFACTS_PATH }} 124 | 125 | test-report: 126 | name: Produce Test Report 127 | needs: build 128 | runs-on: ubuntu-latest 129 | if: always() 130 | steps: 131 | - uses: dorny/test-reporter@v1 132 | with: 133 | artifact: /test-results-(.*)/ 134 | name: .NET Test Results 135 | path: '*.trx' 136 | reporter: dotnet-trx 137 | 138 | 139 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish NuGet Packages 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: 'Package Version' 7 | required: true 8 | default: '' 9 | 10 | jobs: 11 | publish: 12 | name: Publish NuGet 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v3 18 | with: 19 | dotnet-version: | 20 | 8.0.x 21 | 9.0.x 22 | 23 | - name: Restore dependencies 24 | run: dotnet restore 25 | 26 | - name: Build 27 | run: dotnet build -c Release 28 | 29 | - name: Pack Basic.CompilerLog.Util 30 | run: dotnet pack --no-build -p:IncludeSymbols=false -p:RepositoryCommit=${GITHUB_SHA} -p:PackageVersion="${{ github.event.inputs.version }}" -c Release src/Basic.CompilerLog.Util/Basic.CompilerLog.Util.csproj -o . 31 | 32 | - name: Pack Basic.CompilerLog 33 | run: dotnet pack --no-build -p:IncludeSymbols=false -p:RepositoryCommit=${GITHUB_SHA} -p:PackageVersion="${{ github.event.inputs.version }}" -c Release src/Basic.CompilerLog/Basic.CompilerLog.csproj -o . 34 | 35 | - name: Publish NuPkg Files 36 | run: dotnet nuget push "*.nupkg" -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json 37 | 38 | - name: Create Tag and Release 39 | id: create_release 40 | uses: actions/create-release@v1 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | with: 44 | tag_name: v${{ github.event.inputs.version }} 45 | release_name: Release v${{ github.event.inputs.version }} 46 | body: | 47 | Create release ${{ github.event.inputs.version }} 48 | draft: false 49 | prerelease: false -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotnet-test-explorer.testProjectPath": "**/*UnitTests.csproj", 3 | "dotnet.defaultSolution": "BasicCompilerLog.sln", 4 | "dotnet.testWindow.disableAutoDiscovery": false, 5 | "cSpell.words": [ 6 | "appconfig", 7 | "binlog", 8 | "cacheable", 9 | "classlib", 10 | "classlibmulti", 11 | "complog", 12 | "Complogs", 13 | "cryptodir", 14 | "cryptokeyfile", 15 | "inmemory", 16 | "jaredpar", 17 | "msbuild", 18 | "Mvid", 19 | "NET", 20 | "netstandard", 21 | "ondisk", 22 | "Relogger", 23 | "ruleset", 24 | "Xunit" 25 | ] 26 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/BasicCompilerLog.sln", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile", 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "label": "publish", 22 | "command": "dotnet", 23 | "type": "process", 24 | "args": [ 25 | "publish", 26 | "${workspaceFolder}/src/Basic.CompilerLog/Basic.CompilerLog.csproj", 27 | "/property:GenerateFullPaths=true", 28 | "/consoleloggerparameters:NoSummary" 29 | ], 30 | "problemMatcher": "$msCompile" 31 | }, 32 | { 33 | "label": "watch", 34 | "command": "dotnet", 35 | "type": "process", 36 | "args": [ 37 | "watch", 38 | "run", 39 | "--project", 40 | "${workspaceFolder}/src/Basic.CompilerLog/Basic.CompilerLog.csproj" 41 | ], 42 | "problemMatcher": "$msCompile" 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /BasicCompilerLog.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32616.157 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Basic.CompilerLog.Util", "src\Basic.CompilerLog.Util\Basic.CompilerLog.Util.csproj", "{3E6234A8-1FD7-444C-9CE4-C0AB7E9E1031}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scratch", "src\Scratch\Scratch.csproj", "{C0F2CBDC-C782-4CE7-86A5-CC8E0A172887}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Basic.CompilerLog", "src\Basic.CompilerLog\Basic.CompilerLog.csproj", "{3A77B0E1-F383-4663-BA36-A1855E7624DC}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Basic.CompilerLog.UnitTests", "src\Basic.CompilerLog.UnitTests\Basic.CompilerLog.UnitTests.csproj", "{FFA87128-16E5-440F-8335-28354C2EE2C1}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {3E6234A8-1FD7-444C-9CE4-C0AB7E9E1031}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {3E6234A8-1FD7-444C-9CE4-C0AB7E9E1031}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {3E6234A8-1FD7-444C-9CE4-C0AB7E9E1031}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {3E6234A8-1FD7-444C-9CE4-C0AB7E9E1031}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {C0F2CBDC-C782-4CE7-86A5-CC8E0A172887}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {C0F2CBDC-C782-4CE7-86A5-CC8E0A172887}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {C0F2CBDC-C782-4CE7-86A5-CC8E0A172887}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {C0F2CBDC-C782-4CE7-86A5-CC8E0A172887}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {3A77B0E1-F383-4663-BA36-A1855E7624DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {3A77B0E1-F383-4663-BA36-A1855E7624DC}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {3A77B0E1-F383-4663-BA36-A1855E7624DC}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {3A77B0E1-F383-4663-BA36-A1855E7624DC}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {FFA87128-16E5-440F-8335-28354C2EE2C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {FFA87128-16E5-440F-8335-28354C2EE2C1}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {FFA87128-16E5-440F-8335-28354C2EE2C1}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {FFA87128-16E5-440F-8335-28354C2EE2C1}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {237FF09A-E587-485D-9894-30D24BACA531} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jared Parsons 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Compiler Logs 2 | 3 | [![codecov](https://codecov.io/gh/jaredpar/complog/graph/badge.svg?token=MIM7Y2JZ5G)](https://codecov.io/gh/jaredpar/complog) 4 | 5 | This is the repository for creating and consuming compiler log files. These are files created from a [MSBuild binary log](https://github.com/KirillOsenkov/MSBuildStructuredLog) that contain information necessary to recreate all of the [Compilation](https://docs.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.compilation?view=roslyn-dotnet-4.2.0) instances from that build. 6 | 7 | The compiler log files are self contained. They must be created on the same machine where the binary log was created but after creation they can be freely copied between machines. That enables a number of scenarios: 8 | 9 | 1. GitHub pipelines can cleanly separate build and build analysis into different legs. The analysis can be done on a separate machine entirely independent of where the build happens. 10 | 1. Allows for easier customer investigations by the C# / VB compiler teams. Instead of trying to re-create a customer build environment, customers can provide a compiler log file that developers can easily open with a call to the API. 11 | 12 | ## complog 13 | 14 | This global tool can be installed via 15 | 16 | > dotnet tool install --global complog 17 | 18 | From there the following commands are available: 19 | 20 | - `create`: create a complog file from an existing binary log 21 | - `replay`: replay the builds from the complog 22 | - `export`: export complete compilations to disk 23 | - `ref`: export references for a compilation to disk 24 | - `rsp`: generate rsp files for compilation events 25 | - `print`: print the summary of a complog on the command line 26 | 27 | ## Info 28 | :warning: A compiler log **will** include potentially sensitive artifacts :warning: 29 | 30 | A compiler log file contains all of the information necessary to recreate a `Compilation`. That includes all source, resources, references, strong name keys, etc .... That will be visible to anyone you provide a compiler log to. 31 | 32 | ## Creating Compiler Logs 33 | 34 | :warning: A compiler log **will** include artifacts like source code, references, etc ... :warning: 35 | 36 | There are a number of ways to create a compiler log. The first step is to install the `complog` global tool as that will be used to create the compiler log. 37 | 38 | ```cmd 39 | > dotnet tool install -g complog 40 | ``` 41 | 42 | The easiest is to create it off of a [binary log](https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Binary-Log.md) file from a previous build. 43 | 44 | ```cmd 45 | > msbuild -bl MySolution.sln 46 | > complog create msbuild.binlog 47 | ``` 48 | 49 | By default this will include every project in the binary log. If there are a lot of projects this can produce a large compiler log. You can use the `-p` option to limit the compiler log to a specific set of projects. 50 | 51 | ```cmd 52 | > complog create msbuild.binlog -p MyProject.csproj 53 | ``` 54 | 55 | For solutions or projects that can be built with `dotnet build` a compiler log can be created by just running `create` against the solution or project file directly. 56 | 57 | ```cmd 58 | > complog create MyProject.csproj 59 | ``` 60 | 61 | When trying to get a compiler log from a build that occurs in a GitHub action you can use the `complog-action` action to simplify creating and uploading the compiler log. 62 | 63 | ```yml 64 | - name: Build .NET app 65 | run: dotnet build -bl 66 | 67 | - name: Create and upload the compiler log 68 | uses: jaredpar/basic-compilerlog-action@v1 69 | with: 70 | binlog: msbuild.binlog 71 | ``` 72 | 73 | ## Debugging Compiler Logs 74 | 75 | ### Running locally 76 | 77 | To re-run all of the compilations in a compiler log use the `replay` command 78 | 79 | ```cmd 80 | > complog replay build.complog 81 | Microsoft.VisualStudio.IntegrationTest.IntegrationService.csproj (net472) ...Success 82 | Roslyn.Test.Performance.Utilities.csproj (net472) ...Success 83 | Microsoft.CodeAnalysis.XunitHook.csproj (net472) ...Success 84 | ``` 85 | 86 | Passing the `-export` argument will cause all failed compilations to be exported to the local disk for easy analysis. 87 | 88 | ### Debugging in Visual Studio 89 | 90 | To debug a compilation in Visual Studio first export it to disk: 91 | 92 | ```cmd 93 | > complog export build.complog 94 | ``` 95 | 96 | That will write out all the artifacts necessary to run a command line build to disk. Use the `--project` option to limit the output to specific projects. For each project it will generate a build.rsp command that uses the exported arguments. It will also generate several `build*.cmd` files. Those will execute `dotnet exec csc.dll @build.rsp` on the build for every SDK installed on the machine. 97 | 98 | ![example of export output](/docs/images/debug-rsp-1.png) 99 | 100 | The next step is to setup csc / vbc to use the build.rsp file for debugging. Open the debug settings for csc / vbc and set them to have the argument `@build.rsp` and make the working directory the location of that file. 101 | 102 | ![example of debug settnigs](/docs/images/debug-rsp-2.png) 103 | 104 | Then launch csc / vbc and it will debug that project. 105 | 106 | ## Using the API 107 | 108 | The samples below for creating Roslyn objects work for both compiler and binary logs. For a binary log though it must be done on the same machine where the build occurred. 109 | 110 | ### Creating a Compilation 111 | 112 | Log files can be used to recreate a `Compilation` instance. This is done by calling the `Compilation.Create` method and passing in the path to the compiler log file. 113 | 114 | ```csharp 115 | using var reader = CompilerCallReaderUtil.Create(logFilePath); 116 | foreach (var compilationData in reader.ReadAllCompilationData()) 117 | { 118 | var compilation = compilationData.GetCompilationAfterGenerators(); 119 | // ... 120 | } 121 | ``` 122 | 123 | ### Creating a Workspace 124 | 125 | The `SolutionReader` type can be used to create a `SolutionInfo` instance from the log file: 126 | 127 | ```csharp 128 | var reader = SolutionReader.Create(logFilePath); 129 | var workspace = new AdhocWorkspace(); 130 | var solution = workspace.AddSolution(reader.ReadSolutionInfo()); 131 | ``` 132 | -------------------------------------------------------------------------------- /docs/images/debug-rsp-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredpar/complog/8ef8225190174df2315f5e84b47f372679f9e82d/docs/images/debug-rsp-1.png -------------------------------------------------------------------------------- /docs/images/debug-rsp-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredpar/complog/8ef8225190174df2315f5e84b47f372679f9e82d/docs/images/debug-rsp-2.png -------------------------------------------------------------------------------- /docs/investigating.md: -------------------------------------------------------------------------------- 1 | # Investigating Issues 2 | 3 | ## AssemblyLoadContext won't fully unload 4 | 5 | The tests validate that we're able to fully unload an `AssemblyLoadContext` when disposing of 6 | `CompilerLogReader` and it's associated state. 7 | 8 | ### Getting a dump to investigate 9 | 10 | Set the windows registry so that it will create crash dumps when programs crash. This particular entry creates heap dumps (type 2) and retains up to 10 of them. 11 | 12 | ```reg 13 | [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps] 14 | "DumpFolder"="C:\\Users\\jaredpar\\temp\\dumps" 15 | "DumpCount"=dword:00000010 16 | "DumpType"=dword:00000002 17 | ``` 18 | 19 | Then change the test to crash on failure which will create a dump file: 20 | 21 | ```cs 22 | Environment.FailFast("The condition was hit"); 23 | ``` 24 | 25 | ### Investigating the failure 26 | 27 | Install `dotnet-sos` to get the location for sos in WinDbg 28 | 29 | ```cmd 30 | ⚡🔨 > dotnet-sos install 31 | Installing SOS to C:\Users\jaredpar\.dotnet\sos 32 | Installing over existing installation... 33 | Creating installation directory... 34 | Copying files from C:\Users\jaredpar\.dotnet\tools\.store\dotnet-sos\9.0.607501\dotnet-sos\9.0.607501\tools\net6.0\any\win-x64 35 | Copying files from C:\Users\jaredpar\.dotnet\tools\.store\dotnet-sos\9.0.607501\dotnet-sos\9.0.607501\tools\net6.0\any\lib 36 | Execute '.load C:\Users\jaredpar\.dotnet\sos\sos.dll' to load SOS in your Windows debugger. 37 | Cleaning up... 38 | SOS install succeeded 39 | ``` 40 | 41 | The type `LoaderAllocatorScout` is what will root the types keeping the `AssemblyLoadContext` alive. 42 | 43 | ```txt 44 | 0:004> !dumpheap -type LoaderAllocatorScout 45 | Address MT Size 46 | 01735d1e9b18 7ffc3b9b86f8 24 47 | 01735d8dc0f0 7ffc3b9b86f8 24 48 | 49 | Statistics: 50 | MT Count TotalSize Class Name 51 | 7ffc3b9b86f8 2 48 System.Reflection.LoaderAllocatorScout 52 | Total 2 objects, 48 bytes 53 | ``` 54 | 55 | Then run `!gcroot` on the instances to see what is rooting them. 56 | 57 | ```txt 58 | 0:004> !gcroot 1735d8dc0f0 59 | Caching GC roots, this may take a while. 60 | Subsequent runs of this command will be faster. 61 | 62 | Found 0 unique roots. 63 | 0:004> !gcroot 01735d1e9b18 64 | HandleTable: 65 | 000001735acc12c8 (strong handle) 66 | -> 01735b00ff88 System.Object[] 67 | -> 01735d1d9880 System.Lazy (static variable: MessagePack.MessagePackSerializerOptions.Options) 68 | -> 01735d1e9aa0 MessagePack.Internal.DynamicAssembly 69 | -> 01735d1e9ac0 System.Reflection.Emit.RuntimeAssemblyBuilder 70 | -> 01735d1e9b30 System.Reflection.RuntimeAssembly 71 | -> 01735d1e9ae8 System.Reflection.LoaderAllocator 72 | -> 01735d1e9b18 System.Reflection.LoaderAllocatorScout 73 | 74 | Found 1 unique roots. 75 | ``` -------------------------------------------------------------------------------- /notes.txt: -------------------------------------------------------------------------------- 1 | Converting looks like this 2 | 3 | public static PortableExecutableReference EmitToPortableExecutableReference( 4 | this Compilation comp, 5 | EmitOptions options = null, 6 | bool embedInteropTypes = false, 7 | ImmutableArray aliases = default, 8 | DiagnosticDescription[] expectedWarnings = null) 9 | { 10 | var image = comp.EmitToArray(options, expectedWarnings: expectedWarnings); 11 | if (comp.Options.OutputKind == OutputKind.NetModule) 12 | { 13 | return ModuleMetadata.CreateFromImage(image).GetReference(display: comp.MakeSourceModuleName()); 14 | } 15 | else 16 | { 17 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/AppDomainUtils.cs: -------------------------------------------------------------------------------- 1 | #if NETFRAMEWORK 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Xunit; 9 | 10 | namespace Basic.CompilerLog.UnitTests; 11 | 12 | public static class AppDomainUtils 13 | { 14 | private static readonly object s_lock = new object(); 15 | private static bool s_hookedResolve; 16 | 17 | public static AppDomain Create(string? name = null, string? basePath = null) 18 | { 19 | name = name ?? "Custom AppDomain"; 20 | basePath = basePath ?? Path.GetDirectoryName(typeof(AppDomainUtils).Assembly.Location); 21 | 22 | lock (s_lock) 23 | { 24 | if (!s_hookedResolve) 25 | { 26 | AppDomain.CurrentDomain.AssemblyResolve += OnResolve; 27 | s_hookedResolve = true; 28 | } 29 | } 30 | 31 | return AppDomain.CreateDomain(name, null, new AppDomainSetup() 32 | { 33 | ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile, 34 | ApplicationBase = basePath 35 | }); 36 | } 37 | 38 | /// 39 | /// When run under xunit without AppDomains all DLLs get loaded via the AssemblyResolve 40 | /// event. In some cases the xunit, AppDomain marshalling, xunit doesn't fully hook 41 | /// the event and we need to do it for our assemblies. 42 | /// 43 | private static Assembly? OnResolve(object sender, ResolveEventArgs e) 44 | { 45 | var assemblyName = new AssemblyName(e.Name); 46 | var fullPath = Path.Combine( 47 | Path.GetDirectoryName(typeof(AppDomainUtils).Assembly.Location), 48 | assemblyName.Name + ".dll"); 49 | if (File.Exists(fullPath)) 50 | { 51 | return Assembly.LoadFrom(fullPath); 52 | } 53 | 54 | return null; 55 | } 56 | } 57 | 58 | public sealed class AppDomainTestOutputHelper : MarshalByRefObject, ITestOutputHelper 59 | { 60 | public ITestOutputHelper TestOutputHelper { get; } 61 | 62 | public string Output => TestOutputHelper.Output; 63 | 64 | public AppDomainTestOutputHelper(ITestOutputHelper testOutputHelper) 65 | { 66 | TestOutputHelper = testOutputHelper; 67 | } 68 | 69 | public void Write(string message) => 70 | TestOutputHelper.Write(message); 71 | 72 | public void WriteLine(string message) => 73 | TestOutputHelper.WriteLine(message); 74 | 75 | public void WriteLine(string format, params object[] args) => 76 | TestOutputHelper.WriteLine(format, args); 77 | 78 | public void Write(string format, params object[] args) => 79 | TestOutputHelper.Write(format, args); 80 | } 81 | 82 | public sealed class InvokeUtil : MarshalByRefObject 83 | { 84 | private readonly CancellationTokenSource _cts = new CancellationTokenSource(); 85 | 86 | internal void Cancel() 87 | { 88 | _cts.Cancel(); 89 | } 90 | 91 | internal void Invoke(string typeName, string methodName, ITestOutputHelper testOutputHelper, T state) 92 | { 93 | var type = typeof(AppDomainUtils).Assembly.GetType(typeName, throwOnError: false)!; 94 | var member = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance)!; 95 | 96 | // A static lambda will still be an instance method so we need to create the closure 97 | // here. 98 | var obj = member.IsStatic 99 | ? null 100 | : type.Assembly.CreateInstance(typeName); 101 | 102 | try 103 | { 104 | member.Invoke(obj, [testOutputHelper, state, _cts.Token]); 105 | } 106 | catch (TargetInvocationException ex) 107 | { 108 | throw new Exception(ex.InnerException.Message); 109 | } 110 | } 111 | } 112 | 113 | #endif 114 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/AssertEx.cs: -------------------------------------------------------------------------------- 1 | 2 | using Basic.CompilerLog.Util; 3 | using Xunit; 4 | 5 | namespace Basic.CompilerLog.UnitTests; 6 | 7 | internal static class AssertEx 8 | { 9 | internal static void HasData(MemoryStream? stream) 10 | { 11 | Assert.NotNull(stream); 12 | Assert.True(stream.Length > 0); 13 | } 14 | 15 | internal static void Success(ITestOutputHelper testOutputHelper, T emitResult) 16 | where T : struct, IEmitResult 17 | { 18 | if (!emitResult.Success) 19 | { 20 | foreach (var diagnostic in emitResult.Diagnostics) 21 | { 22 | testOutputHelper.WriteLine(diagnostic.ToString()); 23 | } 24 | } 25 | 26 | Assert.True(emitResult.Success); 27 | } 28 | 29 | /// 30 | /// Use this over Assert.Equal for collections as the error messages are more actionable 31 | /// 32 | /// 33 | internal static void SequenceEqual(IEnumerable expected, IEnumerable actual) 34 | { 35 | using var e1 = expected.GetEnumerator(); 36 | using var e2 = actual.GetEnumerator(); 37 | 38 | while (true) 39 | { 40 | if (!e1.MoveNext()) 41 | { 42 | Assert.False(e2.MoveNext()); 43 | break; 44 | } 45 | 46 | Assert.True(e2.MoveNext()); 47 | Assert.Equal(e1.Current, e2.Current); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/Basic.CompilerLog.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0;net472 5 | enable 6 | $(NoWarn);CS0436 7 | $(NoWarn);Nullable 8 | Exe 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | 22 | 23 | 24 | 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | all 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Key.snk 36 | 37 | 38 | MetadataVersion1.console.complog 39 | 40 | 41 | MetadataVersion2.console.complog 42 | 43 | 44 | linux-console.complog 45 | 46 | 47 | windows-console.complog 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/BasicAnalyzerHostTests.cs: -------------------------------------------------------------------------------- 1 | using Basic.CompilerLog.Util; 2 | using Basic.CompilerLog.Util.Impl; 3 | using Microsoft.CodeAnalysis.Text; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Collections.Immutable; 7 | using System.Diagnostics; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using Xunit; 12 | using System.Runtime.InteropServices; 13 | using Microsoft.CodeAnalysis.CSharp; 14 | using Xunit.Runner.Common; 15 | using Microsoft.CodeAnalysis; 16 | 17 | 18 | 19 | #if NET 20 | using System.Runtime.Loader; 21 | #endif 22 | 23 | namespace Basic.CompilerLog.UnitTests; 24 | 25 | [Collection(CompilerLogCollection.Name)] 26 | public sealed class BasicAnalyzerHostTests : TestBase 27 | { 28 | public CompilerLogFixture Fixture { get; } 29 | 30 | public BasicAnalyzerHostTests(ITestOutputHelper testOutputHelper, ITestContextAccessor testContextAccessor, CompilerLogFixture fixture) 31 | : base(testOutputHelper, testContextAccessor, nameof(CompilerLogReaderTests)) 32 | { 33 | Fixture = fixture; 34 | } 35 | 36 | [Fact] 37 | public void Supported() 38 | { 39 | Assert.True(BasicAnalyzerHost.IsSupported(BasicAnalyzerKind.OnDisk)); 40 | Assert.True(BasicAnalyzerHost.IsSupported(BasicAnalyzerKind.None)); 41 | #if NET 42 | Assert.True(BasicAnalyzerHost.IsSupported(BasicAnalyzerKind.InMemory)); 43 | #else 44 | Assert.False(BasicAnalyzerHost.IsSupported(BasicAnalyzerKind.InMemory)); 45 | #endif 46 | 47 | // To make sure this test is updated every time a new value is added 48 | Assert.Equal(3, Enum.GetValues(typeof(BasicAnalyzerKind)).Length); 49 | } 50 | 51 | [Fact] 52 | public void NoneDispose() 53 | { 54 | var host = new BasicAnalyzerHostNone([]); 55 | host.Dispose(); 56 | Assert.Throws(() => { _ = host.AnalyzerReferences; }); 57 | } 58 | 59 | [Fact] 60 | public void NoneProps() 61 | { 62 | var host = new BasicAnalyzerHostNone([]); 63 | host.Dispose(); 64 | Assert.Equal(BasicAnalyzerKind.None, host.Kind); 65 | Assert.Empty(host.GeneratedSourceTexts); 66 | } 67 | 68 | /// 69 | /// What happens when two separate generators produce files with the same name? 70 | /// 71 | [Fact] 72 | public void NoneConflictingFileNames() 73 | { 74 | var root = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) 75 | ? @"c:\code" 76 | : "/code"; 77 | var sourceText1 = SourceText.From("// file 1", CommonUtil.ContentEncoding); 78 | var sourceText2 = SourceText.From("// file 2", CommonUtil.ContentEncoding); 79 | List<(SourceText SourceText, string FilePath)> generatedTexts = 80 | [ 81 | (sourceText1, Path.Combine(root, "file.cs")), 82 | (sourceText2, Path.Combine(root, "file.cs")), 83 | ]; 84 | var generator = new BasicGeneratedFilesAnalyzerReference(generatedTexts); 85 | var compilation = CSharpCompilation.Create( 86 | "example", 87 | [], 88 | Basic.Reference.Assemblies.Net90.References.All); 89 | var driver = CSharpGeneratorDriver.Create([generator!]); 90 | driver.RunGeneratorsAndUpdateCompilation(compilation, out var compilation2, out var diagnostics, CancellationToken); 91 | Assert.Empty(diagnostics); 92 | var syntaxTrees = compilation2.SyntaxTrees.ToList(); 93 | Assert.Equal(2, syntaxTrees.Count); 94 | Assert.Equal("// file 1", syntaxTrees[0].ToString()); 95 | Assert.EndsWith("file.cs", syntaxTrees[0].FilePath); 96 | Assert.Equal("// file 2", syntaxTrees[1].ToString()); 97 | Assert.EndsWith("file.cs", syntaxTrees[1].FilePath); 98 | Assert.NotEqual(syntaxTrees[0].FilePath, syntaxTrees[1].FilePath); 99 | } 100 | 101 | [Fact] 102 | public void Error() 103 | { 104 | var diagnostic = Diagnostic.Create(RoslynUtil.ErrorReadingGeneratedFilesDiagnosticDescriptor, Location.None, "message"); 105 | var host = new BasicAnalyzerHostNone(diagnostic); 106 | var analyzerReferences = host.AnalyzerReferences.Single(); 107 | var list = new List(); 108 | var bar = analyzerReferences.AsBasicAnalyzerReference(); 109 | _ = bar.GetAnalyzers(LanguageNames.CSharp, list); 110 | Assert.Equal([diagnostic], list); 111 | list.Clear(); 112 | _ = bar.GetGenerators(LanguageNames.CSharp, list); 113 | Assert.Empty(list); 114 | } 115 | 116 | #if NETFRAMEWORK 117 | [Fact] 118 | public void InMemory() 119 | { 120 | using var reader = CompilerLogReader.Create(Fixture.Console.Value.CompilerLogPath, BasicAnalyzerKind.InMemory); 121 | var data = reader.ReadCompilationData(0); 122 | var host = (BasicAnalyzerHostInMemory)data.BasicAnalyzerHost; 123 | Assert.NotEmpty(host.AnalyzerReferences); 124 | Assert.Throws(() => host.Loader.LoadFromAssemblyName(typeof(BasicAnalyzerHost).Assembly.GetName())); 125 | } 126 | #endif 127 | } 128 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/BinaryLogUtilTests.cs: -------------------------------------------------------------------------------- 1 | using Basic.CompilerLog.Util; 2 | using Basic.CompilerLog.Util.Impl; 3 | using Microsoft.Build.Logging.StructuredLogger; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.Text; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Collections.Immutable; 9 | using System.Diagnostics; 10 | using System.Linq; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | using Xunit; 14 | 15 | #if NET 16 | using System.Runtime.Loader; 17 | #endif 18 | 19 | namespace Basic.CompilerLog.UnitTests; 20 | 21 | public sealed class BinaryLogUtilTests 22 | { 23 | [Theory] 24 | [InlineData("dotnet exec csc.dll a.cs", "csc.dll", "a.cs")] 25 | [InlineData("dotnet.exe exec csc.dll a.cs", "csc.dll", "a.cs")] 26 | [InlineData("dotnet-can-be-any-host-name exec csc.dll a.cs", "csc.dll", "a.cs")] 27 | [InlineData("csc.exe a.cs b.cs", "csc.exe", "a.cs b.cs")] 28 | public void ParseCompilerAndArgumentsCsc(string inputArgs, string? expectedCompilerFilePath, string expectedArgs) 29 | { 30 | var (actualCompilerFilePath, actualArgs) = BinaryLogUtil.ParseTaskForCompilerAndArguments(inputArgs, "csc.exe", "csc.dll"); 31 | Assert.Equal(ToArray(expectedArgs), actualArgs); 32 | Assert.Equal(expectedCompilerFilePath, actualCompilerFilePath); 33 | static string[] ToArray(string arg) => arg.Split(new char[]{' '}, StringSplitOptions.RemoveEmptyEntries); 34 | } 35 | 36 | [WindowsTheory] 37 | [InlineData(@" C:\Program Files\dotnet\dotnet.exe exec ""C:\Program Files\dotnet\sdk\8.0.301\Roslyn\bincore\csc.dll"" a.cs", @"C:\Program Files\dotnet\sdk\8.0.301\Roslyn\bincore\csc.dll", "a.cs")] 38 | [InlineData(@"C:\Program Files\dotnet\dotnet.exe exec ""C:\Program Files\dotnet\sdk\8.0.301\Roslyn\bincore\csc.dll"" a.cs", @"C:\Program Files\dotnet\sdk\8.0.301\Roslyn\bincore\csc.dll", "a.cs")] 39 | [InlineData(@"""C:\Program Files\dotnet\dotnet.exe"" exec ""C:\Program Files\dotnet\sdk\8.0.301\Roslyn\bincore\csc.dll"" a.cs", @"C:\Program Files\dotnet\sdk\8.0.301\Roslyn\bincore\csc.dll", "a.cs")] 40 | [InlineData(@"'C:\Program Files\dotnet\dotnet.exe' exec ""C:\Program Files\dotnet\sdk\8.0.301\Roslyn\bincore\csc.dll"" a.cs", @"C:\Program Files\dotnet\sdk\8.0.301\Roslyn\bincore\csc.dll", "a.cs")] 41 | [InlineData(@"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\Roslyn\csc.exe a.cs b.cs", @"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\Roslyn\csc.exe", "a.cs b.cs")] 42 | [InlineData(@"""C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\Roslyn\csc.exe"" a.cs b.cs", @"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\Roslyn\csc.exe", "a.cs b.cs")] 43 | public void ParseCompilerAndArgumentsCscWindows(string inputArgs, string? expectedCompilerFilePath, string expectedArgs) 44 | { 45 | var (actualCompilerFilePath, actualArgs) = BinaryLogUtil.ParseTaskForCompilerAndArguments(inputArgs, "csc.exe", "csc.dll"); 46 | Assert.Equal(ToArray(expectedArgs), actualArgs); 47 | Assert.Equal(expectedCompilerFilePath, actualCompilerFilePath); 48 | static string[] ToArray(string arg) => arg.Split(new char[]{' '}, StringSplitOptions.RemoveEmptyEntries); 49 | } 50 | 51 | [UnixTheory] 52 | [InlineData(@"/dotnet/dotnet exec /dotnet/sdk/bincore/csc.dll a.cs", "/dotnet/sdk/bincore/csc.dll", "a.cs")] 53 | [InlineData(@"/dotnet/dotnet exec ""/dotnet/sdk/bincore/csc.dll"" a.cs", "/dotnet/sdk/bincore/csc.dll", "a.cs")] 54 | public void ParseCompilerAndArgumentsCscUnix(string inputArgs, string? expectedCompilerFilePath, string expectedArgs) 55 | { 56 | var (actualCompilerFilePath, actualArgs) = BinaryLogUtil.ParseTaskForCompilerAndArguments(inputArgs, "csc.exe", "csc.dll"); 57 | Assert.Equal(ToArray(expectedArgs), actualArgs); 58 | Assert.Equal(expectedCompilerFilePath, actualCompilerFilePath); 59 | static string[] ToArray(string arg) => arg.Split(new char[]{' '}, StringSplitOptions.RemoveEmptyEntries); 60 | } 61 | 62 | [Theory] 63 | [InlineData("dotnet.exe exec vbc.dll a.cs", "vbc.dll", "a.cs")] 64 | [InlineData("dotnet-can-be-any-host-name exec vbc.dll a.vb", "vbc.dll", "a.vb")] 65 | [InlineData("vbc.exe a.cs b.cs", "vbc.exe", "a.cs b.cs")] 66 | public void ParseCompilerAndArgumentsVbc(string inputArgs, string? expectedCompilerFilePath, string expectedArgs) 67 | { 68 | var (actualCompilerFilePath, actualArgs) = BinaryLogUtil.ParseTaskForCompilerAndArguments(inputArgs, "vbc.exe", "vbc.dll"); 69 | Assert.Equal(ToArray(expectedArgs), actualArgs); 70 | Assert.Equal(expectedCompilerFilePath, actualCompilerFilePath); 71 | static string[] ToArray(string arg) => arg.Split(new char[]{' '}, StringSplitOptions.RemoveEmptyEntries); 72 | } 73 | 74 | [Theory] 75 | [InlineData("dotnet not what we expect a.cs")] 76 | [InlineData("dotnet csc2 what we expect a.cs")] 77 | [InlineData("dotnet exec vbc.dll what we expect a.cs")] 78 | [InlineData("empty")] 79 | [InlineData(" ")] 80 | public void ParseCompilerAndArgumentsBad(string inputArgs) 81 | { 82 | Assert.Throws(() => BinaryLogUtil.ParseTaskForCompilerAndArguments(inputArgs, "csc.exe", "csc.dll")); 83 | } 84 | 85 | [Fact] 86 | public void ParseCompilerAndArgumentsNull() 87 | { 88 | var (actualCompilerFilePath, actualArgs) = BinaryLogUtil.ParseTaskForCompilerAndArguments(null, "csc.exe", "csc.dll"); 89 | Assert.Null(actualCompilerFilePath); 90 | Assert.Empty(actualArgs); 91 | } 92 | } 93 | 94 | public sealed class MSBuildProjectDataTests 95 | { 96 | [Fact] 97 | public void MSBuildProjectDataToString() 98 | { 99 | var evalData = new BinaryLogUtil.MSBuildProjectEvaluationData(@"example.csproj"); 100 | var data = new BinaryLogUtil.MSBuildProjectContextData(@"example.csproj", 100, 1); 101 | Assert.NotEmpty(data.ToString()); 102 | } 103 | } 104 | 105 | public sealed class CompilationTaskDataTests 106 | { 107 | internal BinaryLogUtil.MSBuildProjectEvaluationData EvaluationData { get; } 108 | internal BinaryLogUtil.MSBuildProjectContextData ContextData { get; } 109 | 110 | public CompilationTaskDataTests() 111 | { 112 | EvaluationData = new BinaryLogUtil.MSBuildProjectEvaluationData(@"example.csproj"); 113 | ContextData = new(@"example.csproj", 100, 1); 114 | } 115 | 116 | [Fact] 117 | public void TryCreateCompilerCallBadArguments() 118 | { 119 | var data = new BinaryLogUtil.CompilationTaskData(1, 1, true) 120 | { 121 | CommandLineArguments = "dotnet not a compiler call", 122 | }; 123 | 124 | Assert.Throws(() => data.TryCreateCompilerCall(ContextData.ProjectFile, null, CompilerCallKind.Unknown, ownerState: null)); 125 | } 126 | 127 | [Fact] 128 | public void TryCreateCompilerNoArguments() 129 | { 130 | var data = new BinaryLogUtil.CompilationTaskData(1, 1, true) 131 | { 132 | CommandLineArguments = null, 133 | }; 134 | 135 | Assert.Null(data.TryCreateCompilerCall(ContextData.ProjectFile, null, CompilerCallKind.Unknown, null)); 136 | } 137 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/CodeAnalysisExtensionTests.cs: -------------------------------------------------------------------------------- 1 | 2 | using Basic.CompilerLog.Util; 3 | using Basic.CompilerLog.Util.Impl; 4 | using Xunit; 5 | 6 | namespace Basic.CompilerLog.UnitTests; 7 | 8 | [Collection(CompilerLogCollection.Name)] 9 | public sealed class CodeAnalysisExtensionsTests : TestBase 10 | { 11 | public CompilerLogFixture Fixture { get; } 12 | 13 | public CodeAnalysisExtensionsTests(ITestOutputHelper testOutputHelper, ITestContextAccessor testContextAccessor, CompilerLogFixture fixture) 14 | : base(testOutputHelper, testContextAccessor, nameof(CompilationDataTests)) 15 | { 16 | Fixture = fixture; 17 | } 18 | 19 | [Fact] 20 | public void EmitToMemory() 21 | { 22 | var data = GetCompilationData(Fixture.ClassLib.Value.CompilerLogPath, basicAnalyzerKind: BasicAnalyzerKind.None); 23 | var compilation = data.GetCompilationAfterGenerators(CancellationToken); 24 | var result = compilation.EmitToMemory(EmitFlags.Default, cancellationToken: CancellationToken); 25 | AssertEx.Success(TestOutputHelper, result); 26 | AssertEx.HasData(result.AssemblyStream); 27 | Assert.Null(result.PdbStream); 28 | Assert.Null(result.XmlStream); 29 | Assert.Null(result.MetadataStream); 30 | 31 | result = compilation.EmitToMemory(EmitFlags.IncludePdbStream, cancellationToken: CancellationToken); 32 | AssertEx.Success(TestOutputHelper, result); 33 | AssertEx.HasData(result.AssemblyStream); 34 | AssertEx.HasData(result.PdbStream); 35 | Assert.Null(result.XmlStream); 36 | Assert.Null(result.MetadataStream); 37 | } 38 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/CommonUtilTests.cs: -------------------------------------------------------------------------------- 1 | 2 | using Basic.CompilerLog.Util; 3 | using Microsoft.CodeAnalysis.CSharp; 4 | using Xunit; 5 | #if NET 6 | using System.Runtime.Loader; 7 | #endif 8 | 9 | namespace Basic.CompilerLog.UnitTests; 10 | 11 | public sealed class CommonUtilTests 12 | { 13 | #if NET 14 | [Fact] 15 | public void GetAssemblyLoadContext() 16 | { 17 | var alc = new AssemblyLoadContext("Custom", isCollectible: true); 18 | Assert.Same(alc, CommonUtil.GetAssemblyLoadContext(alc)); 19 | alc.Unload(); 20 | } 21 | #endif 22 | 23 | [Fact] 24 | public void Defines() 25 | { 26 | #if NET 27 | Assert.True(TestBase.IsNetCore); 28 | Assert.False(TestBase.IsNetFramework); 29 | #else 30 | Assert.False(TestBase.IsNetCore); 31 | Assert.True(TestBase.IsNetFramework); 32 | #endif 33 | } 34 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/CompilerCallReaderUtilTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Compression; 2 | using Basic.CompilerLog.Util; 3 | using Xunit; 4 | 5 | namespace Basic.CompilerLog.UnitTests; 6 | 7 | [Collection(CompilerLogCollection.Name)] 8 | public sealed class CompilerCallReaderUtilTests : TestBase 9 | { 10 | public CompilerLogFixture Fixture { get; } 11 | 12 | public CompilerCallReaderUtilTests(ITestOutputHelper testOutputHelper, ITestContextAccessor testContextAccessor, CompilerLogFixture fixture) 13 | : base(testOutputHelper, testContextAccessor, nameof(CompilerLogReaderTests)) 14 | { 15 | Fixture = fixture; 16 | } 17 | 18 | [Fact] 19 | public void CreateBadExtension() 20 | { 21 | Assert.Throws(() => CompilerCallReaderUtil.Create("file.bad")); 22 | } 23 | 24 | [Fact] 25 | public void CreateFromZip() 26 | { 27 | Go(Fixture.Console.Value.CompilerLogPath, "build.complog"); 28 | Go(Fixture.Console.Value.BinaryLogPath!, "build.binlog"); 29 | 30 | void Go(string filePath, string entryName) 31 | { 32 | var d = Root.NewDirectory(); 33 | var zipFilePath = Path.Combine(d, "file.zip"); 34 | CreateZip(zipFilePath, filePath, entryName); 35 | 36 | using var reader = CompilerCallReaderUtil.Create(zipFilePath); 37 | var compilerCalls = reader.ReadAllCompilerCalls(); 38 | Assert.NotEmpty(compilerCalls); 39 | } 40 | 41 | void CreateZip(string zipFilePath, string logFilePath, string entryName) 42 | { 43 | using var fileStream = new FileStream(zipFilePath, FileMode.Create, FileAccess.Write, FileShare.None); 44 | using var zip = new ZipArchive(fileStream, ZipArchiveMode.Create); 45 | zip.CreateEntryFromFile(logFilePath, entryName); 46 | } 47 | } 48 | 49 | 50 | [Theory] 51 | [MemberData(nameof(GetBasicAnalyzerKinds))] 52 | public void GetAllAnalyzerKinds(BasicAnalyzerKind basicAnalyzerKind) 53 | { 54 | using var reader = CompilerCallReaderUtil.Create(Fixture.Console.Value.CompilerLogPath!, basicAnalyzerKind); 55 | Assert.Equal(basicAnalyzerKind, reader.BasicAnalyzerKind); 56 | } 57 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/CompilerCallTests.cs: -------------------------------------------------------------------------------- 1 | 2 | using Basic.CompilerLog.Util; 3 | using Xunit; 4 | 5 | namespace Basic.CompilerLog.UnitTests; 6 | 7 | public class CompilerCallTests 8 | { 9 | [Fact] 10 | public void GetDiagnosticNameNoTargetFramework() 11 | { 12 | var compilerCall = new CompilerCall("test.csproj"); 13 | Assert.Null(compilerCall.TargetFramework); 14 | Assert.Equal(compilerCall.ProjectFileName, compilerCall.GetDiagnosticName()); 15 | Assert.Empty(compilerCall.GetArguments()); 16 | } 17 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/CompilerLogBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using Basic.CompilerLog.Util; 2 | using Xunit; 3 | 4 | namespace Basic.CompilerLog.UnitTests; 5 | 6 | [Collection(SolutionFixtureCollection.Name)] 7 | public sealed class CompilerLogBuilderTests : TestBase 8 | { 9 | public SolutionFixture Fixture { get; } 10 | 11 | public CompilerLogBuilderTests(ITestOutputHelper testOutputHelper, ITestContextAccessor testContextAccessor, SolutionFixture fixture) 12 | : base(testOutputHelper, testContextAccessor, nameof(CompilerLogBuilderTests)) 13 | { 14 | Fixture = fixture; 15 | } 16 | 17 | /// 18 | /// We should be able to create log files that are resilient to artifacts missing on disk. Basically we can create 19 | /// a for this scenario, it will have diagnostics. 20 | /// 21 | [Fact] 22 | public void MissingFileSourceLink() 23 | { 24 | using var stream = new MemoryStream(); 25 | using var builder = new CompilerLogBuilder(stream, new()); 26 | using var binlogStream = new FileStream(Fixture.ConsoleWithDiagnosticsBinaryLogPath, FileMode.Open, FileAccess.Read, FileShare.Read); 27 | 28 | var compilerCall = BinaryLogUtil.ReadAllCompilerCalls(binlogStream).First(x => x.IsCSharp); 29 | compilerCall = compilerCall.WithArguments(["/sourcelink:does-not-exist.txt"]); 30 | builder.AddFromDisk(compilerCall, BinaryLogUtil.ReadCommandLineArgumentsUnsafe(compilerCall)); 31 | } 32 | 33 | [Fact] 34 | public void AddWithMissingCompilerFilePath() 35 | { 36 | using var stream = new MemoryStream(); 37 | using var builder = new CompilerLogBuilder(stream, new()); 38 | using var binlogStream = new FileStream(Fixture.ConsoleWithDiagnosticsBinaryLogPath, FileMode.Open, FileAccess.Read, FileShare.Read); 39 | 40 | var compilerCall = BinaryLogUtil.ReadAllCompilerCalls(binlogStream).First(x => x.IsCSharp); 41 | var args = compilerCall.GetArguments(); 42 | compilerCall = new CompilerCall( 43 | compilerCall.ProjectFilePath, 44 | targetFramework: compilerCall.TargetFramework, 45 | arguments: args.ToArray()); 46 | builder.AddFromDisk(compilerCall, BinaryLogUtil.ReadCommandLineArgumentsUnsafe(compilerCall)); 47 | } 48 | 49 | [Fact] 50 | public void PortablePdbMissing() 51 | { 52 | RunDotNet("new console -o ."); 53 | RunDotNet("build -bl:msbuild.binlog"); 54 | 55 | Directory 56 | .EnumerateFiles(RootDirectory, "*.pdb", SearchOption.AllDirectories) 57 | .ForEach(File.Delete); 58 | 59 | using var complogStream = new MemoryStream(); 60 | using var binlogStream = new FileStream(Path.Combine(RootDirectory, "msbuild.binlog"), FileMode.Open, FileAccess.Read, FileShare.Read); 61 | var diagnostics = CompilerLogUtil.ConvertBinaryLog(binlogStream, complogStream); 62 | Assert.Contains(diagnostics, x => x.Contains("Can't find portable pdb")); 63 | } 64 | 65 | [Fact] 66 | public void CloseTwice() 67 | { 68 | var builder = new CompilerLogBuilder(new MemoryStream(), []); 69 | builder.Close(); 70 | Assert.Throws(() => builder.Close()); 71 | } 72 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/CompilerLogReaderExTests.cs: -------------------------------------------------------------------------------- 1 | using Basic.CompilerLog.Util; 2 | using Basic.CompilerLog.Util.Impl; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp.Syntax; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.ComponentModel.Design.Serialization; 8 | using System.Data.Common; 9 | using System.Diagnostics; 10 | using System.IO.Pipelines; 11 | using System.Linq; 12 | using System.Runtime.InteropServices; 13 | #if NET 14 | using System.Runtime.Loader; 15 | #endif 16 | using System.Text; 17 | using System.Threading.Tasks; 18 | using Xunit; 19 | 20 | namespace Basic.CompilerLog.UnitTests; 21 | 22 | /// 23 | /// Similar to but using the 24 | /// instead. This allows for a lot of modding of the compiler log that lets us test corner 25 | /// cases. 26 | /// 27 | [Collection(SolutionFixtureCollection.Name)] 28 | public sealed class CompilerLogReaderExTests : TestBase 29 | { 30 | public SolutionFixture Fixture { get; } 31 | 32 | public CompilerLogReaderExTests(ITestOutputHelper testOutputHelper, ITestContextAccessor testContextAccessor, SolutionFixture fixture) 33 | : base(testOutputHelper, testContextAccessor, nameof(CompilerLogReaderTests)) 34 | { 35 | Fixture = fixture; 36 | } 37 | 38 | /// 39 | /// Convert the console binary log and return a reader over it 40 | /// 41 | private CompilerLogReader ConvertConsole(Func func, BasicAnalyzerKind? basicAnalyzerKind = null, List? diagnostics = null) => 42 | ChangeCompilerCall( 43 | Fixture.SolutionBinaryLogPath, 44 | x => x.ProjectFileName == "console.csproj", 45 | func, 46 | basicAnalyzerKind, 47 | diagnostics); 48 | 49 | private CompilerLogReader ConvertConsoleArgs(Func, IReadOnlyCollection> func, BasicAnalyzerKind? basicAnalyzerKind = null) => 50 | ConvertConsole(x => 51 | { 52 | var args = func(x.GetArguments()); 53 | return x.WithArguments(args); 54 | }, basicAnalyzerKind); 55 | 56 | [Fact] 57 | public void AnalyzerConfigNone() 58 | { 59 | var reader = ConvertConsoleArgs(args => 60 | args 61 | .Where(x => !x.StartsWith("/analyzerconfig:", StringComparison.Ordinal)) 62 | .ToArray()); 63 | var data = reader.ReadAllCompilationData().Single(); 64 | var optionsProvider = (BasicAnalyzerConfigOptionsProvider)data.AnalyzerOptions.AnalyzerConfigOptionsProvider; 65 | Assert.True(optionsProvider.IsEmpty); 66 | 67 | var syntaxProvider = (BasicSyntaxTreeOptionsProvider?)data.Compilation.Options.SyntaxTreeOptionsProvider; 68 | Assert.NotNull(syntaxProvider); 69 | Assert.False(syntaxProvider.IsEmpty); 70 | } 71 | 72 | [Theory] 73 | [InlineData("true", GeneratedKind.MarkedGenerated)] 74 | [InlineData("false", GeneratedKind.NotGenerated)] 75 | [InlineData("0", GeneratedKind.Unknown)] 76 | public void AnalyzerConfigGeneratedCode(string value, GeneratedKind expectedKind) 77 | { 78 | var text = $""" 79 | # C# files 80 | is_global = true 81 | generated_code = {value} 82 | """; 83 | 84 | var globalConfigFilePath = Root.NewFile(".editorconfig", text); 85 | var reader = ConvertConsoleArgs(args => 86 | args 87 | .Where(x => !x.StartsWith("/analyzerconfig:", StringComparison.Ordinal)) 88 | .Append($"/analyzerconfig:{globalConfigFilePath}") 89 | .ToArray()); 90 | 91 | var data = reader.ReadAllCompilationData().Single(); 92 | var syntaxProvider = (BasicSyntaxTreeOptionsProvider?)data.Compilation.Options.SyntaxTreeOptionsProvider; 93 | Assert.NotNull(syntaxProvider); 94 | 95 | var syntaxTree = data.Compilation.SyntaxTrees.First(); 96 | Assert.Equal(expectedKind, syntaxProvider.IsGenerated(syntaxTree, CancellationToken.None)); 97 | } 98 | 99 | [Theory] 100 | [MemberData(nameof(GetMissingFileArguments))] 101 | public async Task MissingFiles(string? option, string fileName, bool hasDiagnostics) 102 | { 103 | var diagnostics = new List(); 104 | var filePath = Path.Combine(RootDirectory, fileName); 105 | var prefix = option is null ? "" : $"/{option}:"; 106 | using var reader = ConvertConsole(x => x.WithAdditionalArguments([$"{prefix}{filePath}"]), BasicAnalyzerKind.None, diagnostics); 107 | Assert.Equal([RoslynUtil.GetMissingFileDiagnosticMessage(filePath)], diagnostics); 108 | var compilationData = reader.ReadAllCompilationData().Single(); 109 | if (hasDiagnostics) 110 | { 111 | Assert.Equal([RoslynUtil.CannotReadFileDiagnosticDescriptor], compilationData.CreationDiagnostics.Select(x => x.Descriptor)); 112 | 113 | _ = compilationData.GetCompilationAfterGenerators(out var diagnostics2, CancellationToken); 114 | Assert.Contains(RoslynUtil.CannotReadFileDiagnosticDescriptor, diagnostics2.Select(x => x.Descriptor)); 115 | 116 | diagnostics2 = await compilationData.GetAllDiagnosticsAsync(CancellationToken); 117 | Assert.Contains(RoslynUtil.CannotReadFileDiagnosticDescriptor, diagnostics2.Select(x => x.Descriptor)); 118 | } 119 | else 120 | { 121 | Assert.Empty(compilationData.CreationDiagnostics); 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/CompilerLogUtilTests.cs: -------------------------------------------------------------------------------- 1 | using Basic.CompilerLog.Util; 2 | using Xunit; 3 | 4 | namespace Basic.CompilerLog.UnitTests; 5 | 6 | [Collection(CompilerLogCollection.Name)] 7 | public sealed class CompilerLogUtilTests : TestBase 8 | { 9 | public CompilerLogFixture Fixture { get; } 10 | 11 | public CompilerLogUtilTests(ITestOutputHelper testOutputHelper, ITestContextAccessor testContextAccessor, CompilerLogFixture fixture) 12 | : base(testOutputHelper, testContextAccessor, nameof(CompilerLogReaderTests)) 13 | { 14 | Fixture = fixture; 15 | } 16 | 17 | [Fact] 18 | public void CreateBadExtension() 19 | { 20 | Assert.Throws(() => CompilerCallReaderUtil.Create("file.bad")); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/ConditionalFacts.cs: -------------------------------------------------------------------------------- 1 | namespace Basic.CompilerLog.UnitTests; 2 | 3 | using System.Runtime.InteropServices; 4 | using Xunit; 5 | 6 | public sealed class WindowsFactAttribute : FactAttribute 7 | { 8 | public static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 9 | 10 | public WindowsFactAttribute() 11 | { 12 | SkipUnless = nameof(WindowsFactAttribute.IsWindows); 13 | SkipType = typeof(WindowsFactAttribute); 14 | Skip = "This test is only supported on Windows"; 15 | } 16 | } 17 | 18 | public sealed class WindowsTheoryAttribute : TheoryAttribute 19 | { 20 | public WindowsTheoryAttribute() 21 | { 22 | SkipUnless = nameof(WindowsFactAttribute.IsWindows); 23 | SkipType = typeof(WindowsFactAttribute); 24 | Skip = "This test is only supported on Windows"; 25 | } 26 | } 27 | 28 | public sealed class UnixTheoryAttribute : TheoryAttribute 29 | { 30 | public static bool IsUnix => !RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 31 | 32 | public UnixTheoryAttribute() 33 | { 34 | Skip = "This test is only supported on Unix"; 35 | SkipUnless = nameof(IsUnix); 36 | SkipType = typeof(UnixTheoryAttribute); 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Basic.CompilerLog.Util; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.Diagnostics; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Collections.Immutable; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using Xunit; 11 | using Xunit.Runner.Common; 12 | using Xunit.Sdk; 13 | using AssemblyMetadata=Microsoft.CodeAnalysis.AssemblyMetadata; 14 | 15 | namespace Basic.CompilerLog.UnitTests; 16 | 17 | internal static class Extensions 18 | { 19 | internal static Guid GetModuleVersionId(this MetadataReference reference) 20 | { 21 | if (reference is PortableExecutableReference peReference && 22 | peReference.GetMetadata() is AssemblyMetadata metadata && 23 | metadata.GetModules() is { Length: > 0 } modules) 24 | { 25 | var module = modules[0]; 26 | return module.GetModuleVersionId(); 27 | } 28 | 29 | throw new Exception($"Cannot get MVID from reference {reference.Display}"); 30 | } 31 | 32 | internal static void OnDiagnosticMessage(this IMessageSink messageSink, string message) 33 | { 34 | messageSink.OnMessage(new DiagnosticMessage(message)); 35 | } 36 | 37 | internal static void ForEach(this IEnumerable enumerable, Action action) 38 | { 39 | foreach (var item in enumerable) 40 | { 41 | action(item); 42 | } 43 | } 44 | 45 | internal static CompilerCall WithArguments(this CompilerCall compilerCall, IReadOnlyCollection arguments) => 46 | new CompilerCall( 47 | compilerCall.ProjectFilePath, 48 | compilerCall.CompilerFilePath, 49 | compilerCall.Kind, 50 | compilerCall.TargetFramework, 51 | compilerCall.IsCSharp, 52 | new Lazy>(() => arguments), 53 | compilerCall.OwnerState); 54 | 55 | internal static CompilerCall WithAdditionalArguments(this CompilerCall compilerCall, IReadOnlyCollection arguments) 56 | { 57 | string[] args = [.. compilerCall.GetArguments(), .. arguments]; 58 | return compilerCall.WithArguments(args); 59 | } 60 | 61 | internal static CompilerCall WithOwner(this CompilerCall compilerCall, object? ownerState) 62 | { 63 | var args = compilerCall.GetArguments(); 64 | return new CompilerCall( 65 | compilerCall.ProjectFilePath, 66 | compilerCall.CompilerFilePath, 67 | compilerCall.Kind, 68 | compilerCall.TargetFramework, 69 | compilerCall.IsCSharp, 70 | new Lazy>(() => args), 71 | ownerState); 72 | } 73 | 74 | internal static CompilationData WithBasicAnalyzerHost(this CompilationData compilationData, BasicAnalyzerHost basicAnalyzerHost) => 75 | compilationData switch 76 | { 77 | CSharpCompilationData cs => 78 | new CSharpCompilationData( 79 | cs.CompilerCall, 80 | cs.Compilation, 81 | cs.ParseOptions, 82 | cs.EmitOptions, 83 | cs.EmitData, 84 | cs.AdditionalTexts, 85 | basicAnalyzerHost, 86 | cs.AnalyzerConfigOptionsProvider, 87 | cs.CreationDiagnostics), 88 | VisualBasicCompilationData vb => 89 | new VisualBasicCompilationData( 90 | vb.CompilerCall, 91 | vb.Compilation, 92 | vb.ParseOptions, 93 | vb.EmitOptions, 94 | vb.EmitData, 95 | vb.AdditionalTexts, 96 | basicAnalyzerHost, 97 | vb.AnalyzerConfigOptionsProvider, 98 | vb.CreationDiagnostics), 99 | _ => throw new NotSupportedException($"Unsupported compilation data type: {compilationData.GetType()}") 100 | }; 101 | } 102 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/ExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using Basic.CompilerLog.Util; 4 | using Basic.CompilerLog.Util.Impl; 5 | using Xunit; 6 | 7 | namespace Basic.CompilerLog.UnitTests; 8 | 9 | [Collection(CompilerLogCollection.Name)] 10 | public sealed class ExtensionsTests : TestBase 11 | { 12 | public CompilerLogFixture Fixture { get; } 13 | 14 | public ExtensionsTests(ITestOutputHelper testOutputHelper, ITestContextAccessor testContextAccessor, CompilerLogFixture fixture) 15 | : base(testOutputHelper, testContextAccessor, nameof(CompilationDataTests)) 16 | { 17 | Fixture = fixture; 18 | } 19 | 20 | [Fact] 21 | public void CheckEmitFlags() 22 | { 23 | EmitFlags.Default.CheckEmitFlags(); 24 | Assert.Throws(void () => (EmitFlags.IncludePdbStream | EmitFlags.MetadataOnly).CheckEmitFlags()); 25 | } 26 | 27 | [Fact] 28 | public void AddRange() 29 | { 30 | var list = new List(); 31 | Span span = new int[] { 42, 13 }; 32 | list.AddRange(span); 33 | Assert.Equal([42, 13], list); 34 | } 35 | 36 | #if NET 37 | 38 | [Fact] 39 | public void GetFailureString() 40 | { 41 | var ex = new Exception("Hello, world!", new Exception("Inner exception")); 42 | Assert.NotEmpty(ex.GetFailureString()); 43 | } 44 | 45 | #endif 46 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/FilterOptionSetTests.cs: -------------------------------------------------------------------------------- 1 | #if NET 2 | using Xunit; 3 | 4 | namespace Basic.CompilerLog.UnitTests; 5 | 6 | public sealed class FilterOptionSetTest 7 | { 8 | [Fact] 9 | public void CheckForAnalyzers() 10 | { 11 | var options = new FilterOptionSet(analyzers: false); 12 | Assert.Throws(() => options.IncludeAnalyzers); 13 | 14 | options = new FilterOptionSet(analyzers: true); 15 | Assert.True(options.IncludeAnalyzers); 16 | } 17 | } 18 | #endif -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/FixtureBase.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Text; 3 | using Xunit; 4 | using Xunit.Runner.Common; 5 | using Xunit.Sdk; 6 | 7 | namespace Basic.CompilerLog.UnitTests; 8 | 9 | public abstract class FixtureBase 10 | { 11 | private int _processCount; 12 | 13 | protected IMessageSink MessageSink { get; } 14 | 15 | protected FixtureBase(IMessageSink messageSink) 16 | { 17 | MessageSink = messageSink; 18 | } 19 | 20 | protected void RunDotnetCommand(string args, string workingDirectory) 21 | { 22 | var start = DateTime.UtcNow; 23 | var diagnosticBuilder = new StringBuilder(); 24 | 25 | diagnosticBuilder.AppendLine($"Running: {_processCount++} {args} in {workingDirectory}"); 26 | var result = DotnetUtil.Command(args, workingDirectory); 27 | diagnosticBuilder.AppendLine($"Succeeded: {result.Succeeded}"); 28 | diagnosticBuilder.AppendLine($"Standard Output: {result.StandardOut}"); 29 | diagnosticBuilder.AppendLine($"Standard Error: {result.StandardError}"); 30 | diagnosticBuilder.AppendLine($"Finished: {(DateTime.UtcNow - start).TotalSeconds:F2}s"); 31 | MessageSink.OnMessage(new DiagnosticMessage(diagnosticBuilder.ToString())); 32 | if (!result.Succeeded) 33 | { 34 | Assert.Fail($"Command failed: {diagnosticBuilder.ToString()}"); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/InMemoryLoaderTests.cs: -------------------------------------------------------------------------------- 1 | namespace Basic.CompilerLog.UnitTests; 2 | 3 | using System.Runtime.InteropServices; 4 | #if NET 5 | using System.Runtime.Loader; 6 | #endif 7 | using Basic.CompilerLog.Util; 8 | using Basic.CompilerLog.Util.Impl; 9 | using Microsoft.CodeAnalysis; 10 | using Microsoft.CodeAnalysis.CSharp; 11 | using Xunit; 12 | 13 | [Collection(CompilerLogCollection.Name)] 14 | public sealed class InMemoryLoaderTests : TestBase 15 | { 16 | public CompilerLogFixture Fixture { get; } 17 | 18 | public InMemoryLoaderTests(ITestOutputHelper testOutputHelper, ITestContextAccessor testContextAccessor, CompilerLogFixture fixture) 19 | : base(testOutputHelper, testContextAccessor, nameof(CompilerLogReaderTests)) 20 | { 21 | Fixture = fixture; 22 | } 23 | 24 | #if NET 25 | 26 | [Theory] 27 | [InlineData("AbstractTypesShouldNotHaveConstructorsAnalyzer", LanguageNames.CSharp, 1)] 28 | [InlineData("AbstractTypesShouldNotHaveConstructorsAnalyzer", null, 2)] 29 | [InlineData("NotARealName", null, 0)] 30 | public void AnalyzersForNetAnalyzers(string analyzerTypeName, string? language, int expectedCount) 31 | { 32 | using var reader = CompilerLogReader.Create(Fixture.Console.Value.CompilerLogPath, BasicAnalyzerKind.InMemory); 33 | var compilerCall = reader.ReadCompilerCall(0); 34 | var compilerData = reader.ReadCompilationData(compilerCall); 35 | var analyzerReference = compilerData.AnalyzerReferences.Single(x => x.Display == "Microsoft.CodeAnalysis.NetAnalyzers"); 36 | var analyzers = language is null 37 | ? analyzerReference.GetAnalyzersForAllLanguages() 38 | : analyzerReference.GetAnalyzers(language); 39 | var specific = analyzers.Where(x => x.GetType().Name == analyzerTypeName); 40 | Assert.Equal(expectedCount, specific.Count()); 41 | } 42 | 43 | [Theory] 44 | [InlineData("ComClassGenerator", LanguageNames.CSharp, 1)] 45 | [InlineData("ComClassGenerator", null, 1)] 46 | [InlineData("NotARealName", null, 0)] 47 | public void GeneratorsForCom(string analyzerTypeName, string? language, int expectedCount) 48 | { 49 | using var reader = CompilerLogReader.Create(Fixture.Console.Value.CompilerLogPath, BasicAnalyzerKind.InMemory); 50 | var compilerCall = reader.ReadCompilerCall(0); 51 | var compilerData = reader.ReadCompilationData(compilerCall); 52 | var analyzerReference = compilerData.AnalyzerReferences.Single(x => x.Display == "Microsoft.Interop.ComInterfaceGenerator"); 53 | var generators = language is null 54 | ? analyzerReference.GetGeneratorsForAllLanguages() 55 | : analyzerReference.GetGenerators(language); 56 | var specific = generators.Where(x => TestUtil.GetGeneratorType(x).Name == analyzerTypeName); 57 | Assert.Equal(expectedCount, specific.Count()); 58 | } 59 | 60 | [Fact] 61 | public void AnalyzersBadDefinition() 62 | { 63 | using var host = new BasicAnalyzerHostInMemory(LibraryUtil.GetAnalyzersWithBadMetadata()); 64 | Assert.Single(host.AnalyzerReferences); 65 | var analyzerReference = host.AnalyzerReferences.Single(); 66 | var analyzer = analyzerReference.GetAnalyzersForAllLanguages().Single(); 67 | Assert.Equal("GoodAnalyzer", analyzer.GetType().Name); 68 | var generator = analyzerReference.GetGeneratorsForAllLanguages().Single(); 69 | Assert.Equal("GoodGenerator", TestUtil.GetGeneratorType(generator).Name); 70 | } 71 | 72 | #endif 73 | } 74 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/LogReaderStateTests.cs: -------------------------------------------------------------------------------- 1 | 2 | using Basic.CompilerLog.Util; 3 | using Xunit; 4 | 5 | #if NET 6 | using System.Runtime.Loader; 7 | #endif 8 | 9 | namespace Basic.CompilerLog.UnitTests; 10 | 11 | [Collection(CompilerLogCollection.Name)] 12 | public class LogReaderStateTests : TestBase 13 | { 14 | public CompilerLogFixture Fixture { get; } 15 | 16 | public LogReaderStateTests(ITestOutputHelper testOutputHelper, ITestContextAccessor testContextAccessor, CompilerLogFixture fixture) 17 | : base(testOutputHelper, testContextAccessor, nameof(LogReaderState)) 18 | { 19 | Fixture = fixture; 20 | } 21 | 22 | [Fact] 23 | public void DisposeCleansUpDirectories() 24 | { 25 | var state = new Util.LogReaderState(baseDir: Root.NewDirectory()); 26 | Directory.CreateDirectory(state.AnalyzerDirectory); 27 | state.Dispose(); 28 | Assert.True(Directory.Exists(state.BaseDirectory)); 29 | } 30 | 31 | /// 32 | /// Don't throw if state can't clean up the directories because they are locked. 33 | /// 34 | [Fact] 35 | public void DisposeDirectoryLocked() 36 | { 37 | var state = new Util.LogReaderState(baseDir: Root.NewDirectory()); 38 | Directory.CreateDirectory(state.AnalyzerDirectory); 39 | var fileStream = new FileStream(Path.Combine(state.AnalyzerDirectory, "example.txt"), FileMode.Create, FileAccess.ReadWrite, FileShare.None); 40 | state.Dispose(); 41 | fileStream.Dispose(); 42 | } 43 | 44 | [Fact] 45 | public void CreateBasicAnalyzerHostBadKind() 46 | { 47 | using var reader = CompilerLogReader.Create(Fixture.Console.Value.CompilerLogPath, BasicAnalyzerKind.None); 48 | Assert.Throws(() => BasicAnalyzerHost.Create( 49 | reader, 50 | (BasicAnalyzerKind)42, 51 | reader.ReadCompilerCall(0), 52 | [])); 53 | } 54 | 55 | [Fact] 56 | public void DisposeGuards() 57 | { 58 | var state = new Util.LogReaderState(baseDir: Root.NewDirectory()); 59 | state.Dispose(); 60 | Assert.True(state.IsDisposed); 61 | Assert.Throws(() => state.GetOrCreateBasicAnalyzerHost(null!, BasicAnalyzerKind.InMemory, null!)); 62 | } 63 | 64 | #if NET 65 | [Fact] 66 | public void CustomAssemblyLoadContext() 67 | { 68 | var alc = new AssemblyLoadContext("Custom", isCollectible: true); 69 | var options = new Util.LogReaderState(alc); 70 | Assert.Same(alc, options.CompilerLoadContext); 71 | alc.Unload(); 72 | } 73 | #endif 74 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/MetadataTests.cs: -------------------------------------------------------------------------------- 1 | using Basic.CompilerLog.Util; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace Basic.CompilerLog.UnitTests; 10 | 11 | public sealed class MetadataTests 12 | { 13 | private static Metadata Parse(string content) 14 | { 15 | using var reader = new StringReader(content); 16 | return Metadata.Read(reader); 17 | } 18 | 19 | [Fact] 20 | public void ParseVersion0() 21 | { 22 | var content = """ 23 | count:50 24 | """; 25 | var metadata = Parse(content); 26 | Assert.Equal(0, metadata.MetadataVersion); 27 | Assert.Equal(50, metadata.Count); 28 | Assert.True(metadata.IsWindows); 29 | } 30 | 31 | [Fact] 32 | public void ParseVersion1() 33 | { 34 | var content = """ 35 | version:1 36 | count:50 37 | windows:true 38 | """; 39 | var metadata = Parse(content); 40 | Assert.Equal(1, metadata.MetadataVersion); 41 | Assert.Equal(50, metadata.Count); 42 | Assert.True(metadata.IsWindows); 43 | } 44 | 45 | [Fact] 46 | public void ParseBadVersion() 47 | { 48 | var content = """ 49 | version:true 50 | count:50 51 | windows:true 52 | """; 53 | Assert.Throws(() => Parse(content)); 54 | } 55 | 56 | [Fact] 57 | public void ParseBadCount() 58 | { 59 | var content = """ 60 | version:1 61 | count:true 62 | windows:true 63 | """; 64 | Assert.Throws(() => Parse(content)); 65 | } 66 | 67 | [Fact] 68 | public void ParseBadFormat() 69 | { 70 | var content = """ 71 | version 72 | """; 73 | Assert.Throws(() => Parse(content)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/PathNormalizationUtilTests.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Runtime.InteropServices; 3 | using Basic.CompilerLog.Util; 4 | using Xunit; 5 | 6 | namespace Basic.CompilerLog.UnitTests; 7 | 8 | public sealed class PathNormalizationUtilTests 9 | { 10 | [Theory] 11 | [InlineData(@"c:\", "/code/")] 12 | [InlineData(@"c:\\", "/code/")] 13 | [InlineData(@"c:\\\", "/code/")] 14 | [InlineData(@"c:\src\blah.cs", "/code/src/blah.cs")] 15 | [InlineData(@"c:\src\..\blah.cs", "/code/src/../blah.cs")] 16 | [InlineData(null, null)] 17 | public void WindowsToUnixNormalize(string? path, string? expected) 18 | { 19 | var actual = PathNormalizationUtil.WindowsToUnix.NormalizePath(path); 20 | Assert.Equal(expected, actual); 21 | } 22 | 23 | [Theory] 24 | [InlineData(@"example.cs", "/code/example.cs")] 25 | [InlineData(@"a.cs", "/code/a.cs")] 26 | public void WindowsToUnixRootFileName(string fileName, string? expected) 27 | { 28 | var actual = PathNormalizationUtil.WindowsToUnix.RootFileName(fileName); 29 | Assert.Equal(expected, actual); 30 | } 31 | 32 | [Theory] 33 | [InlineData("/", @"c:\code\")] 34 | [InlineData("/example", @"c:\code\example")] 35 | [InlineData("/example/", @"c:\code\example\")] 36 | [InlineData("/example/blah.cs", @"c:\code\example\blah.cs")] 37 | [InlineData("/example/../blah.cs", @"c:\code\example\..\blah.cs")] 38 | [InlineData(null, null)] 39 | public void UnixToWindowsNormalize(string? path, string? expected) 40 | { 41 | var actual = PathNormalizationUtil.UnixToWindows.NormalizePath(path); 42 | Assert.Equal(expected, actual); 43 | } 44 | 45 | [Theory] 46 | [InlineData(@"example.cs", @"c:\code\example.cs")] 47 | [InlineData(@"a.cs", @"c:\code\a.cs")] 48 | public void UnixToWindowsRootFileName(string fileName, string? expected) 49 | { 50 | var actual = PathNormalizationUtil.UnixToWindows.RootFileName(fileName); 51 | Assert.Equal(expected, actual); 52 | } 53 | 54 | [Theory] 55 | [InlineData(@"c:\", true)] 56 | [InlineData(@"c:", true)] 57 | [InlineData(@"c:\\\", true)] 58 | [InlineData(@"c:\..\", true)] 59 | [InlineData(@"\..\", false)] 60 | [InlineData(@"example\blah.cs", false)] 61 | [InlineData(null, false)] 62 | public void WindowsIsRooted(string? path, bool expected) 63 | { 64 | Assert.Equal(expected, PathNormalizationUtil.WindowsToUnix.IsPathRooted(path)); 65 | } 66 | 67 | [Theory] 68 | [InlineData(@"/", true)] 69 | [InlineData(@"/blah", true)] 70 | [InlineData(@"/code/blah.cs", true)] 71 | [InlineData(@"../", false)] 72 | [InlineData(@"example/blah.cs", false)] 73 | [InlineData(null, false)] 74 | public void UnixIsRooted(string? path, bool expected) 75 | { 76 | Assert.Equal(expected, PathNormalizationUtil.UnixToWindows.IsPathRooted(path)); 77 | } 78 | 79 | [Fact] 80 | public void EmptyIsRooted() 81 | { 82 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 83 | { 84 | Assert.True(PathNormalizationUtil.Empty.IsPathRooted(@"c:\")); 85 | Assert.True(PathNormalizationUtil.Empty.IsPathRooted(@"/")); 86 | } 87 | else 88 | { 89 | Assert.False(PathNormalizationUtil.Empty.IsPathRooted(@"c:\")); 90 | Assert.True(PathNormalizationUtil.Empty.IsPathRooted(@"/")); 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/PathUtilTests.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | using Basic.CompilerLog.Util; 5 | using Xunit; 6 | 7 | namespace Basic.CompilerLog.UnitTests; 8 | 9 | public sealed class PathUtilTests 10 | { 11 | [Fact] 12 | public void RemovePathStart() 13 | { 14 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 15 | { 16 | Core(@"C:\a\b\c.cs", @"C:\a\", @"b\c.cs"); 17 | Core(@"a\b\c.cs", @"a\", @"b\c.cs"); 18 | } 19 | else 20 | { 21 | Core("a/b/c.cs", "a/", "b/c.cs"); 22 | Core("a/b/c.cs", "a", "b/c.cs"); 23 | } 24 | 25 | static void Core(string filePath, string start, string expected) 26 | { 27 | Assert.Equal(expected, PathUtil.RemovePathStart(filePath, start)); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/PolyfillTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | using Basic.CompilerLog.Util; 8 | 9 | namespace Basic.CompilerLog.UnitTests; 10 | 11 | public class PolyfillTests 12 | { 13 | [Fact] 14 | public void ReadExactlyTooMany() 15 | { 16 | using var stream = new MemoryStream(); 17 | stream.Write([1, 2, 3], 0, 3); 18 | 19 | byte[] buffer = new byte[10]; 20 | Assert.Throws(() => stream.ReadExactly(buffer.AsSpan())); 21 | } 22 | 23 | [Fact] 24 | public void WriteSimple() 25 | { 26 | var writer = new StringWriter(); 27 | ReadOnlySpan span = "hello".AsSpan(); 28 | writer.Write(span); 29 | Assert.Equal("hello", writer.ToString()); 30 | } 31 | 32 | [Fact] 33 | public void WriteLineSimple() 34 | { 35 | var writer = new StringWriter(); 36 | ReadOnlySpan span = "hello".AsSpan(); 37 | writer.WriteLine(span); 38 | Assert.Equal("hello" + Environment.NewLine, writer.ToString()); 39 | } 40 | 41 | [Fact] 42 | public void ContainsCharSimple() 43 | { 44 | var span = "test".AsSpan(); 45 | Assert.True(span.Contains('e')); 46 | Assert.False(span.Contains('f')); 47 | } 48 | 49 | [Fact] 50 | public void GetByteCountEmpty() 51 | { 52 | Assert.Equal(0, TestBase.DefaultEncoding.GetByteCount(ReadOnlySpan.Empty)); 53 | Assert.Equal(0, TestBase.DefaultEncoding.GetByteCount((ReadOnlySpan)default)); 54 | } 55 | 56 | [Fact] 57 | public void GetBytesCountEmpty() 58 | { 59 | var buffer = new byte[10]; 60 | Assert.Equal(0, TestBase.DefaultEncoding.GetBytes(ReadOnlySpan.Empty, buffer.AsSpan())); 61 | Assert.Throws(() => TestBase.DefaultEncoding.GetBytes("hello".AsSpan(), buffer.AsSpan(0, 0))); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/Properties.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | 8 | [assembly: CollectionBehavior(DisableTestParallelization = true)] 9 | 10 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/ResilientDirectoryTests.cs: -------------------------------------------------------------------------------- 1 | using Basic.CompilerLog.Util; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Runtime.InteropServices; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Xunit; 9 | 10 | namespace Basic.CompilerLog.UnitTests; 11 | 12 | public sealed class ResilientDirectoryTests 13 | { 14 | public static string RootPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) 15 | ? @"c:\" 16 | : "/"; 17 | 18 | [Fact] 19 | public void GetNewFilePathFlatten1() 20 | { 21 | using var tempDir = new TempDir(); 22 | var dir = new ResilientDirectory(tempDir.DirectoryPath, flatten: true); 23 | var path1 = dir.GetNewFilePath(Path.Combine(RootPath, "temp1", "resource.txt")); 24 | var path2 = dir.GetNewFilePath(Path.Combine(RootPath, "temp2", "resource.txt")); 25 | Assert.NotEqual(path1, path2); 26 | Assert.Equal(Path.Combine(tempDir.DirectoryPath, "resource.txt"), path1); 27 | Assert.Equal("resource.txt", Path.GetFileName(path2)); 28 | } 29 | 30 | [Fact] 31 | public void GetNewFilePathFlatten2() 32 | { 33 | using var tempDir = new TempDir(); 34 | var dir = new ResilientDirectory(tempDir.DirectoryPath, flatten: true); 35 | Assert.True(dir.Flatten); 36 | var originalPath = Path.Combine(RootPath, "temp", "resource.txt"); 37 | var path1 = dir.GetNewFilePath(originalPath); 38 | var path2 = dir.GetNewFilePath(originalPath); 39 | Assert.Equal(path1, path2); 40 | Assert.Equal(Path.Combine(tempDir.DirectoryPath, "resource.txt"), path1); 41 | } 42 | 43 | [Fact] 44 | public void GetNewFilePath1() 45 | { 46 | using var tempDir = new TempDir(); 47 | var dir = new ResilientDirectory(tempDir.DirectoryPath, flatten: false); 48 | var path1 = dir.GetNewFilePath(Path.Combine(RootPath, "temp1", "resource.txt")); 49 | var path2 = dir.GetNewFilePath(Path.Combine(RootPath, "temp2", "resource.txt")); 50 | Assert.NotEqual(path1, path2); 51 | Assert.NotEqual(Path.Combine(tempDir.DirectoryPath, "resource.txt"), path1); 52 | Assert.NotEqual(Path.Combine(tempDir.DirectoryPath, "resource.txt"), path2); 53 | Assert.Equal("resource.txt", Path.GetFileName(path1)); 54 | Assert.Equal("resource.txt", Path.GetFileName(path2)); 55 | } 56 | 57 | [Fact] 58 | public void GetNewFilePath2() 59 | { 60 | using var tempDir = new TempDir(); 61 | var dir = new ResilientDirectory(tempDir.DirectoryPath, flatten: false); 62 | var originalPath = Path.Combine(RootPath, "temp", "resource.txt"); 63 | var path1 = dir.GetNewFilePath(originalPath); 64 | var path2 = dir.GetNewFilePath(originalPath); 65 | Assert.Equal(path1, path2); 66 | Assert.NotEqual(Path.Combine(tempDir.DirectoryPath, "resource.txt"), path1); 67 | Assert.Equal("resource.txt", Path.GetFileName(path1)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/ResourceLoader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Text; 5 | 6 | namespace Basic.CompilerLog.UnitTests; 7 | 8 | internal static class ResourceLoader 9 | { 10 | public static Stream GetResourceStream(string name) 11 | { 12 | var assembly = typeof(ResourceLoader).GetTypeInfo().Assembly; 13 | 14 | var stream = assembly.GetManifestResourceStream(name); 15 | if (stream == null) 16 | { 17 | throw new InvalidOperationException($"Resource '{name}' not found in {assembly.FullName}."); 18 | } 19 | 20 | return stream; 21 | } 22 | 23 | public static byte[] GetResourceBlob(string name) 24 | { 25 | using (var stream = GetResourceStream(name)) 26 | { 27 | var bytes = new byte[stream.Length]; 28 | using (var memoryStream = new MemoryStream(bytes)) 29 | { 30 | stream.CopyTo(memoryStream); 31 | } 32 | 33 | return bytes; 34 | } 35 | } 36 | 37 | public static byte[] GetOrCreateResource(ref byte[]? resource, string name) 38 | { 39 | if (resource == null) 40 | { 41 | resource = GetResourceBlob(name); 42 | } 43 | 44 | return resource; 45 | } 46 | 47 | public static string GetOrCreateResource(ref string resource, string name) 48 | { 49 | if (resource == null) 50 | { 51 | using (var stream = GetResourceStream(name)) 52 | { 53 | using (var streamReader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true)) 54 | { 55 | resource = streamReader.ReadToEnd(); 56 | } 57 | } 58 | } 59 | 60 | return resource; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/Resources/Key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredpar/complog/8ef8225190174df2315f5e84b47f372679f9e82d/src/Basic.CompilerLog.UnitTests/Resources/Key.snk -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/Resources/MetadataVersion1/console.complog: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredpar/complog/8ef8225190174df2315f5e84b47f372679f9e82d/src/Basic.CompilerLog.UnitTests/Resources/MetadataVersion1/console.complog -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/Resources/MetadataVersion2/console.complog: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredpar/complog/8ef8225190174df2315f5e84b47f372679f9e82d/src/Basic.CompilerLog.UnitTests/Resources/MetadataVersion2/console.complog -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/Resources/linux-console.complog: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredpar/complog/8ef8225190174df2315f5e84b47f372679f9e82d/src/Basic.CompilerLog.UnitTests/Resources/linux-console.complog -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/Resources/windows-console.complog: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredpar/complog/8ef8225190174df2315f5e84b47f372679f9e82d/src/Basic.CompilerLog.UnitTests/Resources/windows-console.complog -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/SdkUtilTests.cs: -------------------------------------------------------------------------------- 1 | 2 | using Basic.CompilerLog.Util; 3 | using Xunit; 4 | using Xunit.Sdk; 5 | 6 | namespace Basic.CompilerLog.UnitTests; 7 | 8 | public sealed class SdkUtilTests 9 | { 10 | [Fact] 11 | public void GetDotnetDirectoryBadPath() 12 | { 13 | Assert.Throws(() => SdkUtil.GetDotnetDirectory(@"C:\")); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/SolutionReaderTests.cs: -------------------------------------------------------------------------------- 1 | using Basic.CompilerLog.Util; 2 | using Basic.CompilerLog.Util.Impl; 3 | using Microsoft.CodeAnalysis; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Runtime.CompilerServices; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using Xunit; 11 | using Xunit.Sdk; 12 | 13 | namespace Basic.CompilerLog.UnitTests; 14 | 15 | [Collection(CompilerLogCollection.Name)] 16 | public sealed class SolutionReaderTests : TestBase 17 | { 18 | public List ReaderList { get; } = new(); 19 | public CompilerLogFixture Fixture { get; } 20 | 21 | public SolutionReaderTests(ITestOutputHelper testOutputHelper, ITestContextAccessor testContextAccessor, CompilerLogFixture fixture) 22 | : base(testOutputHelper, testContextAccessor, nameof(SolutionReader)) 23 | { 24 | Fixture = fixture; 25 | } 26 | 27 | public override void Dispose() 28 | { 29 | foreach (var reader in ReaderList) 30 | { 31 | reader.Dispose(); 32 | } 33 | ReaderList.Clear(); 34 | 35 | #if NET 36 | // The underlying solution structure holds lots of references that root our contexts 37 | // so there is no way to fully free here. 38 | OnDiskLoader.ClearActiveAssemblyLoadContext(); 39 | #endif 40 | 41 | base.Dispose(); 42 | } 43 | 44 | private Solution GetSolution(string compilerLogFilePath, BasicAnalyzerKind basicAnalyzerKind) 45 | { 46 | var reader = SolutionReader.Create(compilerLogFilePath, basicAnalyzerKind); 47 | ReaderList.Add(reader); 48 | var workspace = new AdhocWorkspace(); 49 | var solution = workspace.AddSolution(reader.ReadSolutionInfo()); 50 | return solution; 51 | } 52 | 53 | [Theory] 54 | [MemberData(nameof(GetSimpleBasicAnalyzerKinds))] 55 | public async Task DocumentsGeneratedDefaultHost(BasicAnalyzerKind basicAnalyzerKind) 56 | { 57 | await Run(Fixture.Console.Value.BinaryLogPath!); 58 | await Run(Fixture.Console.Value.CompilerLogPath); 59 | 60 | async Task Run(string filePath) 61 | { 62 | var solution = GetSolution(filePath, basicAnalyzerKind); 63 | var project = solution.Projects.Single(); 64 | Assert.NotEmpty(project.AnalyzerReferences); 65 | var docs = project.Documents.ToList(); 66 | var generatedDocs = (await project.GetSourceGeneratedDocumentsAsync(CancellationToken)).ToList(); 67 | Assert.Null(docs.FirstOrDefault(x => x.Name == "RegexGenerator.g.cs")); 68 | Assert.Single(generatedDocs); 69 | Assert.NotNull(generatedDocs.First(x => x.Name == "RegexGenerator.g.cs")); 70 | } 71 | } 72 | 73 | [Fact] 74 | public void CreateRespectLeaveOpen() 75 | { 76 | using var stream = new FileStream(Fixture.ConsoleComplex.Value.CompilerLogPath, FileMode.Open, FileAccess.Read, FileShare.Read); 77 | var reader = SolutionReader.Create(stream, leaveOpen: true); 78 | reader.Dispose(); 79 | 80 | // Throws if the underlying stream is disposed 81 | stream.Seek(0, SeekOrigin.Begin); 82 | } 83 | 84 | [Fact] 85 | public async Task ProjectReference_Simple() 86 | { 87 | await Run(Fixture.ConsoleWithReference.Value.BinaryLogPath!); 88 | await Run(Fixture.ConsoleWithReference.Value.CompilerLogPath); 89 | 90 | async Task Run(string filePath) 91 | { 92 | var solution = GetSolution(filePath, BasicAnalyzerKind.None); 93 | var consoleProject = solution.Projects 94 | .Where(x => x.Name == "console-with-reference.csproj") 95 | .Single(); 96 | var projectReference = consoleProject.ProjectReferences.Single(); 97 | var utilProject = solution.GetProject(projectReference.ProjectId); 98 | Assert.NotNull(utilProject); 99 | Assert.Equal("util.csproj", utilProject.Name); 100 | var compilation = await consoleProject.GetCompilationAsync(CancellationToken); 101 | Assert.NotNull(compilation); 102 | var result = compilation.EmitToMemory(cancellationToken: CancellationToken); 103 | Assert.True(result.Success); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/StringStreamTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Basic.CompilerLog.Util; 3 | using DotUtils.StreamUtils; 4 | using Xunit; 5 | 6 | namespace Basic.CompilerLog.UnitTests; 7 | 8 | public class StringStreamTests 9 | { 10 | private static readonly Encoding[] Encodings = 11 | [ 12 | Encoding.UTF8, 13 | Encoding.UTF32 14 | ]; 15 | 16 | private void RoundTripByteByByte(string input) 17 | { 18 | foreach (var encoding in Encodings) 19 | { 20 | using var inputStream = new StringStream(input, encoding); 21 | using var memoryStream = new MemoryStream(); 22 | while (inputStream.ReadByte() is int b && b != -1) 23 | { 24 | memoryStream.WriteByte((byte)b); 25 | } 26 | 27 | memoryStream.Position = 0; 28 | var actual = encoding.GetString(memoryStream.ToArray()); 29 | Assert.Equal(input, actual); 30 | } 31 | } 32 | 33 | private void RoundTripCopy(string input) 34 | { 35 | foreach (var encoding in Encodings) 36 | { 37 | using var inputStream = new StringStream(input, encoding); 38 | using var memoryStream = new MemoryStream(); 39 | inputStream.CopyTo(memoryStream); 40 | 41 | memoryStream.Position = 0; 42 | var actual = encoding.GetString(memoryStream.ToArray()); 43 | Assert.Equal(input, actual); 44 | } 45 | } 46 | 47 | private void RoundTripReset(string input) 48 | { 49 | foreach (var encoding in Encodings) 50 | { 51 | using var inputStream = new StringStream(input, encoding); 52 | using var memoryStream = new MemoryStream(); 53 | inputStream.Position = 0; 54 | memoryStream.Position = 0; 55 | inputStream.CopyTo(memoryStream); 56 | 57 | memoryStream.Position = 0; 58 | var actual = encoding.GetString(memoryStream.ToArray()); 59 | Assert.Equal(input, actual); 60 | } 61 | } 62 | 63 | private void RoundTripAll(string input) 64 | { 65 | RoundTripByteByByte(input); 66 | RoundTripCopy(input); 67 | RoundTripReset(input); 68 | } 69 | 70 | [Fact] 71 | public void Behaviors() 72 | { 73 | var stream = new StringStream("hello", Encoding.UTF8); 74 | Assert.True(stream.CanRead); 75 | Assert.False(stream.CanWrite); 76 | Assert.False(stream.CanSeek); 77 | Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); 78 | Assert.Throws(() => stream.Write(Array.Empty(), 0, 0)); 79 | Assert.Throws(() => stream.SetLength(1)); 80 | stream.Flush(); // no-op 81 | } 82 | 83 | [Fact] 84 | public void PositionReset() 85 | { 86 | var stream = new StringStream("hello", Encoding.UTF8); 87 | var bytes1 = stream.ReadToEnd(); 88 | stream.Position = 0; 89 | var bytes2 = stream.ReadToEnd(); 90 | Assert.Equal(bytes1, bytes2); 91 | } 92 | 93 | [Fact] 94 | public void PositionSetToMiddle() 95 | { 96 | var stream = new StringStream("hello", Encoding.UTF8); 97 | var bytes1 = stream.ReadToEnd(); 98 | stream.Position = 0; 99 | Assert.Throws(() => stream.Position = 1); 100 | } 101 | 102 | [Theory] 103 | [InlineData("Hello, world!")] 104 | [InlineData("")] 105 | [InlineData("lets try this value")] 106 | public void RoundTrip(string input) => RoundTripAll(input); 107 | 108 | [Fact] 109 | public void RoundTripGenerated() 110 | { 111 | RoundTripAll(new string('a', 1000)); 112 | RoundTripAll(new string('a', 10_000)); 113 | } 114 | 115 | [Fact] 116 | public void ReadEmpty() 117 | { 118 | var stream = new StringStream("hello world", Encoding.UTF8); 119 | Assert.Equal(0, stream.Read(Array.Empty(), 0, 0)); 120 | #if NET 121 | Assert.Equal(0, stream.Read(Array.Empty().AsSpan())); 122 | #endif 123 | } 124 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/TempDir.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Basic.CompilerLog.UnitTests; 8 | 9 | internal sealed class TempDir : IDisposable 10 | { 11 | internal string DirectoryPath { get; } 12 | 13 | public TempDir(string? name = null) 14 | { 15 | DirectoryPath = TestUtil.CreateUniqueSubDirectory(Path.Combine(TestUtil.TestTempRoot, "temps")); 16 | if (name != null) 17 | { 18 | DirectoryPath = Path.Combine(DirectoryPath, name); 19 | } 20 | 21 | Directory.CreateDirectory(DirectoryPath); 22 | } 23 | 24 | public void Dispose() 25 | { 26 | if (Directory.Exists(DirectoryPath)) 27 | { 28 | Directory.Delete(DirectoryPath, recursive: true); 29 | } 30 | } 31 | 32 | public string NewFile(string fileName, string content) 33 | { 34 | var filePath = Path.Combine(DirectoryPath, fileName); 35 | File.WriteAllText(filePath, content); 36 | return filePath; 37 | } 38 | 39 | public string NewFile(string fileName, Stream content) 40 | { 41 | var filePath = Path.Combine(DirectoryPath, fileName); 42 | using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None); 43 | content.CopyTo(fileStream); 44 | return filePath; 45 | } 46 | 47 | public string NewDirectory(string? name = null) 48 | { 49 | name ??= Guid.NewGuid().ToString(); 50 | var path = Path.Combine(DirectoryPath, name); 51 | _ = Directory.CreateDirectory(path); 52 | return path; 53 | } 54 | 55 | public string CopyDirectory(string dir) 56 | { 57 | var newDir = NewDirectory(); 58 | 59 | var info = new DirectoryInfo(dir); 60 | foreach (var item in info.GetFiles()) 61 | { 62 | var tempPath = Path.Combine(newDir, item.Name); 63 | item.CopyTo(tempPath, overwrite: true); 64 | } 65 | 66 | return newDir; 67 | } 68 | 69 | public void EmptyDirectory() 70 | { 71 | var d = new DirectoryInfo(DirectoryPath); 72 | foreach(System.IO.FileInfo file in d.GetFiles()) file.Delete(); 73 | foreach(System.IO.DirectoryInfo subDirectory in d.GetDirectories()) subDirectory.Delete(true); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/TestUtil.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Diagnostics; 3 | using System.Reflection; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | using Microsoft.CodeAnalysis; 7 | using Microsoft.CodeAnalysis.VisualBasic.Syntax; 8 | using Xunit; 9 | using Xunit.Sdk; 10 | using Basic.CompilerLog.Util; 11 | using Basic.CompilerLog.Util.Impl; 12 | using System.Collections.Immutable; 13 | 14 | 15 | #if NET 16 | using System.Runtime.Loader; 17 | #endif 18 | 19 | namespace Basic.CompilerLog.UnitTests; 20 | 21 | internal static class TestUtil 22 | { 23 | internal static bool IsNetFramework => 24 | #if NETFRAMEWORK 25 | true; 26 | #else 27 | false; 28 | #endif 29 | 30 | internal static bool IsNetCore => !IsNetFramework; 31 | 32 | internal static bool InGitHubActions => Environment.GetEnvironmentVariable("GITHUB_ACTIONS") is not null; 33 | 34 | internal static string TestArtifactsDirectory 35 | { 36 | get 37 | { 38 | if (InGitHubActions) 39 | { 40 | return TestUtil.GitHubActionsTestArtifactsDirectory; 41 | } 42 | 43 | var assemblyDir = Path.GetDirectoryName(typeof(TestBase).Assembly.Location)!; 44 | return Path.Combine(assemblyDir, "test-artifacts"); 45 | } 46 | } 47 | 48 | internal static string GitHubActionsTestArtifactsDirectory 49 | { 50 | get 51 | { 52 | Debug.Assert(InGitHubActions); 53 | 54 | var testArtifactsDir = Environment.GetEnvironmentVariable("TEST_ARTIFACTS_PATH"); 55 | if (testArtifactsDir is null) 56 | { 57 | throw new Exception("TEST_ARTIFACTS_PATH is not set in GitHub actions"); 58 | 59 | } 60 | 61 | var suffix = IsNetCore ? "netcore" : "netfx"; 62 | 63 | return Path.Combine(testArtifactsDir, suffix); 64 | } 65 | } 66 | 67 | internal static string TestTempRoot { get; } = CreateUniqueSubDirectory(Path.Combine(Path.GetTempPath(), "Basic.CompilerLog.UnitTests")); 68 | 69 | /// 70 | /// This code will generate a unique subdirectory under . This is done instead of using 71 | /// GUIDs because that leads to long path issues on .NET Framework. 72 | /// 73 | /// 74 | /// This method is not entirely foolproof. But it does serve the purpose of creating unique directory names 75 | /// when tests are run in parallel on the same machine provided that we own . 76 | /// 77 | internal static string CreateUniqueSubDirectory(string path) 78 | { 79 | _ = Directory.CreateDirectory(path); 80 | 81 | var id = 0; 82 | while (true) 83 | { 84 | try 85 | { 86 | var filePath = Path.Combine(path, $"{id}.txt"); 87 | var dirPath = Path.Combine(path, $"{id}"); 88 | if (!File.Exists(filePath) && !Directory.Exists(dirPath)) 89 | { 90 | var fileStream = new FileStream(filePath, FileMode.CreateNew); 91 | fileStream.Dispose(); 92 | 93 | _ = Directory.CreateDirectory(dirPath); 94 | return dirPath; 95 | } 96 | } 97 | catch 98 | { 99 | // Don't care why we couldn't create the file or directory, just that it failed 100 | } 101 | 102 | id++; 103 | } 104 | } 105 | 106 | /// 107 | /// Internally a is wrapped in a type called IncrementalGeneratorWrapper. 108 | /// This method will dig through that and return the original type. 109 | /// 110 | /// 111 | /// 112 | internal static Type GetGeneratorType(object obj) 113 | { 114 | var type = obj.GetType(); 115 | if (type.Name == "IncrementalGeneratorWrapper") 116 | { 117 | var prop = type.GetProperty( 118 | "Generator", 119 | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)!; 120 | obj = prop.GetMethod!.Invoke(obj, null)!; 121 | } 122 | 123 | return obj.GetType(); 124 | } 125 | 126 | /// 127 | /// Run the build.cmd / .sh generated from an export command 128 | /// 129 | internal static ProcessResult RunBuildCmd(string directory) => 130 | RuntimeInformation.IsOSPlatform(OSPlatform.Windows) 131 | ? ProcessUtil.Run("cmd", args: "/c build.cmd", workingDirectory: directory) 132 | : ProcessUtil.Run(Path.Combine(directory, "build.sh"), args: "", workingDirectory: directory); 133 | 134 | internal static string GetProjectFile(string directory) => 135 | Directory.EnumerateFiles(directory, "*proj").Single(); 136 | 137 | /// 138 | /// Add a project property to the project file in the current directory 139 | /// 140 | internal static void AddProjectProperty(string property, string directory) 141 | { 142 | var projectFile = GetProjectFile(directory); 143 | var lines = File.ReadAllLines(projectFile); 144 | using var writer = new StreamWriter(projectFile, append: false); 145 | foreach (var line in lines) 146 | { 147 | if (line.Contains("")) 148 | { 149 | writer.WriteLine(property); 150 | } 151 | 152 | writer.WriteLine(line); 153 | } 154 | } 155 | 156 | internal static void SetProjectFileContent(string content, string directory) 157 | { 158 | var projectFile = GetProjectFile(directory); 159 | File.WriteAllText(projectFile, content); 160 | } 161 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/UnitTestsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | 8 | namespace Basic.CompilerLog.UnitTests; 9 | 10 | public sealed class UnitTestsTests 11 | { 12 | [Fact] 13 | public void CreateUniqueSubDirectory() 14 | { 15 | var root = new TempDir(); 16 | var path1 = TestUtil.CreateUniqueSubDirectory(root.DirectoryPath); 17 | Assert.True(Directory.Exists(path1)); 18 | var path2 = TestUtil.CreateUniqueSubDirectory(root.DirectoryPath); 19 | Assert.True(Directory.Exists(path2)); 20 | Assert.NotEqual(path1, path2); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.UnitTests/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "appDomain": "ifAvailable", 3 | "shadowCopy": false, 4 | "parallelizeTestCollections": false, 5 | "printMaxStringLength": 0 6 | } 7 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/AssemblyIdentityData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Basic.CompilerLog.Util; 8 | 9 | public sealed class AssemblyIdentityData(Guid mvid, string? assemblyName, string? assemblyInformationalVersion) 10 | { 11 | public Guid Mvid { get; } = mvid; 12 | public string? AssemblyName { get; } = assemblyName; 13 | public string? AssemblyInformationalVersion { get; } = assemblyInformationalVersion; 14 | } 15 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/BannedSymbols.txt: -------------------------------------------------------------------------------- 1 | M:Microsoft.CodeAnalysis.Diagnostics.AnalyzerReference.GetAnalyzers(System.String); Use GetAnalyzers(string, List) instead 2 | M:Microsoft.CodeAnalysis.Diagnostics.AnalyzerReference.GetAnalyzersForAllLanguages(); 3 | M:Microsoft.CodeAnalysis.Diagnostics.AnalyzerReference.GetGenerators(System.String); Use GetGenerators(string, List) instead 4 | M:Microsoft.CodeAnalysis.Diagnostics.AnalyzerReference.GetGeneratorsForAllLanguages(); -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/Basic.CompilerLog.Util.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net9.0;net472;netstandard2.0 5 | enable 6 | true 7 | true 8 | true 9 | $(NoWarn);RS2008;CS1591 10 | README.md 11 | true 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | contentfiles; analyzers 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/BasicAnalyzerHost.cs: -------------------------------------------------------------------------------- 1 | using Basic.CompilerLog.Util.Impl; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.Diagnostics; 4 | using System.Collections.Immutable; 5 | using System.Collections.Concurrent; 6 | using System.Diagnostics; 7 | 8 | namespace Basic.CompilerLog.Util; 9 | 10 | /// 11 | /// Controls how analyzers (and generators) are loaded 12 | /// 13 | public enum BasicAnalyzerKind 14 | { 15 | /// 16 | /// Analyzers and generators from the original are not loaded at all. In the case 17 | /// the original build had generated files they are just added directly to the 18 | /// compilation. 19 | /// 20 | /// 21 | /// This option avoids loading third party analyzers and generators. 22 | /// 23 | None = 0, 24 | 25 | /// 26 | /// Analyzers are loaded in memory and disk is not used. 27 | /// 28 | InMemory = 1, 29 | 30 | /// 31 | /// Analyzers are written to disk and loaded from there. This will produce as a 32 | /// side effect instances. 33 | /// 34 | OnDisk = 2, 35 | } 36 | 37 | public interface IBasicAnalyzerReference 38 | { 39 | public ImmutableArray GetAnalyzers(string language, List diagnostics); 40 | public ImmutableArray GetGenerators(string language, List diagnostics); 41 | } 42 | 43 | /// 44 | /// The set of analyzers loaded for a given 45 | /// 46 | public abstract class BasicAnalyzerHost : IDisposable 47 | { 48 | public static BasicAnalyzerKind DefaultKind 49 | { 50 | get 51 | { 52 | #if NET 53 | return BasicAnalyzerKind.InMemory; 54 | #else 55 | return BasicAnalyzerKind.OnDisk; 56 | #endif 57 | } 58 | } 59 | 60 | public BasicAnalyzerKind Kind { get; } 61 | public ImmutableArray AnalyzerReferences 62 | { 63 | get 64 | { 65 | CheckDisposed(); 66 | return AnalyzerReferencesCore; 67 | } 68 | } 69 | 70 | protected abstract ImmutableArray AnalyzerReferencesCore { get; } 71 | 72 | public bool IsDisposed { get; private set; } 73 | 74 | protected BasicAnalyzerHost(BasicAnalyzerKind kind) 75 | { 76 | Kind = kind; 77 | } 78 | 79 | public void Dispose() 80 | { 81 | if (IsDisposed) 82 | { 83 | return; 84 | } 85 | 86 | try 87 | { 88 | DisposeCore(); 89 | } 90 | finally 91 | { 92 | IsDisposed = true; 93 | } 94 | } 95 | 96 | protected abstract void DisposeCore(); 97 | 98 | protected void CheckDisposed() 99 | { 100 | if (IsDisposed) 101 | { 102 | throw new ObjectDisposedException(nameof(BasicAnalyzerHost)); 103 | } 104 | } 105 | 106 | public static bool IsSupported(BasicAnalyzerKind kind) 107 | { 108 | #if NET 109 | return true; 110 | #else 111 | return kind is BasicAnalyzerKind.OnDisk or BasicAnalyzerKind.None; 112 | #endif 113 | } 114 | 115 | internal static BasicAnalyzerHost Create( 116 | IBasicAnalyzerHostDataProvider dataProvider, 117 | BasicAnalyzerKind kind, 118 | CompilerCall compilerCall, 119 | List analyzers) 120 | { 121 | return kind switch 122 | { 123 | BasicAnalyzerKind.OnDisk => new BasicAnalyzerHostOnDisk(dataProvider, analyzers), 124 | BasicAnalyzerKind.InMemory => new BasicAnalyzerHostInMemory(dataProvider, analyzers), 125 | BasicAnalyzerKind.None => CreateNone(analyzers), 126 | _ => throw new InvalidOperationException() 127 | }; 128 | 129 | BasicAnalyzerHostNone CreateNone(List analyzers) 130 | { 131 | if (analyzers.Count == 0) 132 | { 133 | return new BasicAnalyzerHostNone(); 134 | } 135 | 136 | if (!dataProvider.HasAllGeneratedFileContent(compilerCall)) 137 | { 138 | return new(CreateDiagnostic("Generated files not available in the PDB")); 139 | } 140 | 141 | try 142 | { 143 | var generatedSourceTexts = dataProvider.ReadAllGeneratedSourceTexts(compilerCall); 144 | return new BasicAnalyzerHostNone(generatedSourceTexts); 145 | } 146 | catch (Exception ex) 147 | { 148 | return new(CreateDiagnostic(ex.Message)); 149 | } 150 | } 151 | 152 | static Diagnostic CreateDiagnostic(string message) => 153 | Diagnostic.Create( 154 | RoslynUtil.ErrorReadingGeneratedFilesDiagnosticDescriptor, 155 | Location.None, 156 | message); 157 | } 158 | } 159 | 160 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/CodeAnalysisExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.Emit; 3 | using Microsoft.CodeAnalysis.Text; 4 | 5 | namespace Basic.CompilerLog.Util; 6 | 7 | public static class CodeAnalysisExtensions 8 | { 9 | internal static readonly EmitOptions DefaultEmitOptions = new EmitOptions( 10 | metadataOnly: false, 11 | debugInformationFormat: DebugInformationFormat.PortablePdb); 12 | 13 | public static EmitOptions WithEmitFlags(this EmitOptions emitOptions, EmitFlags emitFlags) 14 | { 15 | emitFlags.CheckEmitFlags(); 16 | if ((emitFlags & EmitFlags.MetadataOnly) != 0) 17 | { 18 | return emitOptions.WithEmitMetadataOnly(true); 19 | } 20 | 21 | return emitOptions; 22 | } 23 | 24 | public static EmitMemoryResult EmitToMemory( 25 | this Compilation compilation, 26 | EmitFlags emitFlags = EmitFlags.Default, 27 | Stream? win32ResourceStream = null, 28 | IEnumerable? manifestResources = null, 29 | EmitOptions? emitOptions = null, 30 | IMethodSymbol? debugEntryPoint = null, 31 | Stream? sourceLinkStream = null, 32 | IEnumerable? embeddedTexts = null, 33 | CancellationToken cancellationToken = default) 34 | { 35 | emitFlags.CheckEmitFlags(); 36 | emitOptions ??= DefaultEmitOptions; 37 | MemoryStream assemblyStream = new MemoryStream(); 38 | MemoryStream? pdbStream = null; 39 | MemoryStream? xmlStream = null; 40 | MemoryStream? metadataStream = null; 41 | 42 | if ((emitFlags & EmitFlags.IncludePdbStream) != 0 && emitOptions.DebugInformationFormat != DebugInformationFormat.Embedded) 43 | { 44 | pdbStream = new MemoryStream(); 45 | } 46 | 47 | if ((emitFlags & EmitFlags.IncludeXmlStream) != 0) 48 | { 49 | xmlStream = new MemoryStream(); 50 | } 51 | 52 | if ((emitFlags & EmitFlags.IncludeMetadataStream) != 0) 53 | { 54 | metadataStream = new MemoryStream(); 55 | } 56 | 57 | emitOptions = emitOptions.WithEmitFlags(emitFlags); 58 | var result = compilation.Emit( 59 | assemblyStream, 60 | pdbStream, 61 | xmlStream, 62 | win32ResourceStream, 63 | manifestResources, 64 | emitOptions, 65 | debugEntryPoint: debugEntryPoint, 66 | sourceLinkStream, 67 | embeddedTexts, 68 | metadataPEStream: metadataStream, 69 | cancellationToken: cancellationToken); 70 | return new EmitMemoryResult( 71 | result.Success, 72 | assemblyStream, 73 | pdbStream, 74 | xmlStream, 75 | metadataStream, 76 | result.Diagnostics); 77 | } 78 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/CommonUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using MessagePack; 7 | using Microsoft.CodeAnalysis; 8 | using Microsoft.CodeAnalysis.Emit; 9 | 10 | #if NET 11 | using System.Runtime.Loader; 12 | #endif 13 | 14 | namespace Basic.CompilerLog.Util; 15 | 16 | internal static class CommonUtil 17 | { 18 | internal const string MetadataFileName = "metadata.txt"; 19 | internal const string AssemblyInfoFileName = "assemblyinfo.txt"; 20 | internal const string LogInfoFileName = "loginfo.txt"; 21 | internal static readonly Encoding ContentEncoding = Encoding.UTF8; 22 | internal static readonly MessagePackSerializerOptions SerializerOptions = MessagePackSerializerOptions.Standard.WithAllowAssemblyVersionMismatch(true); 23 | 24 | internal static string GetCompilerEntryName(int index) => $"compilations/{index}.txt"; 25 | internal static string GetAssemblyEntryName(Guid mvid) => $"assembly/{mvid:N}"; 26 | internal static string GetContentEntryName(string contentHash) => $"content/{contentHash}"; 27 | 28 | #if NET 29 | 30 | internal static AssemblyLoadContext GetAssemblyLoadContext(AssemblyLoadContext? context = null) 31 | { 32 | if (context is { }) 33 | { 34 | return context; 35 | } 36 | 37 | // This code path is only valid in a runtime context so this will be non-null. 38 | return AssemblyLoadContext.GetLoadContext(typeof(CommonUtil).Assembly)!; 39 | } 40 | 41 | #endif 42 | 43 | /// 44 | /// This is a _best effort_ attempt to delete a directory if it is empty. It returns true 45 | /// in the case that at some point in the execution if this method the directory did 46 | /// not exist, false otherwise. 47 | /// 48 | internal static bool DeleteDirectoryIfEmpty(string directory) 49 | { 50 | if (!Directory.Exists(directory)) 51 | { 52 | return true; 53 | } 54 | 55 | try 56 | { 57 | if (Directory.EnumerateFileSystemEntries(directory).Any()) 58 | { 59 | return false; 60 | } 61 | 62 | Directory.Delete(directory, recursive: false); 63 | return true; 64 | } 65 | catch 66 | { 67 | return false; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/CompilerAssemblyData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Reflection; 4 | 5 | namespace Basic.CompilerLog.Util; 6 | 7 | public sealed class CompilerAssemblyData(string filePath, AssemblyName assemblyName, string? commitHash) 8 | { 9 | public string FilePath { get; } = filePath; 10 | public AssemblyName AssemblyName { get; } = assemblyName; 11 | public string? CommitHash { get; } = commitHash; 12 | 13 | [ExcludeFromCodeCoverage] 14 | public override string ToString() => $"{FilePath} {CommitHash}"; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/CompilerCall.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Net.Http.Headers; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | using Microsoft.CodeAnalysis.VisualBasic; 6 | 7 | namespace Basic.CompilerLog.Util; 8 | 9 | public enum CompilerCallKind 10 | { 11 | /// 12 | /// Standard compilation call that goes through the C# / VB targets 13 | /// 14 | Regular, 15 | 16 | /// 17 | /// Compilation to build a satellite assembly 18 | /// 19 | Satellite, 20 | 21 | /// 22 | /// Temporary assembly generated for WPF projects 23 | /// 24 | WpfTemporaryCompile, 25 | 26 | /// 27 | /// Compilation that occurs in the XAML pipeline to create a temporary assembly used 28 | /// to reflect on to generate types for the real compilation 29 | /// 30 | XamlPreCompile, 31 | 32 | /// 33 | /// Compilation that doesn't fit existing classifications 34 | /// 35 | Unknown 36 | } 37 | 38 | /// 39 | /// Represents a call to the compiler. The file paths and arguments provided here are correct 40 | /// for the machine on which the compiler was run. They cannot be relied on to be correct on 41 | /// machines where a compiler log is rehydrated. 42 | /// 43 | public sealed class CompilerCall 44 | { 45 | private readonly Lazy> _lazyArguments; 46 | 47 | public string ProjectFilePath { get; } 48 | public string? CompilerFilePath { get; } 49 | public CompilerCallKind Kind { get; } 50 | public string? TargetFramework { get; } 51 | public bool IsCSharp { get; } 52 | internal object? OwnerState { get; } 53 | public string ProjectFileName { get; } 54 | public string ProjectDirectory { get; } 55 | 56 | public bool IsVisualBasic => !IsCSharp; 57 | 58 | internal CompilerCall( 59 | string projectFilePath, 60 | string? compilerFilePath, 61 | CompilerCallKind kind, 62 | string? targetFramework, 63 | bool isCSharp, 64 | Lazy> arguments, 65 | object? ownerState = null) 66 | { 67 | CompilerFilePath = compilerFilePath; 68 | ProjectFilePath = projectFilePath; 69 | Kind = kind; 70 | TargetFramework = targetFramework; 71 | IsCSharp = isCSharp; 72 | OwnerState = ownerState; 73 | _lazyArguments = arguments; 74 | ProjectFileName = Path.GetFileName(ProjectFilePath); 75 | ProjectDirectory = Path.GetDirectoryName(ProjectFilePath)!; 76 | } 77 | 78 | internal CompilerCall( 79 | string projectFilePath, 80 | string? compilerFilePath = null, 81 | CompilerCallKind kind = CompilerCallKind.Regular, 82 | string? targetFramework = null, 83 | bool isCSharp = true, 84 | string[]? arguments = null, 85 | object? ownerState = null) 86 | : this( 87 | projectFilePath, 88 | compilerFilePath, 89 | kind, 90 | targetFramework, 91 | isCSharp, 92 | new Lazy>(() => arguments ?? []), 93 | ownerState) 94 | { 95 | } 96 | 97 | public string GetDiagnosticName() 98 | { 99 | var baseName = string.IsNullOrEmpty(TargetFramework) 100 | ? ProjectFileName 101 | : $"{ProjectFileName} ({TargetFramework})"; 102 | if (Kind != CompilerCallKind.Regular) 103 | { 104 | return $"{baseName} ({Kind})"; 105 | } 106 | 107 | return baseName; 108 | } 109 | 110 | public IReadOnlyCollection GetArguments() => _lazyArguments.Value; 111 | 112 | [ExcludeFromCodeCoverage] 113 | public override string ToString() => GetDiagnosticName(); 114 | } 115 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/CompilerCallData.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Diagnostics.CodeAnalysis; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.Emit; 5 | 6 | namespace Basic.CompilerLog.Util; 7 | 8 | public sealed class CompilerCallData( 9 | CompilerCall compilerCall, 10 | string assemblyFileName, 11 | string? outputDirectory, 12 | ParseOptions parseOptions, 13 | CompilationOptions compilationOptions, 14 | EmitOptions emitOptions) 15 | { 16 | public CompilerCall CompilerCall { get; } = compilerCall; 17 | public string AssemblyFileName { get; } = assemblyFileName; 18 | public string? OutputDirectory { get; } = outputDirectory; 19 | public ParseOptions ParseOptions { get; } = parseOptions; 20 | public CompilationOptions CompilationOptions { get; } = compilationOptions; 21 | public EmitOptions EmitOptions { get; } = emitOptions; 22 | 23 | [ExcludeFromCodeCoverage] 24 | public override string ToString() => CompilerCall.ToString(); 25 | } 26 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/CompilerCallReaderUtil.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Build.Logging.StructuredLogger; 2 | 3 | namespace Basic.CompilerLog.Util; 4 | 5 | public static class CompilerCallReaderUtil 6 | { 7 | /// 8 | /// Create an directly over the provided file path 9 | /// 10 | public static ICompilerCallReader Create(string filePath, BasicAnalyzerKind? basicAnalyzerKind = null, LogReaderState? logReaderState = null) 11 | { 12 | var ext = Path.GetExtension(filePath); 13 | return ext switch 14 | { 15 | ".binlog" => BinaryLogReader.Create(filePath, basicAnalyzerKind, logReaderState), 16 | ".complog" => CompilerLogReader.Create(filePath, basicAnalyzerKind, logReaderState), 17 | ".zip" => CreateFromZip(), 18 | _ => throw new ArgumentException($"Unrecognized extension: {ext}") 19 | }; 20 | 21 | ICompilerCallReader CreateFromZip() 22 | { 23 | if (CompilerLogUtil.TryCopySingleFileWithExtensionFromZip(filePath, ".complog") is { } c) 24 | { 25 | return CompilerLogReader.Create(c, basicAnalyzerKind, logReaderState, leaveOpen: false); 26 | } 27 | 28 | if (CompilerLogUtil.TryCopySingleFileWithExtensionFromZip(filePath, ".binlog") is { } b) 29 | { 30 | return BinaryLogReader.Create(b, basicAnalyzerKind, logReaderState, leaveOpen: false); 31 | } 32 | 33 | throw new Exception($"Could not find a .complog or .binlog file in {filePath}"); 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/CompilerLogException.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Build.Logging.StructuredLogger; 2 | 3 | namespace Basic.CompilerLog.Util; 4 | 5 | public sealed class CompilerLogException(string message, List? diagnostics = null) : Exception(message) 6 | { 7 | public IReadOnlyList Diagnostics { get; } = diagnostics ?? (IReadOnlyList)Array.Empty(); 8 | } 9 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/CompilerLogTextLoader.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.Text; 3 | 4 | namespace Basic.CompilerLog.Util; 5 | 6 | internal sealed class CompilerLogTextLoader : TextLoader 7 | { 8 | internal ICompilerCallReader Reader { get; } 9 | internal VersionStamp VersionStamp { get; } 10 | internal SourceTextData SourceTextData { get; } 11 | 12 | internal CompilerLogTextLoader(ICompilerCallReader reader, VersionStamp versionStamp, SourceTextData sourceTextData) 13 | { 14 | Reader = reader; 15 | VersionStamp = versionStamp; 16 | SourceTextData = sourceTextData; 17 | } 18 | 19 | public override Task LoadTextAndVersionAsync(LoadTextOptions options, CancellationToken cancellationToken) 20 | { 21 | SourceText sourceText; 22 | 23 | // The loader can operate on multiple threads due to the nature of solutions and 24 | // workspaces. Need to guard access here as the underlying data structures in the 25 | // reader are not safe for paralell reads. 26 | lock (Reader) 27 | { 28 | sourceText = Reader.ReadSourceText(SourceTextData); 29 | } 30 | 31 | var textAndVersion = TextAndVersion.Create(sourceText, VersionStamp, SourceTextData.FilePath); 32 | return Task.FromResult(textAndVersion); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/CompilerLogUtil.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.IO.Compression; 3 | using System.Text; 4 | 5 | namespace Basic.CompilerLog.Util; 6 | 7 | public readonly struct ConvertBinaryLogResult 8 | { 9 | public bool Succeeded { get; } 10 | 11 | /// 12 | /// The set of included in the log 13 | /// 14 | public List CompilerCalls { get; } 15 | 16 | /// 17 | /// The diagnostics produced during conversion 18 | /// 19 | public List Diagnostics { get; } 20 | 21 | public ConvertBinaryLogResult(bool succeeded, List compilerCalls, List diagnostics) 22 | { 23 | Succeeded = succeeded; 24 | CompilerCalls = compilerCalls; 25 | Diagnostics = diagnostics; 26 | } 27 | } 28 | 29 | public static class CompilerLogUtil 30 | { 31 | /// 32 | /// Opens or creates a valid compiler log stream from the provided file path. The file path 33 | /// must refer to a binary or compiler log 34 | /// 35 | public static Stream GetOrCreateCompilerLogStream(string filePath) 36 | { 37 | var ext = Path.GetExtension(filePath); 38 | if (ext is ".binlog") 39 | { 40 | var memoryStream = new MemoryStream(); 41 | using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); 42 | ConvertBinaryLog(fileStream, memoryStream); 43 | memoryStream.Position = 0; 44 | return memoryStream; 45 | } 46 | 47 | if (ext is ".zip") 48 | { 49 | var memoryStream = TryCopySingleFileWithExtensionFromZip(filePath, ".complog"); 50 | if (memoryStream is not null) 51 | { 52 | return memoryStream; 53 | } 54 | 55 | throw new Exception($"Could not find a .complog file in {filePath}"); 56 | } 57 | 58 | if (ext is ".complog") 59 | { 60 | return new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); 61 | } 62 | 63 | throw new Exception($"Unrecognized extension: {ext}"); 64 | } 65 | 66 | public static MemoryStream? TryCopySingleFileWithExtensionFromZip(string filePath, string ext) 67 | { 68 | Debug.Assert(ext.Length > 0 && ext[0] == '.', "Extension must start with a period"); 69 | 70 | using var zipArchive = ZipFile.OpenRead(filePath); 71 | var entry = zipArchive.Entries 72 | .Where(x => Path.GetExtension(x.FullName) == ext) 73 | .ToList(); 74 | 75 | if (entry.Count == 1) 76 | { 77 | var memoryStream = new MemoryStream(); 78 | using var entryStream = entry[0].Open(); 79 | entryStream.CopyTo(memoryStream); 80 | memoryStream.Position = 0; 81 | return memoryStream; 82 | } 83 | 84 | return null; 85 | } 86 | 87 | public static List ConvertBinaryLog(string binaryLogFilePath, string compilerLogFilePath, Func? predicate = null) 88 | { 89 | using var compilerLogStream = new FileStream(compilerLogFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None); 90 | using var binaryLogStream = new FileStream(binaryLogFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); 91 | return ConvertBinaryLog(binaryLogStream, compilerLogStream, predicate); 92 | } 93 | 94 | public static List ConvertBinaryLog(Stream binaryLogStream, Stream compilerLogStream, Func? predicate = null) 95 | { 96 | var diagnostics = new List(); 97 | if (!TryConvertBinaryLog(binaryLogStream, compilerLogStream, diagnostics, predicate)) 98 | { 99 | throw new CompilerLogException("Could not convert binary log", diagnostics); 100 | } 101 | 102 | return diagnostics; 103 | } 104 | 105 | public static bool TryConvertBinaryLog(Stream binaryLogStream, Stream compilerLogStream, List diagnostics, Func? predicate = null) 106 | { 107 | var result = TryConvertBinaryLog(binaryLogStream, compilerLogStream, predicate); 108 | diagnostics.AddRange(result.Diagnostics); 109 | return result.Succeeded; 110 | } 111 | 112 | public static ConvertBinaryLogResult TryConvertBinaryLog(string binaryLogFilePath, string compilerLogFilePath, Func? predicate = null) 113 | { 114 | using var compilerLogStream = new FileStream(compilerLogFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None); 115 | using var binaryLogStream = new FileStream(binaryLogFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); 116 | return TryConvertBinaryLog(binaryLogStream, compilerLogStream, predicate); 117 | } 118 | 119 | public static ConvertBinaryLogResult TryConvertBinaryLog(Stream binaryLogStream, Stream compilerLogStream, Func? predicate = null) => 120 | TryConvertBinaryLog(binaryLogStream, compilerLogStream, predicate, metadataVersion: null); 121 | 122 | internal static ConvertBinaryLogResult TryConvertBinaryLog(Stream binaryLogStream, Stream compilerLogStream, Func? predicate = null, int? metadataVersion = null) 123 | { 124 | predicate ??= static _ => true; 125 | var diagnostics = new List(); 126 | var included = new List(); 127 | 128 | var list = BinaryLogUtil.ReadAllCompilerCalls(binaryLogStream, predicate); 129 | using var builder = new CompilerLogBuilder(compilerLogStream, diagnostics, metadataVersion); 130 | var success = true; 131 | foreach (var compilerCall in list) 132 | { 133 | try 134 | { 135 | var commandLineArguments = BinaryLogUtil.ReadCommandLineArgumentsUnsafe(compilerCall); 136 | builder.AddFromDisk(compilerCall, commandLineArguments); 137 | included.Add(compilerCall); 138 | } 139 | catch (Exception ex) 140 | { 141 | diagnostics.Add($"Error adding {compilerCall.ProjectFilePath}: {ex.Message}"); 142 | success = false; 143 | } 144 | } 145 | 146 | return new ConvertBinaryLogResult(success, included, diagnostics); 147 | } 148 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/Data.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Security.Cryptography.X509Certificates; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CSharp.Syntax; 6 | using StructuredLogViewer; 7 | 8 | namespace Basic.CompilerLog.Util; 9 | 10 | public readonly struct AssemblyData(Guid mvid, string filePath) 11 | { 12 | public Guid Mvid { get; } = mvid; 13 | 14 | /// 15 | /// The file path for the given assembly 16 | /// 17 | /// 18 | /// This path is only valid on the machine where the log was generated. It's 19 | /// generally only useful for informational diagnostics. 20 | /// 21 | public string FilePath { get; } = filePath; 22 | } 23 | 24 | public sealed class ReferenceData( 25 | AssemblyIdentityData assemblyIdentityData, 26 | string filePath, 27 | ImmutableArray aliases, 28 | bool embedInteropTypes) 29 | { 30 | public AssemblyIdentityData AssemblyIdentityData { get; } = assemblyIdentityData; 31 | 32 | /// 33 | public string FilePath { get; } = filePath; 34 | public ImmutableArray Aliases { get; } = aliases; 35 | public bool EmbedInteropTypes { get; } = embedInteropTypes; 36 | 37 | public AssemblyData AssemblyData => new(Mvid, FilePath); 38 | public Guid Mvid => AssemblyIdentityData.Mvid; 39 | public string FileName => Path.GetFileName(FilePath); 40 | 41 | [ExcludeFromCodeCoverage] 42 | public override string ToString() => $"{FileName} {Mvid}"; 43 | } 44 | 45 | public sealed class AnalyzerData( 46 | AssemblyIdentityData assemblyIdentityData, 47 | string filePath) 48 | { 49 | public AssemblyIdentityData AssemblyIdentityData { get; } = assemblyIdentityData; 50 | 51 | /// 52 | public string FilePath { get; } = filePath; 53 | 54 | public AssemblyData AssemblyData => new(Mvid, FilePath); 55 | public Guid Mvid => AssemblyIdentityData.Mvid; 56 | public string FileName => Path.GetFileName(FilePath); 57 | 58 | [ExcludeFromCodeCoverage] 59 | public override string ToString() => $"{FileName} {Mvid}"; 60 | } 61 | 62 | public sealed class ResourceData( 63 | string contentHash, 64 | string? fileName, 65 | string name, 66 | bool isPublic) 67 | { 68 | public string ContentHash { get; } = contentHash; 69 | public string? FileName { get; } = fileName; 70 | public string Name { get; } = name; 71 | public bool IsPublic { get; } = isPublic; 72 | 73 | [ExcludeFromCodeCoverage] 74 | public override string ToString() => Name; 75 | } 76 | 77 | public readonly struct AssemblyFileData(string fileName, MemoryStream Image) 78 | { 79 | public string FileName { get; } = fileName; 80 | public MemoryStream Image { get; } = Image; 81 | 82 | [ExcludeFromCodeCoverage] 83 | public override string ToString() => FileName; 84 | } 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/EmitData.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections.Immutable; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace Basic.CompilerLog.Util; 11 | 12 | /// 13 | /// Data about a compilation that is only interesting at Emit time 14 | /// 15 | public sealed class EmitData 16 | { 17 | public string AssemblyFileName { get; } 18 | public string? XmlFilePath { get; } 19 | public bool EmitPdb { get; } 20 | public MemoryStream? Win32ResourceStream { get; } 21 | public MemoryStream? SourceLinkStream { get; } 22 | public IEnumerable? Resources { get; } 23 | public IEnumerable? EmbeddedTexts { get; } 24 | 25 | public EmitData( 26 | string assemblyFileName, 27 | string? xmlFilePath, 28 | bool emitPdb, 29 | MemoryStream? win32ResourceStream, 30 | MemoryStream? sourceLinkStream, 31 | IEnumerable? resources, 32 | IEnumerable? embeddedTexts) 33 | { 34 | AssemblyFileName = assemblyFileName; 35 | EmitPdb = emitPdb; 36 | XmlFilePath = xmlFilePath; 37 | Win32ResourceStream = win32ResourceStream; 38 | SourceLinkStream = sourceLinkStream; 39 | Resources = resources; 40 | EmbeddedTexts = embeddedTexts; 41 | } 42 | } 43 | 44 | public interface IEmitResult 45 | { 46 | public bool Success { get; } 47 | public ImmutableArray Diagnostics { get; } 48 | } 49 | 50 | 51 | public readonly struct EmitDiskResult : IEmitResult 52 | { 53 | public bool Success { get; } 54 | public string Directory { get; } 55 | public string AssemblyFileName { get; } 56 | public string AssemblyFilePath { get; } 57 | public string? PdbFilePath { get; } 58 | public string? XmlFilePath { get; } 59 | public string? MetadataFilePath { get; } 60 | public ImmutableArray Diagnostics { get; } 61 | 62 | public EmitDiskResult( 63 | bool success, 64 | string directory, 65 | string assemblyFileName, 66 | string? pdbFilePath, 67 | string? xmlFilePath, 68 | string? metadataFilePath, 69 | ImmutableArray diagnostics) 70 | { 71 | Success = success; 72 | Directory = directory; 73 | AssemblyFileName = assemblyFileName; 74 | AssemblyFilePath = Path.Combine(Directory, assemblyFileName); 75 | PdbFilePath = pdbFilePath; 76 | XmlFilePath = xmlFilePath; 77 | MetadataFilePath = metadataFilePath; 78 | Diagnostics = diagnostics; 79 | } 80 | } 81 | 82 | public readonly struct EmitMemoryResult : IEmitResult 83 | { 84 | public bool Success { get; } 85 | public MemoryStream AssemblyStream { get; } 86 | public MemoryStream? PdbStream { get; } 87 | public MemoryStream? XmlStream { get; } 88 | public MemoryStream? MetadataStream { get; } 89 | public ImmutableArray Diagnostics { get; } 90 | 91 | public EmitMemoryResult( 92 | bool success, 93 | MemoryStream assemblyStream, 94 | MemoryStream? pdbStream, 95 | MemoryStream? xmlStream, 96 | MemoryStream? metadataStream, 97 | ImmutableArray diagnostics) 98 | { 99 | Success = success; 100 | AssemblyStream = assemblyStream; 101 | PdbStream = pdbStream; 102 | XmlStream = xmlStream; 103 | MetadataStream = metadataStream; 104 | Diagnostics = diagnostics; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/IBasicAnalyzerHostDataProvider.cs: -------------------------------------------------------------------------------- 1 | 2 | using Microsoft.CodeAnalysis.Text; 3 | 4 | namespace Basic.CompilerLog.Util; 5 | 6 | internal interface IBasicAnalyzerHostDataProvider 7 | { 8 | public LogReaderState LogReaderState { get; } 9 | public void CopyAssemblyBytes(AssemblyData data, Stream stream); 10 | public byte[] GetAssemblyBytes(AssemblyData data); 11 | 12 | /// 13 | public List ReadAllAnalyzerData(CompilerCall compilerCall); 14 | 15 | /// 16 | public bool HasAllGeneratedFileContent(CompilerCall compilerCall); 17 | 18 | /// 19 | public List<(SourceText SourceText, string FilePath)> ReadAllGeneratedSourceTexts(CompilerCall compilerCall); 20 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/ICompilerCallReader.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.Emit; 3 | using Microsoft.CodeAnalysis.Text; 4 | 5 | namespace Basic.CompilerLog.Util; 6 | 7 | public interface ICompilerCallReader : IDisposable 8 | { 9 | public BasicAnalyzerKind BasicAnalyzerKind { get; } 10 | public LogReaderState LogReaderState { get; } 11 | public bool OwnsLogReaderState { get; } 12 | public CompilerCall ReadCompilerCall(int index); 13 | public List ReadAllCompilerCalls(Func? predicate = null); 14 | public List ReadAllCompilationData(Func? predicate = null); 15 | public CompilationData ReadCompilationData(CompilerCall compilerCall); 16 | public CompilerCallData ReadCompilerCallData(CompilerCall compilerCall); 17 | public SourceText ReadSourceText(SourceTextData sourceTextData); 18 | 19 | /// 20 | /// Read all of the for documents passed to the compilation 21 | /// 22 | public List ReadAllSourceTextData(CompilerCall compilerCall); 23 | 24 | /// 25 | /// Read all of the for references passed to the compilation 26 | /// 27 | public List ReadAllReferenceData(CompilerCall compilerCall); 28 | 29 | /// 30 | /// Read all of the for analyzers passed to the compilation 31 | /// 32 | public List ReadAllAnalyzerData(CompilerCall compilerCall); 33 | 34 | /// 35 | /// Read all of the compilers used in this build. 36 | /// 37 | public List ReadAllCompilerAssemblies(); 38 | 39 | public BasicAnalyzerHost CreateBasicAnalyzerHost(CompilerCall compilerCall); 40 | 41 | public bool TryGetCompilerCallIndex(Guid mvid, out int compilerCallIndex); 42 | 43 | /// 44 | /// Copy the bytes of the to the provided 45 | /// 46 | public void CopyAssemblyBytes(AssemblyData referenceData, Stream stream); 47 | 48 | public MetadataReference ReadMetadataReference(ReferenceData referenceData); 49 | 50 | /// 51 | /// Are all the generated files contained in the data? This should be true in the cases 52 | /// where there are provably no generated files (like a compilation without analyzers) 53 | /// 54 | /// 55 | /// This can fail in a few cases 56 | /// - Older compiler log versions don't encode all the data 57 | /// - Compilations using native PDBS don't have this capability 58 | /// 59 | public bool HasAllGeneratedFileContent(CompilerCall compilerCall); 60 | 61 | /// 62 | /// Read the set of generated sources from the compilation. This should only be called 63 | /// when returns true 64 | /// 65 | public List<(SourceText SourceText, string FilePath)> ReadAllGeneratedSourceTexts(CompilerCall compilerCall); 66 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/Impl/BasicAdditionalTextFile.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.Text; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Collections.Immutable; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace Basic.CompilerLog.Util.Impl; 11 | 12 | internal abstract class BasicAdditionalText(string filePath) : AdditionalText 13 | { 14 | public override string Path { get; } = filePath; 15 | public abstract ImmutableArray Diagnostics { get; } 16 | 17 | public abstract override SourceText? GetText(CancellationToken cancellationToken = default); 18 | } 19 | 20 | internal sealed class BasicAdditionalSourceText : BasicAdditionalText 21 | { 22 | private ImmutableArray _coreDiagnostics = []; 23 | public SourceText? SourceText { get; } 24 | public override ImmutableArray Diagnostics => _coreDiagnostics; 25 | 26 | internal BasicAdditionalSourceText(string filePath, SourceText? sourceText) 27 | : base(filePath) 28 | { 29 | SourceText = sourceText; 30 | } 31 | 32 | public override SourceText? GetText(CancellationToken cancellationToken = default) 33 | { 34 | if (SourceText is null && _coreDiagnostics.Length == 0) 35 | { 36 | _coreDiagnostics = [Diagnostic.Create(RoslynUtil.CannotReadFileDiagnosticDescriptor, Location.None, Path)]; 37 | } 38 | 39 | return SourceText; 40 | } 41 | } 42 | 43 | internal sealed class BasicAdditionalTextFile(string filePath, SourceHashAlgorithm checksumAlgorithm) 44 | : BasicAdditionalText(filePath) 45 | { 46 | private ImmutableArray _coreDiagnostics = []; 47 | public SourceHashAlgorithm ChecksumAlgorithm { get; } = checksumAlgorithm; 48 | public override ImmutableArray Diagnostics => _coreDiagnostics; 49 | 50 | public override SourceText? GetText(CancellationToken cancellationToken = default) => 51 | _coreDiagnostics.Length == 0 52 | ? RoslynUtil.TryGetSourceText(Path, ChecksumAlgorithm, canBeEmbedded: false, out _coreDiagnostics) 53 | : null; 54 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/Impl/BasicAnalyzerConfigOptionsProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.Diagnostics; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Collections.Immutable; 6 | using System.Diagnostics.CodeAnalysis; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace Basic.CompilerLog.Util.Impl; 12 | 13 | internal sealed class BasicAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider 14 | { 15 | internal sealed class BasicAnalyzerConfigOptions : AnalyzerConfigOptions 16 | { 17 | internal static readonly ImmutableDictionary EmptyDictionary = ImmutableDictionary.Create(KeyComparer); 18 | 19 | public static BasicAnalyzerConfigOptions Empty { get; } = new BasicAnalyzerConfigOptions(EmptyDictionary); 20 | 21 | // Note: Do not rename. Older versions of analyzers access this field via reflection. 22 | // https://github.com/dotnet/roslyn/blob/8e3d62a30b833631baaa4e84c5892298f16a8c9e/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Options/EditorConfig/EditorConfigStorageLocationExtensions.cs#L21 23 | internal readonly ImmutableDictionary Options; 24 | 25 | public BasicAnalyzerConfigOptions(ImmutableDictionary options) 26 | => Options = options; 27 | 28 | public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) 29 | => Options.TryGetValue(key, out value); 30 | } 31 | 32 | private readonly Dictionary _optionMap; 33 | 34 | public bool IsEmpty => _optionMap.Count == 0; 35 | public override AnalyzerConfigOptions GlobalOptions { get; } 36 | 37 | internal BasicAnalyzerConfigOptionsProvider( 38 | bool isConfigEmpty, 39 | AnalyzerConfigOptionsResult globalOptions, 40 | List<(object, AnalyzerConfigOptionsResult)> resultList) 41 | { 42 | GlobalOptions = isConfigEmpty 43 | ? BasicAnalyzerConfigOptions.Empty 44 | : new BasicAnalyzerConfigOptions(globalOptions.AnalyzerOptions); 45 | 46 | _optionMap = new(); 47 | if (!isConfigEmpty) 48 | { 49 | foreach (var tuple in resultList) 50 | { 51 | var options = tuple.Item2.AnalyzerOptions; 52 | if (options.Count > 0) 53 | { 54 | _optionMap[tuple.Item1] = new BasicAnalyzerConfigOptions(tuple.Item2.AnalyzerOptions); 55 | } 56 | } 57 | } 58 | } 59 | 60 | public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => 61 | _optionMap.TryGetValue(tree, out var options) ? options : BasicAnalyzerConfigOptions.Empty; 62 | 63 | public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => 64 | _optionMap.TryGetValue(textFile, out var options) ? options : BasicAnalyzerConfigOptions.Empty; 65 | } 66 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/Impl/BasicAnalyzerHostNone.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Diagnostics; 3 | using System.Diagnostics.CodeAnalysis; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.Diagnostics; 6 | using Microsoft.CodeAnalysis.Text; 7 | 8 | namespace Basic.CompilerLog.Util.Impl; 9 | 10 | /// 11 | /// This is the analyzer host which doesn't actually run generators / analyzers. Instead it 12 | /// uses the source texts that were generated at the original build time. 13 | /// 14 | internal sealed class BasicAnalyzerHostNone : BasicAnalyzerHost 15 | { 16 | internal List<(SourceText SourceText, string FilePath)> GeneratedSourceTexts { get; } 17 | 18 | protected override ImmutableArray AnalyzerReferencesCore { get; } 19 | 20 | /// 21 | /// This creates a host with a single analyzer that returns . This 22 | /// should be used if there is an analyzer even if it generated no files. 23 | /// 24 | /// 25 | internal BasicAnalyzerHostNone(List<(SourceText SourceText, string FilePath)> generatedSourceTexts) 26 | : base(BasicAnalyzerKind.None) 27 | { 28 | GeneratedSourceTexts = generatedSourceTexts; 29 | AnalyzerReferencesCore = [new BasicGeneratedFilesAnalyzerReference(generatedSourceTexts)]; 30 | } 31 | 32 | internal BasicAnalyzerHostNone(Diagnostic diagnostic) 33 | : this([]) 34 | { 35 | AnalyzerReferencesCore = [new BasicErrorAnalyzerReference(diagnostic)]; 36 | } 37 | 38 | /// 39 | /// This creates a none host with no analyzers. 40 | /// 41 | internal BasicAnalyzerHostNone() 42 | : base(BasicAnalyzerKind.None) 43 | { 44 | GeneratedSourceTexts = []; 45 | AnalyzerReferencesCore = []; 46 | } 47 | 48 | protected override void DisposeCore() 49 | { 50 | // Do nothing 51 | } 52 | } 53 | 54 | /// 55 | /// This _cannot_ be a file class. The full generated name is used in file paths of generated files. Those 56 | /// cannot include many characters that are in the full name of a file type. 57 | /// 58 | internal sealed class BasicGeneratedFilesAnalyzerReference(List<(SourceText SourceText, string FilePath)> generatedSourceTexts) : AnalyzerReference, IIncrementalGenerator, IBasicAnalyzerReference 59 | { 60 | internal List<(SourceText SourceText, string FilePath)> GeneratedSourceTexts { get; } = generatedSourceTexts; 61 | 62 | public override string? FullPath => null; 63 | 64 | [ExcludeFromCodeCoverage] 65 | public override object Id => this; 66 | 67 | public ImmutableArray GetAnalyzers(string language, List? diagnostics) => []; 68 | 69 | public override ImmutableArray GetAnalyzers(string language) => []; 70 | 71 | [ExcludeFromCodeCoverage] 72 | public override ImmutableArray GetAnalyzersForAllLanguages() => []; 73 | 74 | public override ImmutableArray GetGeneratorsForAllLanguages() => [this.AsSourceGenerator()]; 75 | 76 | public override ImmutableArray GetGenerators(string language) => GetGenerators(language, null); 77 | 78 | public ImmutableArray GetGenerators(string language, List? diagnostics) => [this.AsSourceGenerator()]; 79 | 80 | public void Initialize(IncrementalGeneratorInitializationContext context) 81 | { 82 | context.RegisterPostInitializationOutput(context => 83 | { 84 | var set = new HashSet(PathUtil.Comparer); 85 | foreach (var tuple in GeneratedSourceTexts) 86 | { 87 | var fileName = Path.GetFileName(tuple.FilePath); 88 | int count = 0; 89 | while (!set.Add(fileName)) 90 | { 91 | fileName = Path.Combine(count.ToString(), fileName); 92 | count++; 93 | } 94 | 95 | context.AddSource(fileName, tuple.SourceText); 96 | } 97 | }); 98 | } 99 | } 100 | 101 | internal sealed class BasicErrorAnalyzerReference(Diagnostic diagnostic) : AnalyzerReference, IBasicAnalyzerReference 102 | { 103 | [ExcludeFromCodeCoverage] 104 | public override string? FullPath => null; 105 | 106 | [ExcludeFromCodeCoverage] 107 | public override object Id => this; 108 | 109 | public ImmutableArray GetAnalyzers(string language, List? diagnostics) 110 | { 111 | diagnostics?.Add(diagnostic); 112 | return []; 113 | } 114 | 115 | public ImmutableArray GetGenerators(string language, List diagnostics) => []; 116 | 117 | public override ImmutableArray GetAnalyzers(string language) => GetAnalyzers(language, null); 118 | 119 | [ExcludeFromCodeCoverage] 120 | public override ImmutableArray GetAnalyzersForAllLanguages() => []; 121 | 122 | [ExcludeFromCodeCoverage] 123 | public override ImmutableArray GetGeneratorsForAllLanguages() => []; 124 | 125 | public override ImmutableArray GetGenerators(string language) => []; 126 | } 127 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/Impl/BasicSyntaxTreeOptionsProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections.Immutable; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Basic.CompilerLog.Util.Impl; 10 | 11 | internal class BasicSyntaxTreeOptionsProvider : SyntaxTreeOptionsProvider 12 | { 13 | internal static readonly ImmutableDictionary EmptyDiagnosticOptions = 14 | ImmutableDictionary.Create(CaseInsensitiveComparison.Comparer); 15 | 16 | internal readonly struct Options 17 | { 18 | public readonly GeneratedKind IsGenerated; 19 | public readonly ImmutableDictionary DiagnosticOptions; 20 | 21 | public Options(AnalyzerConfigOptionsResult? result) 22 | { 23 | if (result is AnalyzerConfigOptionsResult r) 24 | { 25 | DiagnosticOptions = r.TreeOptions; 26 | IsGenerated = GetIsGeneratedCodeFromOptions(r.AnalyzerOptions); 27 | } 28 | else 29 | { 30 | DiagnosticOptions = EmptyDiagnosticOptions; 31 | IsGenerated = GeneratedKind.Unknown; 32 | } 33 | } 34 | 35 | internal static GeneratedKind GetIsGeneratedCodeFromOptions(ImmutableDictionary options) 36 | { 37 | // Check for explicit user configuration for generated code. 38 | // generated_code = true | false 39 | if (options.TryGetValue("generated_code", out string? optionValue) && 40 | bool.TryParse(optionValue, out var boolValue)) 41 | { 42 | return boolValue ? GeneratedKind.MarkedGenerated : GeneratedKind.NotGenerated; 43 | } 44 | 45 | // Either no explicit user configuration or we don't recognize the option value. 46 | return GeneratedKind.Unknown; 47 | } 48 | } 49 | 50 | private readonly ImmutableDictionary _options; 51 | 52 | private readonly AnalyzerConfigOptionsResult _globalOptions; 53 | 54 | internal bool IsEmpty => _options.IsEmpty; 55 | 56 | internal BasicSyntaxTreeOptionsProvider( 57 | bool isConfigEmpty, 58 | AnalyzerConfigOptionsResult globalOptions, 59 | List<(object, AnalyzerConfigOptionsResult)> resultList) 60 | { 61 | var builder = ImmutableDictionary.CreateBuilder(); 62 | foreach (var tuple in resultList) 63 | { 64 | if (tuple.Item1 is SyntaxTree syntaxTree) 65 | { 66 | builder.Add(syntaxTree, new Options(isConfigEmpty ? null : tuple.Item2)); 67 | } 68 | } 69 | 70 | _options = builder.ToImmutableDictionary(); 71 | _globalOptions = globalOptions; 72 | } 73 | 74 | public override GeneratedKind IsGenerated(SyntaxTree tree, CancellationToken _) 75 | => _options.TryGetValue(tree, out var value) ? value.IsGenerated : GeneratedKind.Unknown; 76 | 77 | public override bool TryGetDiagnosticValue(SyntaxTree tree, string diagnosticId, CancellationToken _, out ReportDiagnostic severity) 78 | { 79 | if (_options.TryGetValue(tree, out var value)) 80 | { 81 | return value.DiagnosticOptions.TryGetValue(diagnosticId, out severity); 82 | } 83 | severity = ReportDiagnostic.Default; 84 | return false; 85 | } 86 | 87 | public override bool TryGetGlobalDiagnosticValue(string diagnosticId, CancellationToken _, out ReportDiagnostic severity) 88 | { 89 | if (_globalOptions.TreeOptions is object) 90 | { 91 | return _globalOptions.TreeOptions.TryGetValue(diagnosticId, out severity); 92 | } 93 | severity = ReportDiagnostic.Default; 94 | return false; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/Metadata.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Basic.CompilerLog.Util; 9 | 10 | internal sealed class Metadata 11 | { 12 | internal static readonly int LatestMetadataVersion = 3; 13 | 14 | internal int MetadataVersion { get; } 15 | internal int Count { get; } 16 | internal bool IsWindows { get; } 17 | 18 | private Metadata( 19 | int metadataVersion, 20 | int count, 21 | bool isWindows) 22 | { 23 | MetadataVersion = metadataVersion; 24 | Count = count; 25 | IsWindows = isWindows; 26 | } 27 | 28 | internal static Metadata Create(int count, int version) => 29 | new Metadata( 30 | version, 31 | count, 32 | RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); 33 | 34 | internal static Metadata Read(TextReader reader) 35 | { 36 | try 37 | { 38 | var line = reader.ReadLineOrThrow(); 39 | if (line.StartsWith("count", StringComparison.Ordinal)) 40 | { 41 | // This is a version 0, there is just a count method 42 | var count = ParseLine(line, "count", int.Parse); 43 | return new Metadata(metadataVersion: 0, count, isWindows: true); 44 | } 45 | else 46 | { 47 | var metadataVersion = ParseLine(line, "version", int.Parse); 48 | var count = ParseLine(reader.ReadLineOrThrow(), "count", int.Parse); 49 | var isWindows = ParseLine(reader.ReadLineOrThrow(), "windows", bool.Parse); 50 | return new Metadata( 51 | metadataVersion, 52 | count, 53 | isWindows); 54 | } 55 | } 56 | catch (Exception ex) 57 | { 58 | throw new InvalidOperationException("Cannot parse metadata", ex); 59 | } 60 | 61 | T ParseLine(string line, string label, Func func) 62 | { 63 | var items = line.Split(':', StringSplitOptions.RemoveEmptyEntries); 64 | if (items.Length != 2 || items[0] != label) 65 | throw new InvalidOperationException("Line has wrong format"); 66 | 67 | return func(items[1]); 68 | } 69 | } 70 | 71 | internal void Write(StreamWriter writer) 72 | { 73 | writer.WriteLine($"version:{MetadataVersion}"); 74 | writer.WriteLine($"count:{Count}"); 75 | writer.WriteLine($"windows:{RuntimeInformation.IsOSPlatform(OSPlatform.Windows)}"); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/MiscDirectory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Basic.CompilerLog.Util; 8 | 9 | /// 10 | /// Abstraction for getting new file paths for original paths in the compilation that existed 11 | /// outside the cone of the project. For paths like that it's important to keep the original 12 | /// directory structure. There are many parts of compilation that are hierarchical like 13 | /// editorconfig that require this. 14 | /// 15 | internal sealed class MiscDirectory(string baseDirectory) 16 | { 17 | private string BaseDirectory { get; } = baseDirectory; 18 | private Dictionary Map { get; } = new(PathUtil.Comparer); 19 | 20 | public string GetNewFilePath(string path) 21 | { 22 | if (Map.TryGetValue(path, out var newPath)) 23 | { 24 | return newPath; 25 | } 26 | 27 | var parent = Path.GetDirectoryName(path); 28 | if (parent is null) 29 | { 30 | return BaseDirectory; 31 | } 32 | 33 | var newParent = GetNewFilePath(parent); 34 | _ = Directory.CreateDirectory(newParent); 35 | newPath = Path.Combine(newParent, Path.GetFileName(path)); 36 | Map.Add(path, newPath); 37 | return newPath; 38 | } 39 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/PathNormalizationUtil.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Buffers; 3 | using System.Diagnostics; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Runtime.InteropServices; 6 | 7 | namespace Basic.CompilerLog.Util; 8 | 9 | internal abstract class PathNormalizationUtil 10 | { 11 | internal const string WindowsRoot = @"c:\code\"; 12 | internal const string UnixRoot = @"/code/"; 13 | 14 | internal const int MaxPathLength = 520; 15 | internal static PathNormalizationUtil Empty { get; } = new EmtpyNormalizationUtil(); 16 | internal static PathNormalizationUtil WindowsToUnix { get; } = new WindowsToUnixNormalizationUtil(UnixRoot); 17 | internal static PathNormalizationUtil UnixToWindows { get; } = new UnixToWindowsNormalizationUtil(WindowsRoot); 18 | 19 | /// 20 | /// Is the path rooted in the "from" platform 21 | /// 22 | /// 23 | /// 24 | internal abstract bool IsPathRooted([NotNullWhen(true)] string? path); 25 | 26 | /// 27 | /// Normalize the path from the "from" platform to the "to" platform 28 | /// 29 | /// 30 | /// 31 | [return: NotNullIfNotNull("path")] 32 | internal abstract string? NormalizePath(string? path); 33 | 34 | /// 35 | /// Make the file name an absolute path by putting it under the root 36 | /// 37 | internal abstract string RootFileName(string fileName); 38 | } 39 | 40 | /// 41 | /// This will normalize paths from Unix to Windows 42 | /// 43 | file sealed class WindowsToUnixNormalizationUtil(string root) : PathNormalizationUtil 44 | { 45 | internal string Root { get; } = root; 46 | 47 | internal override bool IsPathRooted([NotNullWhen(true)] string? path) => 48 | path != null && 49 | path.Length >= 2 && 50 | char.IsLetter(path[0]) && 51 | ':' == path[1]; 52 | 53 | [return: NotNullIfNotNull("path")] 54 | internal override string? NormalizePath(string? path) 55 | { 56 | if (path is null) 57 | { 58 | return null; 59 | } 60 | 61 | var array = ArrayPool.Shared.Rent(MaxPathLength); 62 | int arrayIndex = 0; 63 | int pathIndex = 0; 64 | if (IsPathRooted(path)) 65 | { 66 | Debug.Assert(Root[Root.Length-1]=='/'); 67 | 68 | Root.AsSpan().CopyTo(array.AsSpan()); 69 | arrayIndex += Root.Length; 70 | pathIndex += 2; 71 | 72 | // Move past any extra slashes after the c: portion of the path. 73 | while (pathIndex < path.Length && path[pathIndex] == '\\') 74 | { 75 | pathIndex++; 76 | } 77 | } 78 | 79 | while (pathIndex < path.Length) 80 | { 81 | if (path[pathIndex] == '\\') 82 | { 83 | array[arrayIndex++] = '/'; 84 | pathIndex++; 85 | while (pathIndex < path.Length && path[pathIndex] == '\\') 86 | { 87 | pathIndex++; 88 | } 89 | } 90 | else 91 | { 92 | array[arrayIndex++] = path[pathIndex++]; 93 | } 94 | } 95 | 96 | var normalizedPath = new string(array, 0, arrayIndex); 97 | ArrayPool.Shared.Return(array); 98 | return normalizedPath; 99 | } 100 | 101 | internal override string RootFileName(string fileName)=> Root + fileName; 102 | } 103 | 104 | file sealed class UnixToWindowsNormalizationUtil(string root) : PathNormalizationUtil 105 | { 106 | internal string Root { get; } = root; 107 | 108 | internal override bool IsPathRooted([NotNullWhen(true)] string? path) => 109 | path != null && 110 | path.Length > 0 && 111 | path[0] == '/'; 112 | 113 | [return: NotNullIfNotNull("path")] 114 | internal override string? NormalizePath(string? path) 115 | { 116 | if (path is null) 117 | { 118 | return null; 119 | } 120 | 121 | var array = ArrayPool.Shared.Rent(MaxPathLength); 122 | int arrayIndex = 0; 123 | int pathIndex = 0; 124 | if (IsPathRooted(path)) 125 | { 126 | Debug.Assert(Root[Root.Length-1]=='\\'); 127 | Root.AsSpan().CopyTo(array.AsSpan()); 128 | arrayIndex += Root.Length; 129 | pathIndex += 1; 130 | } 131 | 132 | while (pathIndex < path.Length) 133 | { 134 | if (path[pathIndex] == '/') 135 | { 136 | array[arrayIndex++] = '\\'; 137 | pathIndex++; 138 | } 139 | else 140 | { 141 | array[arrayIndex++] = path[pathIndex++]; 142 | } 143 | } 144 | 145 | var normalizedPath = new string(array, 0, arrayIndex); 146 | ArrayPool.Shared.Return(array); 147 | return normalizedPath; 148 | } 149 | 150 | internal override string RootFileName(string fileName)=> Root + fileName; 151 | } 152 | 153 | /// 154 | /// This is used when the current platform is the same as the platform that generated the log 155 | /// hence no normalization is needed. 156 | /// 157 | file sealed class EmtpyNormalizationUtil : PathNormalizationUtil 158 | { 159 | internal string Root { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? WindowsRoot : UnixRoot; 160 | 161 | internal override bool IsPathRooted(string? path) => Path.IsPathRooted(path); 162 | internal override string? NormalizePath(string? path) => path; 163 | 164 | internal override string RootFileName(string fileName)=> Root + fileName; 165 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/Polyfill.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.CompilerServices; 5 | using System.Runtime.InteropServices; 6 | using System.Text; 7 | using System.Text.RegularExpressions; 8 | using System.Threading.Tasks; 9 | 10 | namespace Basic.CompilerLog.Util 11 | { 12 | internal static class Polyfill 13 | { 14 | internal static StreamReader NewStreamReader(Stream stream, Encoding? encoding = null, bool detectEncodingFromByteOrderMarks = true, int bufferSize = -1, bool leaveOpen = false) 15 | { 16 | #if !NET 17 | if (bufferSize < 0) 18 | { 19 | bufferSize = 1024; 20 | } 21 | #endif 22 | return new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks, bufferSize, leaveOpen); 23 | } 24 | 25 | internal static StreamWriter NewStreamWriter(Stream stream, Encoding? encoding = null, int bufferSize = -1, bool leaveOpen = false) 26 | { 27 | #if !NET 28 | if (bufferSize < 0) 29 | { 30 | bufferSize = 1024; 31 | } 32 | #endif 33 | return new StreamWriter(stream, encoding, bufferSize, leaveOpen); 34 | } 35 | 36 | internal static unsafe ref T GetNonNullPinnableReference(Span span) => 37 | ref (span.Length != 0) 38 | ? ref MemoryMarshal.GetReference(span) 39 | : ref Unsafe.AsRef((void*)1); 40 | 41 | internal static unsafe ref T GetNonNullPinnableReference(ReadOnlySpan span) => 42 | ref (span.Length != 0) 43 | ? ref MemoryMarshal.GetReference(span) 44 | : ref Unsafe.AsRef((void*)1); 45 | } 46 | 47 | #if !NET 48 | 49 | internal static partial class PolyfillExtensions 50 | { 51 | internal static string[] Split(this string @this, char separator, StringSplitOptions options = StringSplitOptions.None) => 52 | @this.Split(new char[] { separator }, options); 53 | 54 | internal static string[] Split(this string @this, char separator, int count, StringSplitOptions options = StringSplitOptions.None) => 55 | @this.Split(new char[] { separator }, count, options); 56 | 57 | internal static void Append(this StringBuilder @this, ReadOnlySpan value) 58 | { 59 | foreach (var c in value) 60 | { 61 | @this.Append(c); 62 | } 63 | } 64 | 65 | internal static bool StartsWith(this ReadOnlySpan @this, string value, StringComparison comparisonType) => 66 | @this.StartsWith(value.AsSpan(), comparisonType); 67 | 68 | internal static bool Contains(this string @this, string value, StringComparison comparisonType) => 69 | @this.IndexOf(value, comparisonType) >= 0; 70 | 71 | internal static bool Contains(this ReadOnlySpan @this, char value) => 72 | @this.IndexOf(value) >= 0; 73 | 74 | internal static void ReadExactly(this Stream @this, Span buffer) 75 | { 76 | var bytes = new byte[1024]; 77 | while (buffer.Length > 0) 78 | { 79 | var read = @this.Read(bytes, 0, Math.Min(bytes.Length, buffer.Length)); 80 | if (read == 0) 81 | { 82 | throw new EndOfStreamException(); 83 | } 84 | 85 | bytes.AsSpan(0, read).CopyTo(buffer); 86 | buffer = buffer.Slice(read); 87 | } 88 | } 89 | 90 | internal static void Write(this TextWriter @this, ReadOnlySpan buffer) 91 | { 92 | for (int i = 0; i < buffer.Length; i++) 93 | { 94 | @this.Write(buffer[i]); 95 | } 96 | } 97 | 98 | internal static void WriteLine(this TextWriter @this, ReadOnlySpan buffer) 99 | { 100 | Write(@this, buffer); 101 | @this.WriteLine(); 102 | } 103 | 104 | internal static unsafe int GetByteCount(this Encoding @this, ReadOnlySpan chars) 105 | { 106 | fixed (char* charsPtr = &Polyfill.GetNonNullPinnableReference(chars)) 107 | { 108 | return @this.GetByteCount(charsPtr, chars.Length); 109 | } 110 | } 111 | 112 | internal static unsafe int GetBytes(this Encoding @this, ReadOnlySpan chars, Span bytes) 113 | { 114 | fixed (char* charsPtr = &Polyfill.GetNonNullPinnableReference(chars)) 115 | fixed (byte* bytesPtr = &Polyfill.GetNonNullPinnableReference(bytes)) 116 | { 117 | return @this.GetBytes(charsPtr, chars.Length, bytesPtr, bytes.Length); 118 | } 119 | } 120 | 121 | internal static bool IsMatch(this Regex @this, ReadOnlySpan input) => 122 | @this.IsMatch(input.ToString()); 123 | } 124 | 125 | #endif 126 | 127 | internal static partial class PolyfillExtensions 128 | { 129 | #if !NET9_0_OR_GREATER 130 | internal static IEnumerable<(int Index, T Item)> Index(this IEnumerable @this) => 131 | @this.Select((item, index) => (index, item)); 132 | #endif 133 | } 134 | 135 | } 136 | 137 | #if !NET 138 | 139 | namespace System.Diagnostics.CodeAnalysis 140 | { 141 | /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. 142 | [ExcludeFromCodeCoverage] 143 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] 144 | public sealed class NotNullWhenAttribute : Attribute 145 | { 146 | /// Initializes the attribute with the specified return value condition. 147 | /// 148 | /// The return value condition. If the method returns this value, the associated parameter will not be null. 149 | /// 150 | public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; 151 | 152 | /// Gets the return value condition. 153 | public bool ReturnValue { get; } 154 | } 155 | 156 | /// Specifies that the output will be non-null if the named parameter is non-null. 157 | [ExcludeFromCodeCoverage] 158 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] 159 | public sealed class NotNullIfNotNullAttribute : Attribute 160 | { 161 | /// Initializes the attribute with the associated parameter name. 162 | /// 163 | /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. 164 | /// 165 | public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; 166 | 167 | /// Gets the associated parameter name. 168 | public string ParameterName { get; } 169 | } 170 | } 171 | 172 | #endif 173 | 174 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/Properties.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("scratch, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f58668e135a441c5baccb85986c3165fce3e8695d91855fb9dbe1cf460f42469046edb62d9776158c45c6d622df2ea27aae4e19c3d7aea71d936a43332e46df7fc9eba67001118770f0d1894932877a029ad3f98f8b2e5a33699c7a884683b653ea4c4e8aa9f5dcac4b9dae925d338feb6461a14f42c52e6c6980850efd842c0")] 4 | [assembly: InternalsVisibleTo("Basic.CompilerLog.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f58668e135a441c5baccb85986c3165fce3e8695d91855fb9dbe1cf460f42469046edb62d9776158c45c6d622df2ea27aae4e19c3d7aea71d936a43332e46df7fc9eba67001118770f0d1894932877a029ad3f98f8b2e5a33699c7a884683b653ea4c4e8aa9f5dcac4b9dae925d338feb6461a14f42c52e6c6980850efd842c0")] -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/RawCompilationData.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.Text; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Collections.Immutable; 6 | using System.Configuration.Internal; 7 | using System.Diagnostics.CodeAnalysis; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | 12 | namespace Basic.CompilerLog.Util; 13 | 14 | internal enum RawContentKind 15 | { 16 | SourceText, 17 | GeneratedText, 18 | AdditionalText, 19 | AnalyzerConfig, 20 | Embed, 21 | 22 | /// 23 | /// This represents a #line directive target in a file that was embedded. These are different 24 | /// than normal line directives in that they are embedded into the compilation as well so the 25 | /// file is read from disk. 26 | /// 27 | EmbedLine, 28 | SourceLink, 29 | RuleSet, 30 | AppConfig, 31 | Win32Manifest, 32 | Win32Resource, 33 | Win32Icon, 34 | CryptoKeyFile, 35 | } 36 | 37 | internal readonly struct RawContent 38 | { 39 | internal string OriginalFilePath { get; } 40 | internal string NormalizedFilePath { get; } 41 | internal string? ContentHash { get; } 42 | internal RawContentKind Kind { get; } 43 | 44 | internal RawContent( 45 | string originalFilePath, 46 | string normalizedFilePath, 47 | string? contentHash, 48 | RawContentKind kind) 49 | { 50 | OriginalFilePath = originalFilePath; 51 | NormalizedFilePath = normalizedFilePath; 52 | ContentHash = contentHash; 53 | Kind = kind; 54 | } 55 | 56 | [ExcludeFromCodeCoverage] 57 | public override string ToString() => $"{Path.GetFileName(OriginalFilePath)} {Kind}"; 58 | } 59 | 60 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/ReflectionUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Basic.CompilerLog.Util; 9 | 10 | internal static class ReflectionUtil 11 | { 12 | internal static T ReadField(object obj, string fieldName, BindingFlags? bindingFlags = null) 13 | { 14 | var type = obj.GetType(); 15 | var fieldInfo = type.GetField(fieldName, bindingFlags ?? (BindingFlags.Instance | BindingFlags.NonPublic))!; 16 | var value = fieldInfo.GetValue(obj); 17 | return (T)value!; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/ResilientDirectory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Basic.CompilerLog.Util; 8 | 9 | /// 10 | /// Abstraction for getting new file paths for original paths in the compilation. 11 | /// 12 | internal sealed class ResilientDirectory 13 | { 14 | /// 15 | /// Content can exist outside the cone of the original project tree. That content 16 | /// is mapped, by original directory name, to a new directory in the output. This 17 | /// holds the map from the old directory to the new one. 18 | /// 19 | private Dictionary _map = new(PathUtil.Comparer); 20 | 21 | /// 22 | /// When doing flattening this holds the map of file name that was flattened to the 23 | /// path that it was flattened from. 24 | /// 25 | private Dictionary? _flattenedMap; 26 | 27 | internal string DirectoryPath { get; } 28 | 29 | /// 30 | /// When true will attempt to flatten the directory structure by writing files 31 | /// directly to the directory when possible. 32 | /// 33 | internal bool Flatten => _flattenedMap is not null; 34 | 35 | internal ResilientDirectory(string path, bool flatten = false) 36 | { 37 | DirectoryPath = path; 38 | Directory.CreateDirectory(DirectoryPath); 39 | if (flatten) 40 | { 41 | _flattenedMap = new(PathUtil.Comparer); 42 | } 43 | } 44 | 45 | internal string GetNewFilePath(string originalFilePath) 46 | { 47 | var fileName = Path.GetFileName(originalFilePath); 48 | if (_flattenedMap is not null) 49 | { 50 | if (!_flattenedMap.TryGetValue(fileName, out var sourcePath) || 51 | PathUtil.Comparer.Equals(sourcePath, originalFilePath)) 52 | { 53 | _flattenedMap[fileName] = originalFilePath; 54 | return Path.Combine(DirectoryPath, fileName); 55 | } 56 | } 57 | 58 | var key = Path.GetDirectoryName(originalFilePath)!; 59 | if (!_map.TryGetValue(key, out var dirPath)) 60 | { 61 | dirPath = Path.Combine(DirectoryPath, $"group{_map.Count}"); 62 | Directory.CreateDirectory(dirPath); 63 | _map[key] = dirPath; 64 | } 65 | 66 | return Path.Combine(dirPath, fileName); 67 | } 68 | 69 | internal string WriteContent(string originalFilePath, Stream stream) 70 | { 71 | var newFilePath = GetNewFilePath(originalFilePath); 72 | using var fileStream = new FileStream(newFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None); 73 | stream.CopyTo(fileStream); 74 | return newFilePath; 75 | } 76 | } 77 | 78 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/SdkUtil.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Build.Logging.StructuredLogger; 2 | using Microsoft.CodeAnalysis.CSharp.Syntax; 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Configuration.Internal; 7 | using System.Diagnostics; 8 | using System.Linq; 9 | using System.Runtime.InteropServices; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | namespace Basic.CompilerLog.Util; 14 | 15 | public static class SdkUtil 16 | { 17 | public static string GetDotnetDirectory(string? path = null) 18 | { 19 | // TODO: has to be a better way to find the runtime directory but this works for the moment 20 | path ??= Path.GetDirectoryName(typeof(object).Assembly.Location); 21 | while (path is not null && !IsDotNetDir(path)) 22 | { 23 | path = Path.GetDirectoryName(path); 24 | } 25 | 26 | if (path is null) 27 | { 28 | throw new Exception("Could not find dotnet directory"); 29 | } 30 | 31 | return path; 32 | 33 | static bool IsDotNetDir(string path) 34 | { 35 | var appName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) 36 | ? "dotnet.exe" 37 | : "dotnet"; 38 | 39 | return 40 | File.Exists(Path.Combine(path, appName)) && 41 | Directory.Exists(Path.Combine(path, "sdk")) && 42 | Directory.Exists(Path.Combine(path, "host")); 43 | } 44 | } 45 | 46 | public static List GetSdkDirectories(string? dotnetDirectory = null) 47 | { 48 | dotnetDirectory ??= GetDotnetDirectory(); 49 | var sdk = Path.Combine(dotnetDirectory, "sdk"); 50 | var sdks = new List(); 51 | foreach (var dir in Directory.EnumerateDirectories(sdk)) 52 | { 53 | var sdkDir = Path.Combine(dir, "Roslyn", "bincore"); 54 | if (Directory.Exists(sdkDir)) 55 | { 56 | sdks.Add(dir); 57 | } 58 | } 59 | 60 | return sdks; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/SolutionReader.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.Diagnostics; 3 | using Microsoft.CodeAnalysis.Text; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Collections.Immutable; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using System.Web; 11 | 12 | namespace Basic.CompilerLog.Util; 13 | 14 | public sealed class SolutionReader : IDisposable 15 | { 16 | private readonly SortedDictionary _indexToProjectDataMap; 17 | 18 | internal ICompilerCallReader Reader { get; } 19 | internal VersionStamp VersionStamp { get; } 20 | internal SolutionId SolutionId { get; } = SolutionId.CreateNewId(); 21 | 22 | public int ProjectCount => _indexToProjectDataMap.Count; 23 | 24 | internal SolutionReader(ICompilerCallReader reader, Func? predicate = null, VersionStamp? versionStamp = null) 25 | { 26 | Reader = reader; 27 | VersionStamp = versionStamp ?? VersionStamp.Default; 28 | 29 | predicate ??= static c => c.Kind == CompilerCallKind.Regular; 30 | var map = new SortedDictionary(); 31 | var compilerCalls = reader.ReadAllCompilerCalls(); 32 | for (int i = 0; i < compilerCalls.Count; i++) 33 | { 34 | var call = reader.ReadCompilerCall(i); 35 | if (predicate(call)) 36 | { 37 | var projectId = ProjectId.CreateNewId(debugName: i.ToString()); 38 | map[i] = (call, projectId); 39 | } 40 | } 41 | 42 | _indexToProjectDataMap = map; 43 | } 44 | 45 | public void Dispose() 46 | { 47 | Reader.Dispose(); 48 | } 49 | 50 | public static SolutionReader Create(Stream stream, BasicAnalyzerKind? basicAnalyzerKind = null, LogReaderState? state = null, bool leaveOpen = false, Func? predicate = null) => 51 | new (CompilerLogReader.Create(stream, basicAnalyzerKind, state, leaveOpen), predicate); 52 | 53 | public static SolutionReader Create(string filePath, BasicAnalyzerKind? basicAnalyzerKind = null, LogReaderState? state = null, Func? predicate = null) 54 | { 55 | var reader = CompilerCallReaderUtil.Create(filePath, basicAnalyzerKind, state); 56 | return new(reader, predicate); 57 | } 58 | 59 | public SolutionInfo ReadSolutionInfo() 60 | { 61 | var projectInfoList = new List(capacity: ProjectCount); 62 | foreach (var kvp in _indexToProjectDataMap) 63 | { 64 | projectInfoList.Add(ReadProjectInfo(kvp.Value.CompilerCall, kvp.Value.ProjectId)); 65 | } 66 | 67 | return SolutionInfo.Create(SolutionId, VersionStamp, projects: projectInfoList); 68 | } 69 | 70 | private ProjectInfo ReadProjectInfo(CompilerCall compilerCall, ProjectId projectId) 71 | { 72 | var documents = new List(); 73 | var additionalDocuments = new List(); 74 | var analyzerConfigDocuments = new List(); 75 | 76 | foreach (var sourceTextData in Reader.ReadAllSourceTextData(compilerCall)) 77 | { 78 | List list = sourceTextData.SourceTextKind switch 79 | { 80 | SourceTextKind.SourceCode => documents, 81 | SourceTextKind.AnalyzerConfig => analyzerConfigDocuments, 82 | SourceTextKind.AdditionalText => additionalDocuments, 83 | _ => throw new InvalidOperationException(), 84 | }; 85 | 86 | var fileName = sourceTextData.FilePath; 87 | var documentId = DocumentId.CreateNewId(projectId, debugName: fileName); 88 | list.Add(DocumentInfo.Create( 89 | documentId, 90 | fileName, 91 | loader: new CompilerLogTextLoader(Reader, VersionStamp, sourceTextData), 92 | filePath: sourceTextData.FilePath)); 93 | } 94 | 95 | var refTuple = ReadReferences(); 96 | var compilerCallData = Reader.ReadCompilerCallData(compilerCall); 97 | var basicAnalyzeHost = Reader.CreateBasicAnalyzerHost(compilerCall); 98 | var projectInfo = ProjectInfo.Create( 99 | projectId, 100 | VersionStamp, 101 | name: compilerCall.ProjectFileName, 102 | assemblyName: compilerCallData.AssemblyFileName, 103 | language: compilerCall.IsCSharp ? LanguageNames.CSharp : LanguageNames.VisualBasic, 104 | filePath: compilerCall.ProjectFilePath, 105 | outputFilePath: Path.Combine(compilerCallData.OutputDirectory ?? "", compilerCallData.AssemblyFileName), 106 | compilationOptions: compilerCallData.CompilationOptions, 107 | parseOptions: compilerCallData.ParseOptions, 108 | documents, 109 | refTuple.Item1, 110 | refTuple.Item2, 111 | analyzerReferences: basicAnalyzeHost.AnalyzerReferences, 112 | additionalDocuments, 113 | isSubmission: false, 114 | hostObjectType: null); 115 | 116 | return projectInfo.WithAnalyzerConfigDocuments(analyzerConfigDocuments); 117 | 118 | (List, List) ReadReferences() 119 | { 120 | // The command line compiler supports having the same reference added multiple times. It's actually 121 | // not uncommon for Microsoft.VisualBasic.dll to be passed twice when working on Visual Basic projects. 122 | // The workspaces layer though cannot handle duplicates hence we need to run a de-dupe pass here. 123 | var hashSet = new HashSet(); 124 | var projectReferences = new List(); 125 | var metadataReferences = new List(); 126 | foreach (var referenceData in Reader.ReadAllReferenceData(compilerCall)) 127 | { 128 | if (!hashSet.Add(referenceData.Mvid)) 129 | { 130 | continue; 131 | } 132 | 133 | if (Reader.TryGetCompilerCallIndex(referenceData.Mvid, out var refCompilerCallIndex)) 134 | { 135 | var refProjectId = _indexToProjectDataMap[refCompilerCallIndex].ProjectId; 136 | projectReferences.Add(new ProjectReference(refProjectId, referenceData.Aliases, referenceData.EmbedInteropTypes)); 137 | } 138 | else 139 | { 140 | var metadataReference = Reader.ReadMetadataReference(referenceData); 141 | metadataReferences.Add(metadataReference); 142 | } 143 | } 144 | 145 | return (projectReferences, metadataReferences); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/SourceTextData.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.Text; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace Basic.CompilerLog.Util; 5 | 6 | public enum SourceTextKind 7 | { 8 | SourceCode, 9 | AnalyzerConfig, 10 | AdditionalText, 11 | } 12 | 13 | public sealed class SourceTextData( 14 | object id, 15 | string filePath, 16 | SourceHashAlgorithm checksumAlgorithm, 17 | SourceTextKind sourceTextKind) 18 | { 19 | public object Id { get; } = id; 20 | public string FilePath { get; } = filePath; 21 | public SourceHashAlgorithm ChecksumAlgorithm { get; } = checksumAlgorithm; 22 | public SourceTextKind SourceTextKind { get; } = sourceTextKind; 23 | 24 | [ExcludeFromCodeCoverage] 25 | public override string ToString() => FilePath; 26 | } 27 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog.Util/StringStream.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Diagnostics; 3 | using System.Text; 4 | 5 | namespace Basic.CompilerLog.Util; 6 | 7 | internal sealed class StringStream(string content, Encoding encoding) : Stream 8 | { 9 | private readonly string _content = content; 10 | private readonly Encoding _encoding = encoding; 11 | private int _contentPosition; 12 | private int _bytePosition; 13 | 14 | // These fields come into play when we have to read portions of a character at a time 15 | private int _splitPosition = -1; 16 | private int _splitCount; 17 | private byte[] _splitBuffer = Array.Empty(); 18 | 19 | internal bool InSplit => _splitPosition >= 0; 20 | 21 | public override bool CanRead => true; 22 | public override bool CanSeek => false; 23 | public override bool CanWrite => false; 24 | public override long Length => throw new NotSupportedException(); 25 | 26 | public override long Position 27 | { 28 | get => _bytePosition; 29 | set 30 | { 31 | if (value != 0) 32 | { 33 | throw new NotSupportedException(); 34 | } 35 | 36 | _bytePosition = 0; 37 | _contentPosition = 0; 38 | _splitPosition = -1; 39 | } 40 | } 41 | 42 | public override void Flush() 43 | { 44 | } 45 | 46 | public override int Read(byte[] buffer, int offset, int count) => 47 | ReadCore(buffer.AsSpan(offset, count)); 48 | 49 | #if NET 50 | public override int Read(Span buffer) => 51 | ReadCore(buffer); 52 | #endif 53 | 54 | private int ReadCore(Span buffer) 55 | { 56 | if (buffer.Length == 0) 57 | { 58 | return 0; 59 | } 60 | 61 | if (_contentPosition >= _content.Length) 62 | { 63 | return 0; 64 | } 65 | 66 | return InSplit 67 | ? ReadFromSplitChar(buffer) 68 | : ReadFromString(buffer); 69 | } 70 | 71 | private int ReadFromSplitChar(Span buffer) 72 | { 73 | Debug.Assert(InSplit); 74 | Debug.Assert(buffer.Length > 0); 75 | Debug.Assert(_splitPosition >= 0); 76 | Debug.Assert(_splitCount > 0 && _splitCount <= _splitBuffer.Length); 77 | 78 | int count = Math.Min(_splitCount - _splitPosition, buffer.Length); 79 | _splitBuffer.AsSpan(_splitPosition, count).CopyTo(buffer); 80 | _splitPosition += count; 81 | if (_splitPosition == _splitBuffer.Length) 82 | { 83 | _splitPosition = -1; 84 | _contentPosition++; 85 | } 86 | 87 | return count; 88 | } 89 | 90 | private int ReadFromString(Span buffer) 91 | { 92 | Debug.Assert(!InSplit); 93 | Debug.Assert(buffer.Length > 0); 94 | Debug.Assert(_contentPosition < _content.Length); 95 | 96 | var charCount = Math.Min(_content.Length - _contentPosition, 512); 97 | do 98 | { 99 | var charSpan = _content.AsSpan(_contentPosition, charCount); 100 | var byteCount = _encoding.GetByteCount(charSpan); 101 | if (byteCount > buffer.Length) 102 | { 103 | if (charCount == 1) 104 | { 105 | // Buffer isn't big enough to hold a single character. Need to move into a split character 106 | // mode to handle this case. 107 | if (byteCount > _splitBuffer.Length) 108 | { 109 | _splitBuffer = new byte[byteCount]; 110 | } 111 | 112 | _splitPosition = 0; 113 | _splitCount = _encoding.GetBytes(_content.AsSpan(_contentPosition, 1), _splitBuffer); 114 | Debug.Assert(_splitCount <= _splitBuffer.Length); 115 | 116 | return ReadFromSplitChar(buffer); 117 | } 118 | 119 | charCount /= 2; 120 | continue; 121 | } 122 | 123 | var read = _encoding.GetBytes(charSpan, buffer); 124 | Debug.Assert(read == byteCount); 125 | _contentPosition += charCount; 126 | return read; 127 | } while (true); 128 | } 129 | 130 | public override long Seek(long offset, SeekOrigin origin) => 131 | throw new NotSupportedException(); 132 | 133 | public override void SetLength(long value) => 134 | throw new NotSupportedException(); 135 | 136 | public override void Write(byte[] buffer, int offset, int count) => 137 | throw new NotSupportedException(); 138 | } -------------------------------------------------------------------------------- /src/Basic.CompilerLog/Basic.CompilerLog.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0;net9.0 6 | enable 7 | LatestMajor 8 | true 9 | complog 10 | complog 11 | 12 | 16 | $(NoWarn);CS8002 17 | 18 | 19 | 42.42.42.42 20 | https://github.com/jaredpar/basic-compiler-logger 21 | https://github.com/jaredpar/basic-compiler-logger 22 | README.md 23 | LICENSE 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | <_BasicToolVersion>$(PackageVersion) 39 | <_BasicToolVersion Condition="'$(_BasicToolVersion)' == ''">42.42.42.42 40 | <_BasicGeneratedConstantsFile>$(IntermediateOutputPath)GeneratedConstants.cs 41 | <_BasicCode>internal static partial class Constants 42 | { 43 | public const string ToolVersion = "$(_BasicToolVersion)"%3B 44 | } 45 | 46 | 47 | 48 | 49 | 50 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog/Constants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Basic.CompilerLog.Util; 7 | 8 | internal static partial class Constants 9 | { 10 | internal const int ExitFailure = 1; 11 | internal const int ExitSuccess = 0; 12 | 13 | internal static string CurrentDirectory { get; set; } = Environment.CurrentDirectory; 14 | internal static string LocalAppDataDirectory { get; set; } = Path.Combine( 15 | Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), 16 | "Basic.CompilerLog"); 17 | internal static TextWriter Out { get; set; } = Console.Out; 18 | 19 | internal static Action OnCompilerCallReader = _ => { }; 20 | } 21 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp.Syntax; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Collections.Immutable; 6 | using System.IO.Compression; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | internal static class Extensions 12 | { 13 | internal static string GetFailureString(this Exception ex) 14 | { 15 | var builder = new StringBuilder(); 16 | builder.AppendLine(ex.Message); 17 | builder.AppendLine(ex.StackTrace); 18 | 19 | while (ex.InnerException is { } inner) 20 | { 21 | builder.AppendLine(); 22 | builder.AppendLine("Inner exception:"); 23 | builder.AppendLine(inner.Message); 24 | builder.AppendLine(inner.StackTrace); 25 | ex = inner; 26 | } 27 | 28 | return builder.ToString(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog/FilterOptionSet.cs: -------------------------------------------------------------------------------- 1 | using Basic.CompilerLog.Util; 2 | using Microsoft.CodeAnalysis.CSharp.Syntax; 3 | using Mono.Options; 4 | 5 | internal sealed class FilterOptionSet : OptionSet 6 | { 7 | private bool _hasAnalyzerOptions; 8 | private BasicAnalyzerKind _basicAnalyzerKind; 9 | 10 | internal List TargetFrameworks { get; } = new(); 11 | internal bool IncludeAllKinds { get; set; } 12 | internal List ProjectNames { get; } = new(); 13 | internal bool Help { get; set; } 14 | 15 | internal BasicAnalyzerKind BasicAnalyzerKind 16 | { 17 | get 18 | { 19 | CheckHasAnalyzerOptions(); 20 | return _basicAnalyzerKind; 21 | } 22 | } 23 | 24 | internal bool IncludeAnalyzers 25 | { 26 | get 27 | { 28 | CheckHasAnalyzerOptions(); 29 | return _basicAnalyzerKind != BasicAnalyzerKind.None; 30 | } 31 | } 32 | 33 | internal FilterOptionSet(bool analyzers = false) 34 | { 35 | Add("all", "include all compilation kinds", i => { if (i is not null) IncludeAllKinds = true; }); 36 | Add("f|framework=", "include only compilations for the target framework (allows multiple)", TargetFrameworks.Add); 37 | Add("p|project=", "include only compilations for the given project (allows multiple)", ProjectNames.Add); 38 | Add("h|help", "print help", h => { if (h != null) Help = true; }); 39 | 40 | if (analyzers) 41 | { 42 | _hasAnalyzerOptions = true; 43 | _basicAnalyzerKind = BasicAnalyzerHost.DefaultKind; 44 | Add("a|analyzers=", "analyzer load strategy: none, ondisk, inmemory", void (BasicAnalyzerKind k) => _basicAnalyzerKind = k); 45 | Add("n|none", "Do not run analyzers", i => { if (i is not null) _basicAnalyzerKind = BasicAnalyzerKind.None; }, hidden: true); 46 | } 47 | } 48 | 49 | private void CheckHasAnalyzerOptions() 50 | { 51 | if (!_hasAnalyzerOptions) 52 | { 53 | throw new InvalidOperationException(); 54 | } 55 | } 56 | 57 | internal bool FilterCompilerCalls(CompilerCall compilerCall) 58 | { 59 | if (compilerCall.Kind != CompilerCallKind.Regular && !IncludeAllKinds) 60 | { 61 | return false; 62 | } 63 | 64 | if (TargetFrameworks.Count > 0 && !TargetFrameworks.Contains(compilerCall.TargetFramework, StringComparer.OrdinalIgnoreCase)) 65 | { 66 | return false; 67 | } 68 | 69 | if (ProjectNames.Count > 0) 70 | { 71 | var name = Path.GetFileName(compilerCall.ProjectFilePath); 72 | var nameNoExtension = Path.GetFileNameWithoutExtension(compilerCall.ProjectFilePath); 73 | var comparer = PathUtil.Comparer; 74 | return ProjectNames.Any(x => comparer.Equals(x, name) || comparer.Equals(x, nameNoExtension)); 75 | } 76 | 77 | return true; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog/Properties.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("scratch, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f58668e135a441c5baccb85986c3165fce3e8695d91855fb9dbe1cf460f42469046edb62d9776158c45c6d622df2ea27aae4e19c3d7aea71d936a43332e46df7fc9eba67001118770f0d1894932877a029ad3f98f8b2e5a33699c7a884683b653ea4c4e8aa9f5dcac4b9dae925d338feb6461a14f42c52e6c6980850efd842c0")] 4 | [assembly: InternalsVisibleTo("Basic.CompilerLog.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f58668e135a441c5baccb85986c3165fce3e8695d91855fb9dbe1cf460f42469046edb62d9776158c45c6d622df2ea27aae4e19c3d7aea71d936a43332e46df7fc9eba67001118770f0d1894932877a029ad3f98f8b2e5a33699c7a884683b653ea4c4e8aa9f5dcac4b9dae925d338feb6461a14f42c52e6c6980850efd842c0")] 5 | -------------------------------------------------------------------------------- /src/Basic.CompilerLog/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "CompilerLogger": { 4 | "commandName": "Project", 5 | "commandLineArgs": "print -c \"C:\\Users\\jaredpar\\Downloads\\out_no_analyzers.binlog\" -p MiddleTier.Provider.csproj", 6 | "workingDirectory": "C:\\users\\jaredpar\\temp" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12.0 4 | embedded 5 | true 6 | $(MSBuildThisFileDirectory)..\artifacts 7 | $(MSBuildThisFileDirectory)key.snk 8 | true 9 | preview 10 | enable 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | true 5 | <_RoslynVersion>4.14.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Scratch/CompilerBenchmarks.cs: -------------------------------------------------------------------------------- 1 | using Basic.CompilerLog; 2 | using Basic.CompilerLog.Util; 3 | using BenchmarkDotNet.Attributes; 4 | using BenchmarkDotNet.Diagnostics.Windows.Configs; 5 | using Microsoft.CodeAnalysis; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Data; 9 | using System.Linq; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | #nullable disable 14 | 15 | namespace Scratch; 16 | 17 | [MemoryDiagnoser] 18 | public class CompilerBenchmark 19 | { 20 | public string TempDirectory { get; set; } 21 | public string CompilerLogPath { get; set; } 22 | 23 | [GlobalSetup] 24 | public void GenerateLog() 25 | { 26 | TempDirectory = Path.Combine(Path.GetTempPath(), nameof(CompilerBenchmark), Guid.NewGuid().ToString("N")); 27 | Directory.CreateDirectory(TempDirectory); 28 | DotnetUtil.Command($"new console --name example --output .", TempDirectory); 29 | DotnetUtil.Command($"build -bl", TempDirectory); 30 | CompilerLogPath = Path.Combine(TempDirectory, "build.complog"); 31 | CompilerLogUtil.ConvertBinaryLog(Path.Combine(TempDirectory, "msbuild.binlog"), CompilerLogPath); 32 | } 33 | 34 | [GlobalCleanup] 35 | public void Cleanup() 36 | { 37 | Directory.Delete(TempDirectory, recursive: true); 38 | } 39 | 40 | [ParamsAllValues] 41 | public BasicAnalyzerKind Kind { get; set; } 42 | 43 | /* 44 | [Benchmark] 45 | public void Emit() 46 | { 47 | var data = Reader.ReadCompilationData(0, Options); 48 | var compilation = data.GetCompilationAfterGenerators(); 49 | var stream = new MemoryStream(); 50 | var result = compilation.Emit( 51 | stream, 52 | options: data.EmitOptions); 53 | if (!result.Success) 54 | { 55 | throw new Exception("compilation failed"); 56 | } 57 | data.BasicAnalyzers.Dispose(); 58 | } 59 | */ 60 | 61 | [Benchmark] 62 | public void LoadAnalyzers() 63 | { 64 | using var reader = CompilerLogReader.Create(CompilerLogPath, Kind); 65 | var compilerCall = reader.ReadCompilerCall(0); 66 | var analyzers = reader.CreateBasicAnalyzerHost(compilerCall); 67 | foreach (var analyzer in analyzers.AnalyzerReferences) 68 | { 69 | _ = analyzer.GetAnalyzersForAllLanguages(); 70 | _ = analyzer.GetGeneratorsForAllLanguages(); 71 | } 72 | analyzers.Dispose(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Scratch/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Console": { 4 | "commandName": "Project", 5 | "commandLineArgs": "", 6 | "workingDirectory": "{workspaceFolder}", 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/Scratch/Scratch.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | 8 | 12 | $(NoWarn);CS8002 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Shared/DotnetUtil.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Build.Logging.StructuredLogger; 2 | using Microsoft.CodeAnalysis.CSharp.Syntax; 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Configuration.Internal; 7 | using System.Diagnostics; 8 | using System.Linq; 9 | using System.Runtime.InteropServices; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | namespace Basic.CompilerLog; 14 | 15 | internal static class DotnetUtil 16 | { 17 | private static readonly Lazy> _lazyDotnetEnvironmentVariables = new(CreateDotnetEnvironmentVariables); 18 | 19 | private static Dictionary CreateDotnetEnvironmentVariables() 20 | { 21 | // The CLI, particularly when run from dotnet test, will set the MSBuildSDKsPath environment variable 22 | // to point to the current SDK. That could be an SDK that is higher than the version that our tests 23 | // are executing under. For example `dotnet test` could spawn an 8.0 process but we end up testing 24 | // the 7.0.400 SDK. This environment variable though will point to 8.0 and end up causing load 25 | // issues. Clear it out here so that the `dotnet` commands have a fresh context. 26 | var map = new Dictionary(StringComparer.OrdinalIgnoreCase); 27 | foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables()) 28 | { 29 | var key = (string)entry.Key; 30 | if (!string.Equals(key, "MSBuildSDKsPath", StringComparison.OrdinalIgnoreCase)) 31 | { 32 | map.Add(key, (string)entry.Value!); 33 | 34 | } 35 | } 36 | return map; 37 | } 38 | 39 | internal static ProcessResult Command(string args, string? workingDirectory = null) => 40 | ProcessUtil.Run( 41 | "dotnet", 42 | args, 43 | workingDirectory: workingDirectory, 44 | environment: _lazyDotnetEnvironmentVariables.Value); 45 | } 46 | -------------------------------------------------------------------------------- /src/Shared/PathUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Basic.CompilerLog.Util; 8 | 9 | internal static class PathUtil 10 | { 11 | internal static readonly StringComparer Comparer = StringComparer.Ordinal; 12 | internal static readonly StringComparison Comparison = StringComparison.Ordinal; 13 | 14 | /// 15 | /// Replace the with inside of 16 | /// 17 | /// 18 | internal static string ReplacePathStart(string filePath, string oldStart, string newStart) 19 | { 20 | var str = RemovePathStart(filePath, oldStart); 21 | return Path.Combine(newStart, str); 22 | } 23 | 24 | internal static string RemovePathStart(string filePath, string start) 25 | { 26 | var str = filePath.Substring(start.Length); 27 | if (str.Length > 0 && str[0] == Path.DirectorySeparatorChar) 28 | { 29 | str = str.Substring(1); 30 | } 31 | 32 | return str; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Shared/ProcessUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Drawing.Printing; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using MessagePack.Formatters; 9 | 10 | namespace Basic.CompilerLog; 11 | 12 | internal readonly struct ProcessResult 13 | { 14 | internal int ExitCode { get; } 15 | internal string StandardOut { get; } 16 | internal string StandardError { get; } 17 | 18 | internal bool Succeeded => ExitCode == 0; 19 | 20 | internal ProcessResult(int exitCode, string standardOut, string standardError) 21 | { 22 | ExitCode = exitCode; 23 | StandardOut = standardOut; 24 | StandardError = standardError; 25 | } 26 | } 27 | 28 | internal static class ProcessUtil 29 | { 30 | internal static ProcessResult Run( 31 | string fileName, 32 | string args, 33 | string? workingDirectory = null, 34 | Dictionary? environment = null) 35 | { 36 | var info = new ProcessStartInfo() 37 | { 38 | FileName = fileName, 39 | Arguments = args, 40 | WorkingDirectory = workingDirectory, 41 | UseShellExecute = false, 42 | RedirectStandardOutput = true, 43 | RedirectStandardError = true, 44 | }; 45 | 46 | if (environment is not null) 47 | { 48 | info.Environment.Clear(); 49 | foreach (var tuple in environment) 50 | { 51 | info.Environment.Add(tuple.Key, tuple.Value); 52 | } 53 | } 54 | 55 | var process = Process.Start(info)!; 56 | var standardOut = process.StandardOutput.ReadToEnd(); 57 | var standardError = process.StandardError.ReadToEnd(); 58 | process.WaitForExit(); 59 | 60 | return new ProcessResult( 61 | process.ExitCode, 62 | standardOut, 63 | standardError); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredpar/complog/8ef8225190174df2315f5e84b47f372679f9e82d/src/key.snk --------------------------------------------------------------------------------