├── .config └── dotnet-tools.json ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .editorconfig ├── .gitattributes ├── .github ├── .editorconfig ├── actions │ └── publish-artifacts │ │ └── action.yaml ├── renovate.json └── workflows │ ├── build.yml │ ├── docs.yml │ ├── libtemplate-update.yml │ └── release.yml ├── .gitignore ├── .prettierrc.yaml ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── AssemblyRefScanner.sln ├── CONTRIBUTING.md ├── Directory.Build.props ├── Directory.Build.rsp ├── Directory.Build.targets ├── Directory.Packages.props ├── LICENSE ├── README.md ├── docfx ├── .gitignore ├── docfx.json ├── docs │ ├── features.md │ ├── getting-started.md │ └── toc.yml ├── index.md └── toc.yml ├── global.json ├── init.cmd ├── init.ps1 ├── nuget.config ├── settings.VisualStudio.json ├── src ├── .editorconfig ├── AssemblyInfo.cs ├── AssemblyInfo.vb ├── AssemblyRefScanner │ ├── ApiRefScanner.cs │ ├── AssemblyRefScanner.csproj │ ├── AssemblyReferenceScanner.cs │ ├── CustomAttributeTypeProvider.cs │ ├── DocId.cs │ ├── DocIdBuilder.cs │ ├── EmbeddedTypeScanner.cs │ ├── MultiVersionOfOneAssemblyNameScanner.cs │ ├── Program.cs │ ├── ResolveAssemblyReferences.cs │ ├── ScannerBase.cs │ ├── SignatureTypeProvider.cs │ ├── TargetFrameworkScanner.cs │ └── TypeRefScanner.cs ├── Directory.Build.props └── Directory.Build.targets ├── strongname.snk ├── stylecop.json ├── test ├── .editorconfig ├── AssemblyRefScanner.Tests │ ├── AssemblyRefScanner.Tests.csproj │ ├── DocIdBuilderTests.cs │ ├── DocIdParserTests.cs │ ├── DocIdSamples.cs │ ├── EmbeddedTypeScannerTests.cs │ └── app.config ├── Directory.Build.props └── Directory.Build.targets ├── tools ├── Check-DotNetRuntime.ps1 ├── Check-DotNetSdk.ps1 ├── Get-ArtifactsStagingDirectory.ps1 ├── Get-CodeCovTool.ps1 ├── Get-LibTemplateBasis.ps1 ├── Get-NuGetTool.ps1 ├── Get-ProcDump.ps1 ├── Get-SymbolFiles.ps1 ├── Get-TempToolsPath.ps1 ├── Install-DotNetSdk.ps1 ├── Install-NuGetCredProvider.ps1 ├── MergeFrom-Template.ps1 ├── Set-EnvVars.ps1 ├── artifacts │ ├── Variables.ps1 │ ├── _all.ps1 │ ├── _stage_all.ps1 │ ├── build_logs.ps1 │ ├── coverageResults.ps1 │ ├── deployables.ps1 │ ├── projectAssetsJson.ps1 │ ├── symbols.ps1 │ ├── testResults.ps1 │ └── test_symbols.ps1 ├── dotnet-test-cloud.ps1 ├── publish-CodeCov.ps1 ├── test.runsettings └── variables │ ├── DotNetSdkVersion.ps1 │ ├── _all.ps1 │ └── _define.ps1 └── version.json /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "powershell": { 6 | "version": "7.4.6", 7 | "commands": [ 8 | "pwsh" 9 | ], 10 | "rollForward": false 11 | }, 12 | "dotnet-coverage": { 13 | "version": "17.13.1", 14 | "commands": [ 15 | "dotnet-coverage" 16 | ], 17 | "rollForward": false 18 | }, 19 | "nbgv": { 20 | "version": "3.7.112", 21 | "commands": [ 22 | "nbgv" 23 | ], 24 | "rollForward": false 25 | }, 26 | "docfx": { 27 | "version": "2.78.2", 28 | "commands": [ 29 | "docfx" 30 | ], 31 | "rollForward": false 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Refer to https://hub.docker.com/_/microsoft-dotnet-sdk for available versions 2 | FROM mcr.microsoft.com/dotnet/sdk:9.0.101-noble 3 | 4 | # Installing mono makes `dotnet test` work without errors even for net472. 5 | # But installing it takes a long time, so it's excluded by default. 6 | #RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF 7 | #RUN echo "deb https://download.mono-project.com/repo/ubuntu stable-bionic main" | tee /etc/apt/sources.list.d/mono-official-stable.list 8 | #RUN apt-get update 9 | #RUN DEBIAN_FRONTEND=noninteractive apt-get install -y mono-devel 10 | 11 | # Clear the NUGET_XMLDOC_MODE env var so xml api doc files get unpacked, allowing a rich experience in Intellisense. 12 | # See https://github.com/dotnet/dotnet-docker/issues/2790 for a discussion on this, where the prioritized use case 13 | # was *not* devcontainers, sadly. 14 | ENV NUGET_XMLDOC_MODE= 15 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dev space", 3 | "dockerFile": "Dockerfile", 4 | "customizations": { 5 | "vscode": { 6 | "settings": { 7 | "terminal.integrated.shell.linux": "/usr/bin/pwsh" 8 | }, 9 | "extensions": [ 10 | "ms-azure-devops.azure-pipelines", 11 | "ms-dotnettools.csharp", 12 | "k--kato.docomment", 13 | "editorconfig.editorconfig", 14 | "esbenp.prettier-vscode", 15 | "pflannery.vscode-versionlens", 16 | "davidanson.vscode-markdownlint", 17 | "dotjoshjohnson.xml", 18 | "ms-vscode-remote.remote-containers", 19 | "ms-azuretools.vscode-docker", 20 | "tintoy.msbuild-project-tools" 21 | ] 22 | } 23 | }, 24 | "postCreateCommand": "./init.ps1 -InstallLocality machine" 25 | } 26 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome:http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Don't use tabs for indentation. 7 | [*] 8 | indent_style = space 9 | 10 | # (Please don't specify an indent_size here; that has too many unintended consequences.) 11 | 12 | [*.yml] 13 | indent_size = 2 14 | indent_style = space 15 | 16 | # Code files 17 | [*.{cs,csx,vb,vbx,h,cpp,idl}] 18 | indent_size = 4 19 | insert_final_newline = true 20 | trim_trailing_whitespace = true 21 | 22 | # MSBuild project files 23 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,msbuildproj,props,targets}] 24 | indent_size = 2 25 | 26 | # Xml config files 27 | [*.{ruleset,config,nuspec,resx,vsixmanifest,vsct,runsettings}] 28 | indent_size = 2 29 | indent_style = space 30 | 31 | # JSON files 32 | [*.json] 33 | indent_size = 2 34 | indent_style = space 35 | 36 | [*.ps1] 37 | indent_style = space 38 | indent_size = 4 39 | 40 | # Dotnet code style settings: 41 | [*.{cs,vb}] 42 | # Sort using and Import directives with System.* appearing first 43 | dotnet_sort_system_directives_first = true 44 | dotnet_separate_import_directive_groups = false 45 | dotnet_style_qualification_for_field = true:warning 46 | dotnet_style_qualification_for_property = true:warning 47 | dotnet_style_qualification_for_method = true:warning 48 | dotnet_style_qualification_for_event = true:warning 49 | 50 | # Use language keywords instead of framework type names for type references 51 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 52 | dotnet_style_predefined_type_for_member_access = true:suggestion 53 | 54 | # Suggest more modern language features when available 55 | dotnet_style_object_initializer = true:suggestion 56 | dotnet_style_collection_initializer = true:suggestion 57 | dotnet_style_coalesce_expression = true:suggestion 58 | dotnet_style_null_propagation = true:suggestion 59 | dotnet_style_explicit_tuple_names = true:suggestion 60 | 61 | # Non-private static fields are PascalCase 62 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion 63 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields 64 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style 65 | 66 | dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field 67 | dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected internal, private protected 68 | dotnet_naming_symbols.non_private_static_fields.required_modifiers = static 69 | 70 | dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case 71 | 72 | # Constants are PascalCase 73 | dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion 74 | dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants 75 | dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style 76 | 77 | dotnet_naming_symbols.constants.applicable_kinds = field, local 78 | dotnet_naming_symbols.constants.required_modifiers = const 79 | 80 | dotnet_naming_style.constant_style.capitalization = pascal_case 81 | 82 | # Static fields are camelCase 83 | dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion 84 | dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields 85 | dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style 86 | 87 | dotnet_naming_symbols.static_fields.applicable_kinds = field 88 | dotnet_naming_symbols.static_fields.required_modifiers = static 89 | 90 | dotnet_naming_style.static_field_style.capitalization = camel_case 91 | 92 | # Instance fields are camelCase 93 | dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion 94 | dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields 95 | dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style 96 | 97 | dotnet_naming_symbols.instance_fields.applicable_kinds = field 98 | 99 | dotnet_naming_style.instance_field_style.capitalization = camel_case 100 | 101 | # Locals and parameters are camelCase 102 | dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion 103 | dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters 104 | dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style 105 | 106 | dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local 107 | 108 | dotnet_naming_style.camel_case_style.capitalization = camel_case 109 | 110 | # Local functions are PascalCase 111 | dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion 112 | dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions 113 | dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style 114 | 115 | dotnet_naming_symbols.local_functions.applicable_kinds = local_function 116 | 117 | dotnet_naming_style.local_function_style.capitalization = pascal_case 118 | 119 | # By default, name items with PascalCase 120 | dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion 121 | dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members 122 | dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style 123 | 124 | dotnet_naming_symbols.all_members.applicable_kinds = * 125 | 126 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 127 | 128 | # CSharp code style settings: 129 | [*.cs] 130 | # Indentation preferences 131 | csharp_indent_block_contents = true 132 | csharp_indent_braces = false 133 | csharp_indent_case_contents = true 134 | csharp_indent_switch_labels = true 135 | csharp_indent_labels = flush_left 136 | 137 | # Prefer "var" everywhere 138 | csharp_style_var_for_built_in_types = false 139 | csharp_style_var_when_type_is_apparent = true:suggestion 140 | csharp_style_var_elsewhere = false:suggestion 141 | 142 | # Prefer method-like constructs to have a block body 143 | csharp_style_expression_bodied_methods = false:none 144 | csharp_style_expression_bodied_constructors = false:none 145 | csharp_style_expression_bodied_operators = false:none 146 | 147 | # Prefer property-like constructs to have an expression-body 148 | csharp_style_expression_bodied_properties = true:none 149 | csharp_style_expression_bodied_indexers = true:none 150 | csharp_style_expression_bodied_accessors = true:none 151 | 152 | # Suggest more modern language features when available 153 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 154 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 155 | csharp_style_inlined_variable_declaration = true:suggestion 156 | csharp_style_throw_expression = true:suggestion 157 | csharp_style_conditional_delegate_call = true:suggestion 158 | 159 | # Newline settings 160 | csharp_new_line_before_open_brace = all 161 | csharp_new_line_before_else = true 162 | csharp_new_line_before_catch = true 163 | csharp_new_line_before_finally = true 164 | csharp_new_line_before_members_in_object_initializers = true 165 | csharp_new_line_before_members_in_anonymous_types = true 166 | 167 | # Blocks are allowed 168 | csharp_prefer_braces = true:silent 169 | 170 | # SA1130: Use lambda syntax 171 | dotnet_diagnostic.SA1130.severity = silent 172 | 173 | # IDE1006: Naming Styles - StyleCop handles these for us 174 | dotnet_diagnostic.IDE1006.severity = none 175 | 176 | dotnet_diagnostic.DOC100.severity = silent 177 | dotnet_diagnostic.DOC104.severity = warning 178 | dotnet_diagnostic.DOC105.severity = warning 179 | dotnet_diagnostic.DOC106.severity = warning 180 | dotnet_diagnostic.DOC107.severity = warning 181 | dotnet_diagnostic.DOC108.severity = warning 182 | dotnet_diagnostic.DOC200.severity = warning 183 | dotnet_diagnostic.DOC202.severity = warning 184 | 185 | # CA1062: Validate arguments of public methods 186 | dotnet_diagnostic.CA1062.severity = warning 187 | 188 | # CA2016: Forward the CancellationToken parameter 189 | dotnet_diagnostic.CA2016.severity = warning 190 | 191 | [*.sln] 192 | indent_style = tab 193 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | # Ensure shell scripts use LF line endings (linux only accepts LF) 7 | *.sh eol=lf 8 | *.ps1 eol=lf 9 | 10 | # The macOS codesign tool is extremely picky, and requires LF line endings. 11 | *.plist eol=lf 12 | 13 | ############################################################################### 14 | # Set default behavior for command prompt diff. 15 | # 16 | # This is need for earlier builds of msysgit that does not have it on by 17 | # default for csharp files. 18 | # Note: This is only used by command line 19 | ############################################################################### 20 | #*.cs diff=csharp 21 | 22 | ############################################################################### 23 | # Set the merge driver for project and solution files 24 | # 25 | # Merging from the command prompt will add diff markers to the files if there 26 | # are conflicts (Merging from VS is not affected by the settings below, in VS 27 | # the diff markers are never inserted). Diff markers may cause the following 28 | # file extensions to fail to load in VS. An alternative would be to treat 29 | # these files as binary and thus will always conflict and require user 30 | # intervention with every merge. To do so, just uncomment the entries below 31 | ############################################################################### 32 | #*.sln merge=binary 33 | #*.csproj merge=binary 34 | #*.vbproj merge=binary 35 | #*.vcxproj merge=binary 36 | #*.vcproj merge=binary 37 | #*.dbproj merge=binary 38 | #*.fsproj merge=binary 39 | #*.lsproj merge=binary 40 | #*.wixproj merge=binary 41 | #*.modelproj merge=binary 42 | #*.sqlproj merge=binary 43 | #*.wwaproj merge=binary 44 | 45 | ############################################################################### 46 | # behavior for image files 47 | # 48 | # image files are treated as binary by default. 49 | ############################################################################### 50 | #*.jpg binary 51 | #*.png binary 52 | #*.gif binary 53 | 54 | ############################################################################### 55 | # diff behavior for common document formats 56 | # 57 | # Convert binary document formats to text before diffing them. This feature 58 | # is only available from the command line. Turn it on by uncommenting the 59 | # entries below. 60 | ############################################################################### 61 | #*.doc diff=astextplain 62 | #*.DOC diff=astextplain 63 | #*.docx diff=astextplain 64 | #*.DOCX diff=astextplain 65 | #*.dot diff=astextplain 66 | #*.DOT diff=astextplain 67 | #*.pdf diff=astextplain 68 | #*.PDF diff=astextplain 69 | #*.rtf diff=astextplain 70 | #*.RTF diff=astextplain 71 | -------------------------------------------------------------------------------- /.github/.editorconfig: -------------------------------------------------------------------------------- 1 | [renovate.json*] 2 | indent_style = tab 3 | -------------------------------------------------------------------------------- /.github/actions/publish-artifacts/action.yaml: -------------------------------------------------------------------------------- 1 | name: Publish artifacts 2 | description: Publish artifacts 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: 📥 Collect artifacts 8 | run: tools/artifacts/_stage_all.ps1 9 | shell: pwsh 10 | if: always() 11 | 12 | # TODO: replace this hard-coded list with a loop that utilizes the NPM package at 13 | # https://github.com/actions/toolkit/tree/main/packages/artifact (or similar) to push the artifacts. 14 | 15 | - name: 📢 Upload project.assets.json files 16 | if: always() 17 | uses: actions/upload-artifact@v4 18 | with: 19 | name: projectAssetsJson-${{ runner.os }} 20 | path: ${{ runner.temp }}/_artifacts/projectAssetsJson 21 | continue-on-error: true 22 | - name: 📢 Upload variables 23 | uses: actions/upload-artifact@v4 24 | with: 25 | name: variables-${{ runner.os }} 26 | path: ${{ runner.temp }}/_artifacts/Variables 27 | continue-on-error: true 28 | - name: 📢 Upload build_logs 29 | if: always() 30 | uses: actions/upload-artifact@v4 31 | with: 32 | name: build_logs-${{ runner.os }} 33 | path: ${{ runner.temp }}/_artifacts/build_logs 34 | continue-on-error: true 35 | - name: 📢 Upload test_logs 36 | if: always() 37 | uses: actions/upload-artifact@v4 38 | with: 39 | name: test_logs-${{ runner.os }} 40 | path: ${{ runner.temp }}/_artifacts/test_logs 41 | continue-on-error: true 42 | - name: 📢 Upload testResults 43 | if: always() 44 | uses: actions/upload-artifact@v4 45 | with: 46 | name: testResults-${{ runner.os }} 47 | path: ${{ runner.temp }}/_artifacts/testResults 48 | continue-on-error: true 49 | - name: 📢 Upload coverageResults 50 | if: always() 51 | uses: actions/upload-artifact@v4 52 | with: 53 | name: coverageResults-${{ runner.os }} 54 | path: ${{ runner.temp }}/_artifacts/coverageResults 55 | continue-on-error: true 56 | - name: 📢 Upload symbols 57 | uses: actions/upload-artifact@v4 58 | with: 59 | name: symbols-${{ runner.os }} 60 | path: ${{ runner.temp }}/_artifacts/symbols 61 | continue-on-error: true 62 | - name: 📢 Upload deployables 63 | uses: actions/upload-artifact@v4 64 | with: 65 | name: deployables-${{ runner.os }} 66 | path: ${{ runner.temp }}/_artifacts/deployables 67 | if: always() 68 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended"], 4 | "semanticCommits": "disabled", 5 | "labels": ["dependencies"], 6 | "packageRules": [ 7 | { 8 | "matchPackageNames": ["nbgv", "nerdbank.gitversioning"], 9 | "groupName": "nbgv and nerdbank.gitversioning updates" 10 | }, 11 | { 12 | "matchPackageNames": ["xunit*"], 13 | "groupName": "xunit" 14 | }, 15 | { 16 | "matchDatasources": ["dotnet-version", "docker"], 17 | "matchDepNames": ["dotnet-sdk", "mcr.microsoft.com/dotnet/sdk"], 18 | "groupName": "Dockerfile and global.json updates" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: 🏭 Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - 'v*.*' 8 | - validate/* 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | env: 13 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 14 | BUILDCONFIGURATION: Release 15 | # codecov_token: 4dc9e7e2-6b01-4932-a180-847b52b43d35 # Get a new one from https://codecov.io/ 16 | NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages/ 17 | 18 | jobs: 19 | build: 20 | name: 🏭 Build 21 | 22 | runs-on: ${{ matrix.os }} 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | os: 27 | - ubuntu-22.04 28 | #- macos-14 29 | #- windows-2022 30 | 31 | steps: 32 | - uses: actions/checkout@v4 33 | with: 34 | fetch-depth: 0 # avoid shallow clone so nbgv can do its work. 35 | - name: ⚙ Install prerequisites 36 | run: | 37 | ./init.ps1 -UpgradePrerequisites 38 | dotnet --info 39 | 40 | # Print mono version if it is present. 41 | if (Get-Command mono -ErrorAction SilentlyContinue) { 42 | mono --version 43 | } 44 | shell: pwsh 45 | - name: ⚙️ Set pipeline variables based on source 46 | run: tools/variables/_define.ps1 47 | shell: pwsh 48 | - name: 🛠 build 49 | run: dotnet build -t:build,pack --no-restore -c ${{ env.BUILDCONFIGURATION }} -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904 /bl:"${{ runner.temp }}/_artifacts/build_logs/build.binlog" 50 | - name: 🧪 test 51 | run: tools/dotnet-test-cloud.ps1 -Configuration ${{ env.BUILDCONFIGURATION }} -Agent ${{ runner.os }} 52 | shell: pwsh 53 | - name: 💅🏻 Verify formatted code 54 | run: dotnet format --verify-no-changes --no-restore 55 | shell: pwsh 56 | if: runner.os == 'Linux' 57 | - name: 📚 Verify docfx build 58 | run: dotnet docfx docfx/docfx.json --warningsAsErrors --disableGitFeatures 59 | if: runner.os == 'Linux' 60 | - name: ⚙ Update pipeline variables based on build outputs 61 | run: tools/variables/_define.ps1 62 | shell: pwsh 63 | - name: 📢 Publish artifacts 64 | uses: ./.github/actions/publish-artifacts 65 | if: cancelled() == false 66 | - name: 📢 Publish code coverage results to codecov.io 67 | run: ./tools/publish-CodeCov.ps1 -CodeCovToken "${{ env.codecov_token }}" -PathToCodeCoverage "${{ runner.temp }}/_artifacts/coverageResults" -Name "${{ runner.os }} Coverage Results" -Flags "${{ runner.os }}" 68 | shell: pwsh 69 | timeout-minutes: 3 70 | continue-on-error: true 71 | if: env.codecov_token != '' 72 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: 📚 Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 9 | permissions: 10 | actions: read 11 | pages: write 12 | id-token: write 13 | contents: read 14 | 15 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 16 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 17 | concurrency: 18 | group: pages 19 | cancel-in-progress: false 20 | 21 | jobs: 22 | publish-docs: 23 | environment: 24 | name: github-pages 25 | url: ${{ steps.deployment.outputs.page_url }} 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 0 # avoid shallow clone so nbgv can do its work. 31 | - name: ⚙ Install prerequisites 32 | run: ./init.ps1 -UpgradePrerequisites 33 | 34 | - run: dotnet docfx docfx/docfx.json 35 | name: 📚 Generate documentation 36 | 37 | - name: Upload artifact 38 | uses: actions/upload-pages-artifact@v3 39 | with: 40 | path: docfx/_site 41 | 42 | - name: Deploy to GitHub Pages 43 | id: deployment 44 | uses: actions/deploy-pages@v4 45 | -------------------------------------------------------------------------------- /.github/workflows/libtemplate-update.yml: -------------------------------------------------------------------------------- 1 | name: ⛜ Library.Template update 2 | 3 | # PREREQUISITE: This workflow requires the repo to be configured to allow workflows to create pull requests. 4 | # Visit https://github.com/USER/REPO/settings/actions 5 | # Under "Workflow permissions" check "Allow GitHub Actions to create ...pull requests" 6 | # Click Save. 7 | 8 | on: 9 | schedule: 10 | - cron: "0 3 * * Mon" # Sun @ 8 or 9 PM Mountain Time (depending on DST) 11 | workflow_dispatch: 12 | 13 | jobs: 14 | merge: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: write 18 | pull-requests: write 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 # avoid shallow clone so nbgv can do its work. 23 | 24 | - name: merge 25 | id: merge 26 | shell: pwsh 27 | run: | 28 | $LibTemplateBranch = & ./tools/Get-LibTemplateBasis.ps1 -ErrorIfNotRelated 29 | if ($LASTEXITCODE -ne 0) { 30 | exit $LASTEXITCODE 31 | } 32 | 33 | git fetch https://github.com/aarnott/Library.Template $LibTemplateBranch 34 | if ($LASTEXITCODE -ne 0) { 35 | exit $LASTEXITCODE 36 | } 37 | $LibTemplateCommit = git rev-parse FETCH_HEAD 38 | git diff --stat ...FETCH_HEAD 39 | 40 | if ((git rev-list FETCH_HEAD ^HEAD --count) -eq 0) { 41 | Write-Host "There are no Library.Template updates to merge." 42 | echo "uptodate=true" >> $env:GITHUB_OUTPUT 43 | exit 0 44 | } 45 | 46 | # Pushing commits that add or change files under .github/workflows will cause our workflow to fail. 47 | # But it usually isn't necessary because the target branch already has (or doesn't have) these changes. 48 | # So if the merged doesn't bring in any changes to these files, try the merge locally and push that 49 | # to keep github happy. 50 | if ((git rev-list FETCH_HEAD ^HEAD --count -- .github/workflows) -eq 0) { 51 | # Indeed there are no changes in that area. So merge locally to try to appease GitHub. 52 | git checkout -b auto/libtemplateUpdate 53 | git config user.name "Andrew Arnott" 54 | git config user.email "andrewarnott@live.com" 55 | git merge FETCH_HEAD 56 | if ($LASTEXITCODE -ne 0) { 57 | Write-Host "Merge conflicts prevent creating the pull request. Please run tools/MergeFrom-Template.ps1 locally and push the result as a pull request." 58 | exit 2 59 | } 60 | 61 | git -c http.extraheader="AUTHORIZATION: bearer $env:GH_TOKEN" push origin -u HEAD 62 | } else { 63 | Write-Host "Changes to github workflows are included in this update. Please run tools/MergeFrom-Template.ps1 locally and push the result as a pull request." 64 | exit 1 65 | } 66 | - name: pull request 67 | shell: pwsh 68 | if: success() && steps.merge.outputs.uptodate != 'true' 69 | run: | 70 | # If there is already an active pull request, don't create a new one. 71 | $existingPR = gh pr list -H auto/libtemplateUpdate --json url | ConvertFrom-Json 72 | if ($existingPR) { 73 | Write-Host "::warning::Skipping pull request creation because one already exists at $($existingPR[0].url)" 74 | exit 0 75 | } 76 | 77 | $prTitle = "Merge latest Library.Template" 78 | $prBody = "This merges the latest features and fixes from [Library.Template's branch](https://github.com/AArnott/Library.Template/tree/). 79 | 80 | ⚠️ Do **not** squash this pull request when completing it. You must *merge* it. 81 | 82 |
83 | Merge conflicts? 84 | Resolve merge conflicts locally by carrying out these steps: 85 | 86 | ``` 87 | git fetch 88 | git checkout auto/libtemplateUpdate 89 | git merge origin/main 90 | # resolve conflicts 91 | git commit 92 | git push 93 | ``` 94 |
" 95 | 96 | gh pr create -H auto/libtemplateUpdate -b $prBody -t $prTitle 97 | env: 98 | GH_TOKEN: ${{ github.token }} 99 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: 🎁 Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | inputs: 8 | ship_run_id: 9 | description: ID of the GitHub workflow run to ship 10 | required: true 11 | 12 | run-name: ${{ github.ref_name }} 13 | 14 | permissions: 15 | actions: read 16 | contents: write 17 | 18 | jobs: 19 | release: 20 | runs-on: ubuntu-24.04 21 | steps: 22 | - name: ⚙️ Initialization 23 | shell: pwsh 24 | run: | 25 | if ('${{ secrets.NUGET_API_KEY }}') { 26 | Write-Host "NUGET_API_KEY secret detected. NuGet packages will be pushed." 27 | echo "NUGET_API_KEY_DEFINED=true" >> $env:GITHUB_ENV 28 | } 29 | 30 | - name: 🔎 Search for build of ${{ github.ref }} 31 | shell: pwsh 32 | id: findrunid 33 | env: 34 | GH_TOKEN: ${{ github.token }} 35 | run: | 36 | if ('${{ inputs.ship_run_id }}') { 37 | $runid = '${{ inputs.ship_run_id }}' 38 | } else { 39 | $restApiRoot = '/repos/${{ github.repository }}' 40 | 41 | # Resolve the tag reference to a commit sha 42 | $resolvedRef = gh api ` 43 | -H "Accept: application/vnd.github+json" ` 44 | -H "X-GitHub-Api-Version: 2022-11-28" ` 45 | $restApiRoot/git/ref/tags/${{ github.ref_name }} ` 46 | | ConvertFrom-Json 47 | $commitSha = $resolvedRef.object.sha 48 | 49 | Write-Host "Resolved ${{ github.ref_name }} to $commitSha" 50 | 51 | $releases = gh run list -R ${{ github.repository }} -c $commitSha -w .github/workflows/build.yml -s success --json databaseId,startedAt ` 52 | | ConvertFrom-Json | Sort-Object startedAt -Descending 53 | 54 | if ($releases.length -eq 0) { 55 | Write-Error "No successful builds found for ${{ github.ref }}." 56 | } elseif ($releases.length -gt 1) { 57 | Write-Warning "More than one successful run found for ${{ github.ref }}. Artifacts from the most recent successful run will ship." 58 | } 59 | 60 | $runid = $releases[0].databaseId 61 | } 62 | 63 | Write-Host "Using artifacts from run-id: $runid" 64 | 65 | Echo "runid=$runid" >> $env:GITHUB_OUTPUT 66 | 67 | - name: 🔻 Download deployables artifacts 68 | uses: actions/download-artifact@v4 69 | with: 70 | name: deployables-Linux 71 | path: ${{ runner.temp }}/deployables 72 | run-id: ${{ steps.findrunid.outputs.runid }} 73 | github-token: ${{ github.token }} 74 | 75 | - name: 💽 Upload artifacts to release 76 | shell: pwsh 77 | if: ${{ github.event.release.assets_url }} != '' 78 | env: 79 | GH_TOKEN: ${{ github.token }} 80 | run: | 81 | Get-ChildItem '${{ runner.temp }}/deployables' |% { 82 | Write-Host "Uploading $($_.Name) to release..." 83 | gh release -R ${{ github.repository }} upload "${{ github.ref_name }}" $_.FullName 84 | } 85 | 86 | - name: 🚀 Push NuGet packages 87 | run: dotnet nuget push ${{ runner.temp }}/deployables/*.nupkg --source https://api.nuget.org/v3/index.json -k '${{ secrets.NUGET_API_KEY }}' 88 | if: ${{ env.NUGET_API_KEY_DEFINED == 'true' }} 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | *.lutconfig 13 | launchSettings.json 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Mono auto generated files 19 | mono_crash.* 20 | 21 | # Build results 22 | [Dd]ebug/ 23 | [Dd]ebugPublic/ 24 | [Rr]elease/ 25 | [Rr]eleases/ 26 | x64/ 27 | x86/ 28 | [Aa][Rr][Mm]/ 29 | [Aa][Rr][Mm]64/ 30 | bld/ 31 | [Bb]in/ 32 | [Oo]bj/ 33 | [Ll]og/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Jetbrains Rider cache directory 41 | .idea/ 42 | 43 | # Visual Studio 2017 auto generated files 44 | Generated\ Files/ 45 | 46 | # MSTest test Results 47 | [Tt]est[Rr]esult*/ 48 | [Bb]uild[Ll]og.* 49 | 50 | # NUnit 51 | *.VisualState.xml 52 | TestResult.xml 53 | nunit-*.xml 54 | 55 | # Build Results of an ATL Project 56 | [Dd]ebugPS/ 57 | [Rr]eleasePS/ 58 | dlldata.c 59 | 60 | # Benchmark Results 61 | BenchmarkDotNet.Artifacts/ 62 | 63 | # StyleCop 64 | StyleCopReport.xml 65 | 66 | # Files built by Visual Studio 67 | *_i.c 68 | *_p.c 69 | *_h.h 70 | *.ilk 71 | *.meta 72 | *.obj 73 | *.iobj 74 | *.pch 75 | *.pdb 76 | *.ipdb 77 | *.pgc 78 | *.pgd 79 | *.rsp 80 | !Directory.Build.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # JustCode is a .NET coding add-in 131 | .JustCode 132 | 133 | # TeamCity is a build add-in 134 | _TeamCity* 135 | 136 | # DotCover is a Code Coverage Tool 137 | *.dotCover 138 | 139 | # AxoCover is a Code Coverage Tool 140 | .axoCover/* 141 | !.axoCover/settings.json 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | /coveragereport/ 147 | 148 | # NCrunch 149 | _NCrunch_* 150 | .*crunch*.local.xml 151 | nCrunchTemp_* 152 | 153 | # MightyMoose 154 | *.mm.* 155 | AutoTest.Net/ 156 | 157 | # Web workbench (sass) 158 | .sass-cache/ 159 | 160 | # Installshield output folder 161 | [Ee]xpress/ 162 | 163 | # DocProject is a documentation generator add-in 164 | DocProject/buildhelp/ 165 | DocProject/Help/*.HxT 166 | DocProject/Help/*.HxC 167 | DocProject/Help/*.hhc 168 | DocProject/Help/*.hhk 169 | DocProject/Help/*.hhp 170 | DocProject/Help/Html2 171 | DocProject/Help/html 172 | 173 | # Click-Once directory 174 | publish/ 175 | 176 | # Publish Web Output 177 | *.[Pp]ublish.xml 178 | *.azurePubxml 179 | # Note: Comment the next line if you want to checkin your web deploy settings, 180 | # but database connection strings (with potential passwords) will be unencrypted 181 | *.pubxml 182 | *.publishproj 183 | 184 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 185 | # checkin your Azure Web App publish settings, but sensitive information contained 186 | # in these scripts will be unencrypted 187 | PublishScripts/ 188 | 189 | # NuGet Packages 190 | *.nupkg 191 | # NuGet Symbol Packages 192 | *.snupkg 193 | # The packages folder can be ignored because of Package Restore 194 | **/[Pp]ackages/* 195 | # except build/, which is used as an MSBuild target. 196 | !**/[Pp]ackages/build/ 197 | # Uncomment if necessary however generally it will be regenerated when needed 198 | #!**/[Pp]ackages/repositories.config 199 | # NuGet v3's project.json files produces more ignorable files 200 | *.nuget.props 201 | *.nuget.targets 202 | 203 | # Microsoft Azure Build Output 204 | csx/ 205 | *.build.csdef 206 | 207 | # Microsoft Azure Emulator 208 | ecf/ 209 | rcf/ 210 | 211 | # Windows Store app package directories and files 212 | AppPackages/ 213 | BundleArtifacts/ 214 | Package.StoreAssociation.xml 215 | _pkginfo.txt 216 | *.appx 217 | *.appxbundle 218 | *.appxupload 219 | 220 | # Visual Studio cache files 221 | # files ending in .cache can be ignored 222 | *.[Cc]ache 223 | # but keep track of directories ending in .cache 224 | !?*.[Cc]ache/ 225 | 226 | # Others 227 | ClientBin/ 228 | ~$* 229 | *~ 230 | *.dbmdl 231 | *.dbproj.schemaview 232 | *.jfm 233 | *.pfx 234 | *.publishsettings 235 | orleans.codegen.cs 236 | 237 | # Including strong name files can present a security risk 238 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 239 | #*.snk 240 | 241 | # Since there are multiple workflows, uncomment next line to ignore bower_components 242 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 243 | #bower_components/ 244 | 245 | # RIA/Silverlight projects 246 | Generated_Code/ 247 | 248 | # Backup & report files from converting an old project file 249 | # to a newer Visual Studio version. Backup files are not needed, 250 | # because we have git ;-) 251 | _UpgradeReport_Files/ 252 | Backup*/ 253 | UpgradeLog*.XML 254 | UpgradeLog*.htm 255 | ServiceFabricBackup/ 256 | *.rptproj.bak 257 | 258 | # SQL Server files 259 | *.mdf 260 | *.ldf 261 | *.ndf 262 | 263 | # Business Intelligence projects 264 | *.rdl.data 265 | *.bim.layout 266 | *.bim_*.settings 267 | *.rptproj.rsuser 268 | *- [Bb]ackup.rdl 269 | *- [Bb]ackup ([0-9]).rdl 270 | *- [Bb]ackup ([0-9][0-9]).rdl 271 | 272 | # Microsoft Fakes 273 | FakesAssemblies/ 274 | 275 | # GhostDoc plugin setting file 276 | *.GhostDoc.xml 277 | 278 | # Node.js Tools for Visual Studio 279 | .ntvs_analysis.dat 280 | node_modules/ 281 | 282 | # Visual Studio 6 build log 283 | *.plg 284 | 285 | # Visual Studio 6 workspace options file 286 | *.opt 287 | 288 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 289 | *.vbw 290 | 291 | # Visual Studio LightSwitch build output 292 | **/*.HTMLClient/GeneratedArtifacts 293 | **/*.DesktopClient/GeneratedArtifacts 294 | **/*.DesktopClient/ModelManifest.xml 295 | **/*.Server/GeneratedArtifacts 296 | **/*.Server/ModelManifest.xml 297 | _Pvt_Extensions 298 | 299 | # Paket dependency manager 300 | .paket/paket.exe 301 | paket-files/ 302 | 303 | # FAKE - F# Make 304 | .fake/ 305 | 306 | # CodeRush personal settings 307 | .cr/personal 308 | 309 | # Python Tools for Visual Studio (PTVS) 310 | __pycache__/ 311 | *.pyc 312 | 313 | # Cake - Uncomment if you are using it 314 | # tools/** 315 | # !tools/packages.config 316 | 317 | # Tabs Studio 318 | *.tss 319 | 320 | # Telerik's JustMock configuration file 321 | *.jmconfig 322 | 323 | # BizTalk build output 324 | *.btp.cs 325 | *.btm.cs 326 | *.odx.cs 327 | *.xsd.cs 328 | 329 | # OpenCover UI analysis results 330 | OpenCover/ 331 | 332 | # Azure Stream Analytics local run output 333 | ASALocalRun/ 334 | 335 | # MSBuild Binary and Structured Log 336 | *.binlog 337 | 338 | # NVidia Nsight GPU debugger configuration file 339 | *.nvuser 340 | 341 | # MFractors (Xamarin productivity tool) working folder 342 | .mfractor/ 343 | 344 | # Local History for Visual Studio 345 | .localhistory/ 346 | 347 | # BeatPulse healthcheck temp database 348 | healthchecksdb 349 | 350 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 351 | MigrationBackup/ 352 | 353 | # dotnet tool local install directory 354 | .store/ 355 | 356 | # mac-created file to track user view preferences for a directory 357 | .DS_Store 358 | 359 | # Analysis results 360 | *.sarif 361 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AArnott/AssemblyRefScanner/4f6a4c693d1c6bac0b0caaa233e77b39817b224e/.prettierrc.yaml -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | // List of extensions which should be recommended for users of this workspace. 5 | "recommendations": [ 6 | "ms-azure-devops.azure-pipelines", 7 | "ms-dotnettools.csharp", 8 | "k--kato.docomment", 9 | "editorconfig.editorconfig", 10 | "esbenp.prettier-vscode", 11 | "pflannery.vscode-versionlens", 12 | "davidanson.vscode-markdownlint", 13 | "dotjoshjohnson.xml", 14 | "ms-vscode-remote.remote-containers", 15 | "ms-azuretools.vscode-docker", 16 | "tintoy.msbuild-project-tools" 17 | ], 18 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 19 | "unwantedRecommendations": [] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Attach", 9 | "type": "coreclr", 10 | "request": "attach", 11 | "processId": "${command:pickProcess}" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.trimTrailingWhitespace": true, 3 | "files.insertFinalNewline": true, 4 | "files.trimFinalNewlines": true, 5 | "omnisharp.enableEditorConfigSupport": true, 6 | "omnisharp.enableRoslynAnalyzers": true, 7 | "dotnet.completion.showCompletionItemsFromUnimportedNamespaces": true, 8 | "editor.formatOnSave": true, 9 | "[xml]": { 10 | "editor.wordWrap": "off" 11 | }, 12 | // Treat these files as Azure Pipelines files 13 | "files.associations": { 14 | "**/azure-pipelines/**/*.yml": "azure-pipelines", 15 | "azure-pipelines.yml": "azure-pipelines" 16 | }, 17 | // Use Prettier as the default formatter for Azure Pipelines files. 18 | // Needs to be explicitly configured: https://github.com/Microsoft/azure-pipelines-vscode#document-formatting 19 | "[azure-pipelines]": { 20 | "editor.defaultFormatter": "esbenp.prettier-vscode", 21 | "editor.formatOnSave": false // enable this when they conform 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /.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}", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /AssemblyRefScanner.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31324.4 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AssemblyRefScanner", "src\AssemblyRefScanner\AssemblyRefScanner.csproj", "{29BAC4B9-86FE-4785-99E6-8EAF035EC3AE}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{336B0685-3599-4C91-A6A3-D75A1E8129F4}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | .gitattributes = .gitattributes 12 | .gitignore = .gitignore 13 | azure-pipelines.yml = azure-pipelines.yml 14 | Directory.Build.props = Directory.Build.props 15 | Directory.Build.rsp = Directory.Build.rsp 16 | Directory.Build.targets = Directory.Build.targets 17 | Directory.Packages.props = Directory.Packages.props 18 | global.json = global.json 19 | init.ps1 = init.ps1 20 | nuget.config = nuget.config 21 | README.md = README.md 22 | version.json = version.json 23 | EndProjectSection 24 | EndProject 25 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C430506A-5492-445A-B593-F9B39871D8D7}" 26 | ProjectSection(SolutionItems) = preProject 27 | src\.editorconfig = src\.editorconfig 28 | src\Directory.Build.props = src\Directory.Build.props 29 | src\Directory.Build.targets = src\Directory.Build.targets 30 | EndProjectSection 31 | EndProject 32 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{A3AD093D-2488-4E7C-922E-74EE4F9E5851}" 33 | ProjectSection(SolutionItems) = preProject 34 | test\.editorconfig = test\.editorconfig 35 | test\Directory.Build.props = test\Directory.Build.props 36 | test\Directory.Build.targets = test\Directory.Build.targets 37 | EndProjectSection 38 | EndProject 39 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AssemblyRefScanner.Tests", "test\AssemblyRefScanner.Tests\AssemblyRefScanner.Tests.csproj", "{932F5171-A4F5-4126-A2BD-A927B402C75D}" 40 | EndProject 41 | Global 42 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 43 | Debug|Any CPU = Debug|Any CPU 44 | Release|Any CPU = Release|Any CPU 45 | EndGlobalSection 46 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 47 | {29BAC4B9-86FE-4785-99E6-8EAF035EC3AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {29BAC4B9-86FE-4785-99E6-8EAF035EC3AE}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {29BAC4B9-86FE-4785-99E6-8EAF035EC3AE}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {29BAC4B9-86FE-4785-99E6-8EAF035EC3AE}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {932F5171-A4F5-4126-A2BD-A927B402C75D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {932F5171-A4F5-4126-A2BD-A927B402C75D}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {932F5171-A4F5-4126-A2BD-A927B402C75D}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {932F5171-A4F5-4126-A2BD-A927B402C75D}.Release|Any CPU.Build.0 = Release|Any CPU 55 | EndGlobalSection 56 | GlobalSection(SolutionProperties) = preSolution 57 | HideSolutionNode = FALSE 58 | EndGlobalSection 59 | GlobalSection(NestedProjects) = preSolution 60 | {29BAC4B9-86FE-4785-99E6-8EAF035EC3AE} = {C430506A-5492-445A-B593-F9B39871D8D7} 61 | {932F5171-A4F5-4126-A2BD-A927B402C75D} = {A3AD093D-2488-4E7C-922E-74EE4F9E5851} 62 | EndGlobalSection 63 | GlobalSection(ExtensibilityGlobals) = postSolution 64 | SolutionGuid = {B632FC96-1F9C-4DF5-9D90-EB3A2BF982DC} 65 | EndGlobalSection 66 | EndGlobal 67 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project has adopted the [Microsoft Open Source Code of 4 | Conduct](https://opensource.microsoft.com/codeofconduct/). 5 | For more information see the [Code of Conduct 6 | FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 7 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) 8 | with any additional questions or comments. 9 | 10 | ## Best practices 11 | 12 | * Use Windows PowerShell or [PowerShell Core][pwsh] (including on Linux/OSX) to run .ps1 scripts. 13 | Some scripts set environment variables to help you, but they are only retained if you use PowerShell as your shell. 14 | 15 | ## Prerequisites 16 | 17 | All dependencies can be installed by running the `init.ps1` script at the root of the repository 18 | using Windows PowerShell or [PowerShell Core][pwsh] (on any OS). 19 | Some dependencies installed by `init.ps1` may only be discoverable from the same command line environment the init script was run from due to environment variables, so be sure to launch Visual Studio or build the repo from that same environment. 20 | Alternatively, run `init.ps1 -InstallLocality Machine` (which may require elevation) in order to install dependencies at machine-wide locations so Visual Studio and builds work everywhere. 21 | 22 | The only prerequisite for building, testing, and deploying from this repository 23 | is the [.NET SDK](https://get.dot.net/). 24 | You should install the version specified in `global.json` or a later version within 25 | the same major.minor.Bxx "hundreds" band. 26 | For example if 2.2.300 is specified, you may install 2.2.300, 2.2.301, or 2.2.310 27 | while the 2.2.400 version would not be considered compatible by .NET SDK. 28 | See [.NET Core Versioning](https://docs.microsoft.com/dotnet/core/versions/) for more information. 29 | 30 | ## Package restore 31 | 32 | The easiest way to restore packages may be to run `init.ps1` which automatically authenticates 33 | to the feeds that packages for this repo come from, if any. 34 | `dotnet restore` or `nuget restore` also work but may require extra steps to authenticate to any applicable feeds. 35 | 36 | ## Building 37 | 38 | This repository can be built on Windows, Linux, and OSX. 39 | 40 | Building, testing, and packing this repository can be done by using the standard dotnet CLI commands (e.g. `dotnet build`, `dotnet test`, `dotnet pack`, etc.). 41 | 42 | [pwsh]: https://docs.microsoft.com/powershell/scripting/install/installing-powershell?view=powershell-6 43 | 44 | ## Releases 45 | 46 | Use `nbgv tag` to create a tag for a particular commit that you mean to release. 47 | [Learn more about `nbgv` and its `tag` and `prepare-release` commands](https://github.com/dotnet/Nerdbank.GitVersioning/blob/main/doc/nbgv-cli.md). 48 | 49 | Push the tag. 50 | 51 | ### GitHub Actions 52 | 53 | When your repo is hosted by GitHub and you are using GitHub Actions, you should create a GitHub Release using the standard GitHub UI. 54 | Having previously used `nbgv tag` and pushing the tag will help you identify the precise commit and name to use for this release. 55 | 56 | After publishing the release, the `.github\workflows\release.yml` workflow will be automatically triggered, which will: 57 | 58 | 1. Find the most recent `.github\workflows\build.yml` GitHub workflow run of the tagged release. 59 | 1. Upload the `deployables` artifact from that workflow run to your GitHub Release. 60 | 1. If you have `NUGET_API_KEY` defined as a secret variable for your repo or org, any nuget packages in the `deployables` artifact will be pushed to nuget.org. 61 | 62 | ### Azure Pipelines 63 | 64 | When your repo builds with Azure Pipelines, use the `azure-pipelines/release.yml` pipeline. 65 | Trigger the pipeline by adding the `auto-release` tag on a run of your main `azure-pipelines.yml` pipeline. 66 | 67 | ## Tutorial and API documentation 68 | 69 | API and hand-written docs are found under the `docfx/` directory. and are built by [docfx](https://dotnet.github.io/docfx/). 70 | 71 | You can make changes and host the site locally to preview them by switching to that directory and running the `dotnet docfx --serve` command. 72 | After making a change, you can rebuild the docs site while the localhost server is running by running `dotnet docfx` again from a separate terminal. 73 | 74 | The `.github/workflows/docs.yml` GitHub Actions workflow publishes the content of these docs to github.io if the workflow itself and [GitHub Pages is enabled for your repository](https://docs.github.com/en/pages/quickstart). 75 | 76 | ## Updating dependencies 77 | 78 | This repo uses Renovate to keep dependencies current. 79 | Configuration is in the `.github/renovate.json` file. 80 | [Learn more about configuring Renovate](https://docs.renovatebot.com/configuration-options/). 81 | 82 | When changing the renovate.json file, follow [these validation steps](https://docs.renovatebot.com/config-validation/). 83 | 84 | If Renovate is not creating pull requests when you expect it to, check that the [Renovate GitHub App](https://github.com/apps/renovate) is configured for your account or repo. 85 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | $(MSBuildThisFileDirectory) 6 | $(RepoRootPath)obj\$([MSBuild]::MakeRelative($(RepoRootPath), $(MSBuildProjectDirectory)))\ 7 | $(RepoRootPath)bin\$(MSBuildProjectName)\ 8 | $(RepoRootPath)bin\Packages\$(Configuration)\ 9 | enable 10 | enable 11 | latest 12 | true 13 | true 14 | true 15 | 16 | 17 | true 18 | 19 | 20 | 21 | false 22 | 23 | 24 | $(MSBuildThisFileDirectory) 25 | 26 | 27 | embedded 28 | 29 | true 30 | $(MSBuildThisFileDirectory)strongname.snk 31 | 32 | https://github.com/aarnott/assemblyrefscanner 33 | Andrew Arnott 34 | Andrew Arnott 35 | © Andrew Arnott. All rights reserved. 36 | MIT 37 | true 38 | true 39 | true 40 | snupkg 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | $(RepositoryUrl)/releases/tag/v$(Version) 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /Directory.Build.rsp: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # This file contains command-line options that MSBuild will process as part of 3 | # every build, unless the "/noautoresponse" switch is specified. 4 | # 5 | # MSBuild processes the options in this file first, before processing the 6 | # options on the command line. As a result, options on the command line can 7 | # override the options in this file. However, depending on the options being 8 | # set, the overriding can also result in conflicts. 9 | # 10 | # NOTE: The "/noautoresponse" switch cannot be specified in this file, nor in 11 | # any response file that is referenced by this file. 12 | #------------------------------------------------------------------------------ 13 | /nr:false 14 | /m 15 | /verbosity:minimal 16 | /clp:Summary;ForceNoAlign 17 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 5 | 16.9 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) COMPANY-PLACEHOLDER 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 | # AssemblyRefScanner 2 | 3 | [![NuGet package](https://img.shields.io/nuget/v/AssemblyRefScanner)](https://www.nuget.org/packages/assemblyrefscanner) 4 | [![🏭 Build](https://github.com/AArnott/AssemblyRefScanner/actions/workflows/build.yml/badge.svg)](https://github.com/AArnott/AssemblyRefScanner/actions/workflows/build.yml) 5 | 6 | This tool will very quickly scan an entire directory tree for all managed assemblies that reference interesting things, including: 7 | 8 | 1. A particular simple assembly name of interest. 9 | 1. Multiple references to the same assembly, but different versions, within the same assembly. For example if A.dll references B.dll v1.0.0.0 and B.dll v2.0.0.0, A.dll would be found. 10 | 1. A particular *type* (useful when making a breaking change). 11 | 12 | ## Usage 13 | 14 | Install or update the CLI tool with: 15 | 16 | ``` 17 | dotnet tool update -g AssemblyRefScanner 18 | ``` 19 | 20 | Then refer to the tool by its CLI name: `refscanner`: 21 | 22 | ``` 23 | PS> refscanner -h 24 | Description: 25 | AssemblyRefScanner v1.0.60-beta+a3320d6221 26 | 27 | Usage: 28 | refscanner [command] [options] 29 | 30 | Options: 31 | --version Show version information 32 | -?, -h, --help Show help and usage information 33 | 34 | Commands: 35 | assembly Searches for references to the assembly with the specified simple name. 36 | multiversions All assemblies that reference multiple versions of *any* assembly will be printed. 37 | embeddedTypes Searches for assemblies that have embedded types. 38 | api Searches for references to a given type or member. 39 | type Searches for references to a given type. 40 | targetFramework Groups all assemblies by TargetFramework. 41 | resolveReferences Lists paths to assemblies referenced by a given assembly. 42 | ``` 43 | 44 | You can then get usage help for a particular command: 45 | 46 | ``` 47 | PS> refscanner assembly -h 48 | assembly: 49 | Searches for references to the assembly with the specified simple name. 50 | 51 | Usage: 52 | AssemblyRefScanner assembly [options] 53 | 54 | Arguments: 55 | The simple assembly name (e.g. "StreamJsonRpc") to search for in referenced assembly lists. 56 | 57 | Options: 58 | --path The path of the directory to search. This should be a full install of VS (i.e. all workloads) to produce complete results. If not specified, the current directory will be searched. [default: 59 | C:\git\Helix] 60 | -?, -h, --help Show help and usage information 61 | ``` 62 | 63 | **Tip:** Enjoy completion of commands and switches at the command line by using PowerShell and taking [these steps](https://github.com/dotnet/command-line-api/blob/main/docs/dotnet-suggest.md). 64 | 65 | ### Samples 66 | 67 | #### Search for all references to StreamJsonRpc 68 | 69 | ``` 70 | PS> refscanner assembly streamjsonrpc --path "C:\Program Files (x86)\Microsoft Visual Studio\2019\master\Common7\IDE\" 71 | 72 | 1.2.0.0 73 | Extensions\Microsoft\LiveShare\Microsoft.VisualStudio.LanguageServer.Client.LiveShare.dll 74 | 75 | 1.3.0.0 76 | CommonExtensions\Microsoft\ModelBuilder\Microsoft.ML.ModelBuilder.dll 77 | Extensions\Microsoft\LiveShare\Microsoft.VisualStudio.LanguageServices.LanguageExtension.15.8.dll 78 | Extensions\Microsoft\LiveShare\Microsoft.VisualStudio.LanguageServices.LanguageExtension.16.0.dll 79 | Extensions\Microsoft\LiveShare\Microsoft.VisualStudio.LanguageServices.LanguageExtension.dll 80 | Extensions\Microsoft\LiveShare\Microsoft.VisualStudio.LiveShare.Core.dll 81 | Extensions\Microsoft\LiveShare\Microsoft.VisualStudio.LiveShare.Rpc.Json.dll 82 | CommonExtensions\Microsoft\ModelBuilder\AutoMLService\Microsoft.ML.ModelBuilder.AutoMLService.dll 83 | CommonExtensions\Microsoft\ModelBuilder\AzCopyService\Microsoft.ML.ModelBuilder.AzCopyService.dll 84 | ``` 85 | 86 | Above we see all the assemblies listed that reference StreamJsonRpc.dll, grouped by the version of StreamJsonRpc.dll that they reference. 87 | 88 | #### Search for multi-version references 89 | 90 | ``` 91 | PS> refscanner multiversions --path "C:\Program Files (x86)\Microsoft Visual Studio\2019\master\Common7\IDE\" 92 | 93 | CommonExtensions\Microsoft\LanguageServer\Microsoft.VisualStudio.LanguageServer.Client.dll 94 | StreamJsonRpc, Version=1.5.0.0, PublicKeyToken=b03f5f7f11d50a3a 95 | StreamJsonRpc, Version=2.4.0.0, PublicKeyToken=b03f5f7f11d50a3a 96 | 97 | CommonExtensions\Microsoft\LanguageServer\Microsoft.VisualStudio.LanguageServer.Client.Implementation.dll 98 | StreamJsonRpc, Version=2.4.0.0, PublicKeyToken=b03f5f7f11d50a3a 99 | StreamJsonRpc, Version=1.5.0.0, PublicKeyToken=b03f5f7f11d50a3a 100 | ``` 101 | 102 | Above we see that two assemblies each reference *two* versions of StreamJsonRpc simultaneously. 103 | -------------------------------------------------------------------------------- /docfx/.gitignore: -------------------------------------------------------------------------------- 1 | _site/ 2 | api/ 3 | -------------------------------------------------------------------------------- /docfx/docfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "src": [ 5 | { 6 | "src": "../src/AssemblyRefScanner", 7 | "files": [ 8 | "**/*.csproj" 9 | ] 10 | } 11 | ], 12 | "dest": "api" 13 | } 14 | ], 15 | "build": { 16 | "content": [ 17 | { 18 | "files": [ 19 | "**/*.{md,yml}" 20 | ], 21 | "exclude": [ 22 | "_site/**" 23 | ] 24 | } 25 | ], 26 | "resource": [ 27 | { 28 | "files": [ 29 | "images/**" 30 | ] 31 | } 32 | ], 33 | "xref": [ 34 | "https://learn.microsoft.com/en-us/dotnet/.xrefmap.json" 35 | ], 36 | "output": "_site", 37 | "template": [ 38 | "default", 39 | "modern" 40 | ], 41 | "globalMetadata": { 42 | "_appName": "AssemblyRefScanner", 43 | "_appTitle": "AssemblyRefScanner", 44 | "_enableSearch": true, 45 | "pdf": false 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docfx/docs/features.md: -------------------------------------------------------------------------------- 1 | # Features 2 | 3 | TODO 4 | -------------------------------------------------------------------------------- /docfx/docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Installation 4 | 5 | Consume this library via its NuGet Package. 6 | Click on the badge to find its latest version and the instructions for consuming it that best apply to your project. 7 | 8 | [![NuGet package](https://img.shields.io/nuget/v/Library.svg)](https://nuget.org/packages/Library) 9 | 10 | ## Usage 11 | 12 | TODO 13 | -------------------------------------------------------------------------------- /docfx/docs/toc.yml: -------------------------------------------------------------------------------- 1 | items: 2 | - name: Features 3 | href: features.md 4 | - name: Getting Started 5 | href: getting-started.md 6 | -------------------------------------------------------------------------------- /docfx/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | _layout: landing 3 | --- 4 | 5 | # Overview 6 | 7 | This is your docfx landing page. 8 | 9 | Click "Docs" across the top to get started. 10 | -------------------------------------------------------------------------------- /docfx/toc.yml: -------------------------------------------------------------------------------- 1 | items: 2 | - name: Docs 3 | href: docs/ 4 | - name: API 5 | href: api/ 6 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.101", 4 | "rollForward": "patch", 5 | "allowPrerelease": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /init.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | SETLOCAL 3 | set PS1UnderCmd=1 4 | 5 | :: Get the datetime in a format that can go in a filename. 6 | set _my_datetime=%date%_%time% 7 | set _my_datetime=%_my_datetime: =_% 8 | set _my_datetime=%_my_datetime::=% 9 | set _my_datetime=%_my_datetime:/=_% 10 | set _my_datetime=%_my_datetime:.=_% 11 | set CmdEnvScriptPath=%temp%\envvarscript_%_my_datetime%.cmd 12 | 13 | powershell.exe -NoProfile -NoLogo -ExecutionPolicy bypass -Command "try { & '%~dpn0.ps1' %*; exit $LASTEXITCODE } catch { write-host $_; exit 1 }" 14 | 15 | :: Set environment variables in the parent cmd.exe process. 16 | IF EXIST "%CmdEnvScriptPath%" ( 17 | ENDLOCAL 18 | CALL "%CmdEnvScriptPath%" 19 | DEL "%CmdEnvScriptPath%" 20 | ) 21 | -------------------------------------------------------------------------------- /init.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | <# 4 | .SYNOPSIS 5 | Installs dependencies required to build and test the projects in this repository. 6 | .DESCRIPTION 7 | This MAY not require elevation, as the SDK and runtimes are installed to a per-user location, 8 | unless the `-InstallLocality` switch is specified directing to a per-repo or per-machine location. 9 | See detailed help on that switch for more information. 10 | 11 | The CmdEnvScriptPath environment variable may be optionally set to a path to a cmd shell script to be created (or appended to if it already exists) that will set the environment variables in cmd.exe that are set within the PowerShell environment. 12 | This is used by init.cmd in order to reapply any new environment variables to the parent cmd.exe process that were set in the powershell child process. 13 | .PARAMETER InstallLocality 14 | A value indicating whether dependencies should be installed locally to the repo or at a per-user location. 15 | Per-user allows sharing the installed dependencies across repositories and allows use of a shared expanded package cache. 16 | Visual Studio will only notice and use these SDKs/runtimes if VS is launched from the environment that runs this script. 17 | Per-repo allows for high isolation, allowing for a more precise recreation of the environment within an Azure Pipelines build. 18 | When using 'repo', environment variables are set to cause the locally installed dotnet SDK to be used. 19 | Per-repo can lead to file locking issues when dotnet.exe is left running as a build server and can be mitigated by running `dotnet build-server shutdown`. 20 | Per-machine requires elevation and will download and install all SDKs and runtimes to machine-wide locations so all applications can find it. 21 | .PARAMETER NoPrerequisites 22 | Skips the installation of prerequisite software (e.g. SDKs, tools). 23 | .PARAMETER NoNuGetCredProvider 24 | Skips the installation of the NuGet credential provider. Useful in pipelines with the `NuGetAuthenticate` task, as a workaround for https://github.com/microsoft/artifacts-credprovider/issues/244. 25 | This switch is ignored and installation is skipped when -NoPrerequisites is specified. 26 | .PARAMETER UpgradePrerequisites 27 | Takes time to install prerequisites even if they are already present in case they need to be upgraded. 28 | No effect if -NoPrerequisites is specified. 29 | .PARAMETER NoRestore 30 | Skips the package restore step. 31 | .PARAMETER NoToolRestore 32 | Skips the dotnet tool restore step. 33 | .PARAMETER AccessToken 34 | An optional access token for authenticating to Azure Artifacts authenticated feeds. 35 | .PARAMETER Interactive 36 | Runs NuGet restore in interactive mode. This can turn authentication failures into authentication challenges. 37 | #> 38 | [CmdletBinding(SupportsShouldProcess = $true)] 39 | Param ( 40 | [ValidateSet('repo', 'user', 'machine')] 41 | [string]$InstallLocality = 'user', 42 | [Parameter()] 43 | [switch]$NoPrerequisites, 44 | [Parameter()] 45 | [switch]$NoNuGetCredProvider, 46 | [Parameter()] 47 | [switch]$UpgradePrerequisites, 48 | [Parameter()] 49 | [switch]$NoRestore, 50 | [Parameter()] 51 | [switch]$NoToolRestore, 52 | [Parameter()] 53 | [string]$AccessToken, 54 | [Parameter()] 55 | [switch]$Interactive 56 | ) 57 | 58 | $EnvVars = @{} 59 | $PrependPath = @() 60 | 61 | if (!$NoPrerequisites) { 62 | if (!$NoNuGetCredProvider) { 63 | & "$PSScriptRoot\tools\Install-NuGetCredProvider.ps1" -AccessToken $AccessToken -Force:$UpgradePrerequisites 64 | } 65 | 66 | & "$PSScriptRoot\tools\Install-DotNetSdk.ps1" -InstallLocality $InstallLocality 67 | if ($LASTEXITCODE -eq 3010) { 68 | Exit 3010 69 | } 70 | 71 | # The procdump tool and env var is required for dotnet test to collect hang/crash dumps of tests. 72 | # But it only works on Windows. 73 | if ($env:OS -eq 'Windows_NT') { 74 | $EnvVars['PROCDUMP_PATH'] = & "$PSScriptRoot\tools\Get-ProcDump.ps1" 75 | } 76 | } 77 | 78 | # Workaround nuget credential provider bug that causes very unreliable package restores on Azure Pipelines 79 | $env:NUGET_PLUGIN_HANDSHAKE_TIMEOUT_IN_SECONDS = 20 80 | $env:NUGET_PLUGIN_REQUEST_TIMEOUT_IN_SECONDS = 20 81 | 82 | Push-Location $PSScriptRoot 83 | try { 84 | $HeaderColor = 'Green' 85 | 86 | $RestoreArguments = @() 87 | if ($Interactive) { 88 | $RestoreArguments += '--interactive' 89 | } 90 | 91 | if (!$NoRestore -and $PSCmdlet.ShouldProcess("NuGet packages", "Restore")) { 92 | Write-Host "Restoring NuGet packages" -ForegroundColor $HeaderColor 93 | dotnet restore @RestoreArguments 94 | if ($lastexitcode -ne 0) { 95 | throw "Failure while restoring packages." 96 | } 97 | } 98 | 99 | if (!$NoToolRestore -and $PSCmdlet.ShouldProcess("dotnet tool", "restore")) { 100 | dotnet tool restore @RestoreArguments 101 | if ($lastexitcode -ne 0) { 102 | throw "Failure while restoring dotnet CLI tools." 103 | } 104 | } 105 | 106 | & "$PSScriptRoot/tools/Set-EnvVars.ps1" -Variables $EnvVars -PrependPath $PrependPath | Out-Null 107 | } 108 | catch { 109 | Write-Error $error[0] 110 | exit $lastexitcode 111 | } 112 | finally { 113 | Pop-Location 114 | } 115 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /settings.VisualStudio.json: -------------------------------------------------------------------------------- 1 | { 2 | "textEditor.codeCleanup.profile": "profile1" 3 | } 4 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | dotnet_diagnostic.SA1600.severity=suggestion -------------------------------------------------------------------------------- /src/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Runtime.InteropServices; 5 | 6 | [assembly: DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] 7 | -------------------------------------------------------------------------------- /src/AssemblyInfo.vb: -------------------------------------------------------------------------------- 1 | ' Copyright (c) COMPANY-PLACEHOLDER. All rights reserved. 2 | ' Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | Imports System.Runtime.InteropServices 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/AssemblyRefScanner/ApiRefScanner.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace AssemblyRefScanner; 5 | 6 | internal class ApiRefScanner : ScannerBase 7 | { 8 | internal required string Path { get; init; } 9 | 10 | internal required string? DeclaringAssembly { get; init; } 11 | 12 | internal required string[] DocIds { get; init; } 13 | 14 | internal async Task Execute(CancellationToken cancellationToken) 15 | { 16 | DocId.Descriptor[] descriptors = [.. this.DocIds.Select(DocId.Parse)]; 17 | var scanner = this.CreateProcessAssembliesBlock( 18 | mdReader => 19 | { 20 | // Skip assemblies that don't reference the declaring assembly. 21 | if (this.DeclaringAssembly is not null && !HasAssemblyReference(mdReader, this.DeclaringAssembly)) 22 | { 23 | return false; 24 | } 25 | 26 | return descriptors.Any(d => HasReferenceTo(mdReader, d)); 27 | }, 28 | cancellationToken); 29 | var report = this.CreateReportBlock( 30 | scanner, 31 | (assemblyPath, result) => 32 | { 33 | if (result) 34 | { 35 | Console.WriteLine(TrimBasePath(assemblyPath, this.Path)); 36 | } 37 | }, 38 | cancellationToken); 39 | return await this.Scan(this.Path, scanner, report, cancellationToken); 40 | } 41 | 42 | private static bool HasReferenceTo(MetadataReader referencingAssemblyReader, DocId.Descriptor api) 43 | { 44 | switch (api.Kind) 45 | { 46 | case DocId.ApiKind.Type: 47 | foreach (TypeReferenceHandle handle in referencingAssemblyReader.TypeReferences) 48 | { 49 | if (api.IsMatch(handle, referencingAssemblyReader)) 50 | { 51 | return true; 52 | } 53 | } 54 | 55 | break; 56 | case DocId.ApiKind.Method: 57 | case DocId.ApiKind.Property: 58 | case DocId.ApiKind.Field: 59 | case DocId.ApiKind.Event: 60 | foreach (MemberReferenceHandle handle in referencingAssemblyReader.MemberReferences) 61 | { 62 | if (api.IsMatch(handle, referencingAssemblyReader)) 63 | { 64 | return true; 65 | } 66 | } 67 | 68 | break; 69 | } 70 | 71 | return false; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/AssemblyRefScanner/AssemblyRefScanner.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | refscanner 4 | True 5 | Exe 6 | net8.0 7 | enable 8 | An assembly, type and member scanner. 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/AssemblyRefScanner/AssemblyReferenceScanner.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace AssemblyRefScanner; 5 | 6 | /// 7 | /// Scans for assemblies that reference a given assembly name, and prints a report grouped by referenced assembly version. 8 | /// 9 | internal class AssemblyReferenceScanner : ScannerBase 10 | { 11 | internal required string SimpleAssemblyName { get; init; } 12 | 13 | internal required string Path { get; init; } 14 | 15 | internal async Task Execute(CancellationToken cancellationToken) 16 | { 17 | var refReader = this.CreateProcessAssembliesBlock( 18 | mdReader => (from referenceHandle in mdReader.AssemblyReferences 19 | let reference = mdReader.GetAssemblyReference(referenceHandle).GetAssemblyName() 20 | group reference by reference.Name).ToImmutableDictionary(kv => kv.Key, kv => kv.ToImmutableArray(), StringComparer.OrdinalIgnoreCase), 21 | cancellationToken); 22 | 23 | var versionsReferenced = new Dictionary>(); 24 | var aggregator = this.CreateReportBlock( 25 | refReader, 26 | (assemblyPath, results) => 27 | { 28 | if (results.TryGetValue(this.SimpleAssemblyName, out ImmutableArray interestingRefs)) 29 | { 30 | foreach (var reference in interestingRefs) 31 | { 32 | if (reference.Version is null) 33 | { 34 | continue; 35 | } 36 | 37 | if (!versionsReferenced.TryGetValue(reference.Version, out List? referencingPaths)) 38 | { 39 | versionsReferenced.Add(reference.Version, referencingPaths = new List()); 40 | } 41 | 42 | referencingPaths.Add(assemblyPath); 43 | } 44 | } 45 | }, 46 | cancellationToken); 47 | 48 | int exitCode = await this.Scan(this.Path, startingBlock: refReader, terminalBlock: aggregator, cancellationToken); 49 | if (exitCode == 0) 50 | { 51 | Console.WriteLine($"The {this.SimpleAssemblyName} assembly is referenced as follows:"); 52 | foreach (var item in versionsReferenced.OrderBy(kv => kv.Key)) 53 | { 54 | Console.WriteLine(item.Key); 55 | foreach (var referencingPath in item.Value) 56 | { 57 | Console.WriteLine($"\t{TrimBasePath(referencingPath, this.Path)}"); 58 | } 59 | 60 | Console.WriteLine(); 61 | } 62 | } 63 | 64 | return exitCode; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/AssemblyRefScanner/CustomAttributeTypeProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace AssemblyRefScanner; 5 | 6 | internal class CustomAttributeTypeProvider : ICustomAttributeTypeProvider 7 | { 8 | public Type GetPrimitiveType(PrimitiveTypeCode typeCode) 9 | { 10 | return typeCode switch 11 | { 12 | PrimitiveTypeCode.String => typeof(string), 13 | PrimitiveTypeCode.Boolean => typeof(bool), 14 | PrimitiveTypeCode.Byte => typeof(byte), 15 | PrimitiveTypeCode.Char => typeof(char), 16 | PrimitiveTypeCode.Single => typeof(float), 17 | PrimitiveTypeCode.Double => typeof(double), 18 | _ => throw new NotImplementedException(), 19 | }; 20 | } 21 | 22 | public Type GetSystemType() => typeof(Type); 23 | 24 | public Type GetSZArrayType(Type elementType) 25 | { 26 | throw new NotImplementedException(); 27 | } 28 | 29 | public Type GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) 30 | { 31 | throw new NotImplementedException(); 32 | } 33 | 34 | public Type GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) 35 | { 36 | throw new NotImplementedException(); 37 | } 38 | 39 | public Type GetTypeFromSerializedName(string name) 40 | { 41 | throw new NotImplementedException(); 42 | } 43 | 44 | public PrimitiveTypeCode GetUnderlyingEnumType(Type type) 45 | { 46 | throw new NotImplementedException(); 47 | } 48 | 49 | public bool IsSystemType(Type type) => type == typeof(Type); 50 | } 51 | -------------------------------------------------------------------------------- /src/AssemblyRefScanner/DocId.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using Microsoft; 5 | 6 | namespace AssemblyRefScanner; 7 | 8 | /// 9 | /// Parses a DocId into its component parts and a matcher. 10 | /// 11 | public class DocId 12 | { 13 | /// 14 | /// Classifies the type of an API. 15 | /// 16 | public enum ApiKind 17 | { 18 | /// 19 | /// The API is a type. 20 | /// 21 | Type, 22 | 23 | /// 24 | /// The API is a method. 25 | /// 26 | Method, 27 | 28 | /// 29 | /// The API is a property. 30 | /// 31 | Property, 32 | 33 | /// 34 | /// The API is a field. 35 | /// 36 | Field, 37 | 38 | /// 39 | /// The API is an event. 40 | /// 41 | Event, 42 | } 43 | 44 | /// 45 | /// Parses a doc ID string into an object that can help find definitions of or references to the identified API. 46 | /// 47 | /// The DocID that identifies the API to find references to. 48 | /// An object that can identify the API. 49 | public static Descriptor Parse(string docId) 50 | { 51 | Requires.NotNullOrEmpty(docId); 52 | Requires.Argument(docId.Length > 2, nameof(docId), "DocId must be at least 3 characters long."); 53 | Requires.Argument(docId[1] == ':', nameof(docId), "Not a valid DocId."); 54 | 55 | return new Descriptor(docId); 56 | } 57 | 58 | /// 59 | /// Describes an API that can be identified by a DocID. 60 | /// 61 | /// The DocID that identifies an API. 62 | public class Descriptor(string docId) 63 | { 64 | /// 65 | /// Gets the kind of API that this DocID represents. 66 | /// 67 | public ApiKind Kind => docId[0] switch 68 | { 69 | 'T' => ApiKind.Type, 70 | 'M' => ApiKind.Method, 71 | 'P' => ApiKind.Property, 72 | 'F' => ApiKind.Field, 73 | 'E' => ApiKind.Event, 74 | _ => throw new ArgumentException("Invalid DocId."), 75 | }; 76 | 77 | /// 78 | /// Gets the DocID that this instance represents. 79 | /// 80 | protected string DocId => docId; 81 | 82 | /// 83 | /// Tests whether a given handle is to an API that defines or references the API identified by this DocID. 84 | /// 85 | /// The handle to an API definition or reference. 86 | /// The metadata reader behind the . 87 | /// A value indicating whether it is a match. 88 | public virtual bool IsMatch(EntityHandle handle, MetadataReader reader) 89 | { 90 | // In this virtual method, we do the simple thing of just constructing a DocID for the candidate API 91 | // to see if it equals the DocID we are looking for. 92 | // But in an override, a more efficient parsing of the DocID could be done that compares the result 93 | // with the referenced entity to see if they are equal, with fewer or no allocations. 94 | DocIdBuilder builder = new(reader); 95 | string actualDocId = builder.GetDocumentationCommentId(handle); 96 | return actualDocId == docId; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/AssemblyRefScanner/DocIdBuilder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Text; 5 | 6 | namespace AssemblyRefScanner; 7 | 8 | /// 9 | /// Builds a doc comment ID 10 | /// for a given type or member reference. 11 | /// 12 | public class DocIdBuilder(MetadataReader mdReader) 13 | { 14 | private const string EventAddPrefix = "add_"; 15 | private const string EventRemovePrefix = "remove_"; 16 | private const string PropertyGetPrefix = "get_"; 17 | private const string PropertySetPrefix = "set_"; 18 | 19 | private static readonly ThreadLocal Builder = new(() => new()); 20 | 21 | /// 22 | /// Constructs a DocID for the given entity handle. 23 | /// 24 | /// The handle to the entity to construct a DocID for. 25 | /// The DocID. 26 | /// Thrown when refers to an entity for which no DocID can be constructed. 27 | /// 28 | /// 29 | /// DocIDs can be constructed for the following entity types: 30 | /// 31 | /// 32 | /// 33 | /// 34 | /// 35 | /// 36 | /// 37 | /// 38 | /// 39 | /// 40 | /// 41 | public string GetDocumentationCommentId(EntityHandle entityHandle) 42 | { 43 | StringBuilder builder = Builder.Value!; 44 | builder.Clear(); 45 | 46 | switch (entityHandle.Kind) 47 | { 48 | case HandleKind.TypeDefinition: 49 | builder.Append("T:"); 50 | this.VisitType(new DefinitionTypeHandleInfo(mdReader, (TypeDefinitionHandle)entityHandle), builder); 51 | break; 52 | case HandleKind.EventDefinition: 53 | builder.Append("E:"); 54 | this.VisitEvent((EventDefinitionHandle)entityHandle, builder); 55 | break; 56 | case HandleKind.FieldDefinition: 57 | builder.Append("F:"); 58 | this.VisitField((FieldDefinitionHandle)entityHandle, builder); 59 | break; 60 | case HandleKind.MethodDefinition: 61 | builder.Append("M:"); 62 | this.VisitMethod((MethodDefinitionHandle)entityHandle, builder); 63 | break; 64 | case HandleKind.PropertyDefinition: 65 | builder.Append("P:"); 66 | this.VisitProperty((PropertyDefinitionHandle)entityHandle, builder); 67 | break; 68 | case HandleKind.TypeReference: 69 | builder.Append("T:"); 70 | this.VisitType((TypeReferenceHandle)entityHandle, builder); 71 | break; 72 | case HandleKind.MemberReference: 73 | MemberReference memberReference = mdReader.GetMemberReference((MemberReferenceHandle)entityHandle); 74 | switch (memberReference.GetKind()) 75 | { 76 | case MemberReferenceKind.Field: 77 | builder.Append("F:"); 78 | this.VisitField(memberReference, builder); 79 | break; 80 | case MemberReferenceKind.Method when this.IsProperty(memberReference): 81 | builder.Append("P:"); 82 | this.VisitProperty(memberReference, builder, fromAccessorMethod: true); 83 | break; 84 | case MemberReferenceKind.Method when this.IsEvent(memberReference): 85 | builder.Append("E:"); 86 | this.VisitEvent(memberReference, builder); 87 | break; 88 | case MemberReferenceKind.Method: 89 | builder.Append("M:"); 90 | this.VisitMethod(memberReference, builder); 91 | break; 92 | default: 93 | throw new NotSupportedException($"Unrecognized member reference kind: {memberReference.GetKind()}"); 94 | } 95 | 96 | break; 97 | default: 98 | throw new NotSupportedException($"Unsupported entity kind: {entityHandle.Kind}."); 99 | } 100 | 101 | return builder.ToString(); 102 | } 103 | 104 | private void VisitType(TypeReferenceHandle typeRefHandle, StringBuilder builder) 105 | { 106 | TypeReference typeReference = mdReader.GetTypeReference(typeRefHandle); 107 | if (typeReference.ResolutionScope.Kind == HandleKind.TypeReference) 108 | { 109 | this.VisitType((TypeReferenceHandle)typeReference.ResolutionScope, builder); 110 | builder.Append('.'); 111 | } 112 | else if (mdReader.GetString(typeReference.Namespace) is { Length: > 0 } ns) 113 | { 114 | builder.Append(ns); 115 | builder.Append('.'); 116 | } 117 | 118 | builder.Append(mdReader.GetString(typeReference.Name)); 119 | } 120 | 121 | private void VisitType(TypeSpecificationHandle typeSpecHandle, StringBuilder builder) 122 | => this.VisitType(mdReader.GetTypeSpecification(typeSpecHandle).DecodeSignature(SignatureTypeProvider.Instance, GenericContext.Instance), builder); 123 | 124 | private void VisitType(TypeDefinitionHandle typeDefHandle, StringBuilder builder) 125 | => this.VisitType(new DefinitionTypeHandleInfo(mdReader, typeDefHandle), builder); 126 | 127 | private void VisitType(TypeHandleInfo typeHandle, StringBuilder builder) 128 | { 129 | switch (typeHandle) 130 | { 131 | case ArrayTypeHandleInfo arrayType: 132 | this.VisitType(arrayType.ElementType, builder); 133 | builder.Append('['); 134 | if (arrayType.Shape is { } shape) 135 | { 136 | for (int i = 0; i < shape.Rank; i++) 137 | { 138 | if (i > 0) 139 | { 140 | builder.Append(','); 141 | } 142 | 143 | if (shape.LowerBounds.Length > i || shape.Sizes.Length > i) 144 | { 145 | if (shape.LowerBounds.Length > i) 146 | { 147 | builder.Append(shape.LowerBounds[i]); 148 | } 149 | 150 | builder.Append(':'); 151 | if (shape.Sizes.Length > i) 152 | { 153 | builder.Append(shape.Sizes[i]); 154 | } 155 | } 156 | } 157 | } 158 | 159 | builder.Append(']'); 160 | 161 | break; 162 | case ByRefTypeHandleInfo { ElementType: { } elementType }: 163 | this.VisitType(elementType, builder); 164 | builder.Append('@'); 165 | break; 166 | case PointerTypeHandleInfo { ElementType: { } elementType }: 167 | this.VisitType(elementType, builder); 168 | builder.Append('*'); 169 | break; 170 | case GenericTypeParameter genericTypeParameter: 171 | builder.Append('`'); 172 | builder.Append(genericTypeParameter.Position); 173 | break; 174 | case GenericMethodParameter genericMethodParameter: 175 | builder.Append("``"); 176 | builder.Append(genericMethodParameter.Position); 177 | break; 178 | case GenericTypeHandleInfo genericInstanceType: 179 | if (genericInstanceType.GenericType.Namespace is { Length: > 0 } ns) 180 | { 181 | builder.Append(ns); 182 | builder.Append('.'); 183 | } 184 | 185 | builder.Append(genericInstanceType.GenericType.NameWithoutArity); 186 | builder.Append('{'); 187 | 188 | foreach (var genericArgument in genericInstanceType.TypeArguments) 189 | { 190 | this.VisitType(genericArgument, builder); 191 | builder.Append(','); 192 | } 193 | 194 | if (builder[^1] == ',') 195 | { 196 | builder.Length--; 197 | } 198 | 199 | builder.Append('}'); 200 | break; 201 | case NamedTypeHandleInfo namedType: 202 | if (namedType.NestingType is not null) 203 | { 204 | this.VisitType(namedType.NestingType, builder); 205 | builder.Append('.'); 206 | } 207 | else if (namedType.Namespace is { Length: > 0 }) 208 | { 209 | builder.Append(namedType.Namespace); 210 | builder.Append('.'); 211 | } 212 | 213 | builder.Append(namedType.Name); 214 | break; 215 | default: 216 | builder.Append($"!:Unsupported type handle: {typeHandle.GetType()}"); 217 | break; 218 | } 219 | } 220 | 221 | private void VisitParentType(MemberReference memberReference, StringBuilder builder) 222 | { 223 | switch (memberReference.Parent.Kind) 224 | { 225 | case HandleKind.TypeReference: 226 | this.VisitType((TypeReferenceHandle)memberReference.Parent, builder); 227 | break; 228 | case HandleKind.TypeSpecification: 229 | this.VisitType((TypeSpecificationHandle)memberReference.Parent, builder); 230 | break; 231 | case HandleKind.TypeDefinition: 232 | this.VisitType((TypeDefinitionHandle)memberReference.Parent, builder); 233 | break; 234 | default: 235 | throw new NotSupportedException($"Parent type is not supported: {memberReference.Parent.Kind}"); 236 | } 237 | } 238 | 239 | private void VisitParentType(EntityHandle typeHandle, StringBuilder builder) 240 | { 241 | switch (typeHandle.Kind) 242 | { 243 | case HandleKind.TypeDefinition: 244 | this.VisitType(new DefinitionTypeHandleInfo(mdReader, (TypeDefinitionHandle)typeHandle), builder); 245 | break; 246 | case HandleKind.TypeReference: 247 | this.VisitType((TypeReferenceHandle)typeHandle, builder); 248 | break; 249 | case HandleKind.TypeSpecification: 250 | this.VisitType((TypeSpecificationHandle)typeHandle, builder); 251 | break; 252 | default: 253 | throw new NotSupportedException($"{typeHandle.Kind} is not supported."); 254 | } 255 | } 256 | 257 | private void VisitMethodHelper(string name, MethodSignature signature, StringBuilder builder) 258 | { 259 | builder.Append('.'); 260 | int nameStartIndex = builder.Length; 261 | builder.Append(name); 262 | builder.Replace('.', '#', nameStartIndex, name.Length); 263 | 264 | if (signature.GenericParameterCount > 0) 265 | { 266 | builder.Append("``"); 267 | builder.Append(signature.GenericParameterCount); 268 | } 269 | 270 | if (signature.ParameterTypes.Length == 0) 271 | { 272 | return; 273 | } 274 | 275 | builder.Append('('); 276 | 277 | foreach (TypeHandleInfo parameterType in signature.ParameterTypes) 278 | { 279 | this.VisitType(parameterType, builder); 280 | 281 | if (builder[^1] == '&') 282 | { 283 | builder.Length--; 284 | builder.Append('@'); 285 | } 286 | 287 | builder.Append(','); 288 | } 289 | 290 | if (builder[^1] == ',') 291 | { 292 | builder.Length--; 293 | } 294 | 295 | builder.Append(')'); 296 | } 297 | 298 | private void VisitMethod(MemberReference methodReference, StringBuilder builder) 299 | { 300 | this.VisitParentType(methodReference, builder); 301 | 302 | string name = mdReader.GetString(methodReference.Name); 303 | MethodSignature signature = methodReference.DecodeMethodSignature(SignatureTypeProvider.Instance, GenericContext.Instance); 304 | this.VisitMethodHelper(name, signature, builder); 305 | } 306 | 307 | private void VisitMethod(MethodDefinitionHandle handle, StringBuilder builder) 308 | { 309 | MethodDefinition methodDef = mdReader.GetMethodDefinition(handle); 310 | this.VisitParentType(methodDef.GetDeclaringType(), builder); 311 | 312 | MethodSignature signature = methodDef.DecodeSignature(SignatureTypeProvider.Instance, GenericContext.Instance); 313 | this.VisitMethodHelper(mdReader.GetString(methodDef.Name), signature, builder); 314 | } 315 | 316 | #if NETFRAMEWORK 317 | private void VisitPropertyHelper(string name, MethodSignature signature, StringBuilder builder) => this.VisitPropertyHelper(name.AsSpan(), signature, builder); 318 | #endif 319 | 320 | private void VisitPropertyHelper(ReadOnlySpan name, MethodSignature signature, StringBuilder builder) 321 | { 322 | builder.Append('.'); 323 | builder.Append(name); 324 | 325 | if (signature.ParameterTypes.Length == 0) 326 | { 327 | return; 328 | } 329 | 330 | builder.Append('('); 331 | 332 | foreach (TypeHandleInfo parameterType in signature.ParameterTypes) 333 | { 334 | this.VisitType(parameterType, builder); 335 | builder.Append(','); 336 | } 337 | 338 | if (builder[^1] == ',') 339 | { 340 | builder.Length--; 341 | } 342 | 343 | builder.Append(')'); 344 | } 345 | 346 | private void VisitProperty(MemberReference propertyReference, StringBuilder builder, bool fromAccessorMethod = false) 347 | { 348 | this.VisitParentType(propertyReference, builder); 349 | 350 | string name = mdReader.GetString(propertyReference.Name); 351 | MethodSignature signature = propertyReference.DecodeMethodSignature(SignatureTypeProvider.Instance, GenericContext.Instance); 352 | this.VisitPropertyHelper(fromAccessorMethod ? name.AsSpan(4) : name.AsSpan(), signature, builder); 353 | } 354 | 355 | private void VisitProperty(PropertyDefinitionHandle handle, StringBuilder builder) 356 | { 357 | PropertyDefinition propertyDef = mdReader.GetPropertyDefinition(handle); 358 | 359 | PropertyAccessors accessors = propertyDef.GetAccessors(); 360 | MethodDefinitionHandle someAccessor = 361 | accessors.Getter.IsNil == false ? accessors.Getter : 362 | accessors.Setter.IsNil == false ? accessors.Setter : 363 | accessors.Others.FirstOrDefault(); 364 | if (someAccessor.IsNil) 365 | { 366 | throw new NotSupportedException("Property with no accessors."); 367 | } 368 | 369 | MethodDefinition accessorMethodDef = mdReader.GetMethodDefinition(someAccessor); 370 | this.VisitParentType(accessorMethodDef.GetDeclaringType(), builder); 371 | 372 | MethodSignature signature = propertyDef.DecodeSignature(SignatureTypeProvider.Instance, GenericContext.Instance); 373 | this.VisitPropertyHelper(mdReader.GetString(propertyDef.Name), signature, builder); 374 | } 375 | 376 | private void VisitField(MemberReference fieldReference, StringBuilder builder) 377 | { 378 | this.VisitParentType(fieldReference, builder); 379 | 380 | builder.Append('.'); 381 | builder.Append(fieldReference.Name); 382 | } 383 | 384 | private void VisitField(FieldDefinitionHandle handle, StringBuilder builder) 385 | { 386 | FieldDefinition fieldDefinition = mdReader.GetFieldDefinition(handle); 387 | this.VisitParentType(fieldDefinition.GetDeclaringType(), builder); 388 | 389 | builder.Append('.'); 390 | builder.Append(mdReader.GetString(fieldDefinition.Name)); 391 | } 392 | 393 | private void VisitEvent(MemberReference eventReference, StringBuilder builder) 394 | { 395 | this.VisitParentType(eventReference, builder); 396 | 397 | builder.Append('.'); 398 | string name = mdReader.GetString(eventReference.Name); 399 | builder.Append( 400 | name.StartsWith(EventAddPrefix) ? name.AsSpan(EventAddPrefix.Length) : 401 | name.StartsWith(EventRemovePrefix) ? name.AsSpan(EventRemovePrefix.Length) : 402 | throw new NotImplementedException()); 403 | } 404 | 405 | private void VisitEvent(EventDefinitionHandle handle, StringBuilder builder) 406 | { 407 | EventDefinition eventDefinition = mdReader.GetEventDefinition(handle); 408 | this.VisitParentType(eventDefinition.Type, builder); 409 | 410 | builder.Append("."); 411 | builder.Append(mdReader.GetString(eventDefinition.Name)); 412 | } 413 | 414 | private bool IsProperty(MemberReference memberReference) 415 | { 416 | // Falling back on naming conventions is our only resort when the reference is to an API outside 417 | // the assembly accessible to the MetadataReader. 418 | // When it is to an API within the same assembly, we can resolve the reference to the actual API definition 419 | // to find out what it is. 420 | // As currently implemented though, we don't bother with the resolving the in-assembly references. 421 | return mdReader.StringComparer.StartsWith(memberReference.Name, PropertyGetPrefix) || mdReader.StringComparer.StartsWith(memberReference.Name, PropertySetPrefix); 422 | } 423 | 424 | private bool IsEvent(MemberReference memberReference) 425 | { 426 | // Falling back on naming conventions is our only resort when the reference is to an API outside 427 | // the assembly accessible to the MetadataReader. 428 | // When it is to an API within the same assembly, we can resolve the reference to the actual API definition 429 | // to find out what it is. 430 | // As currently implemented though, we don't bother with the resolving the in-assembly references. 431 | return mdReader.StringComparer.StartsWith(memberReference.Name, EventAddPrefix) || mdReader.StringComparer.StartsWith(memberReference.Name, EventRemovePrefix); 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /src/AssemblyRefScanner/EmbeddedTypeScanner.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.CommandLine.Invocation; 5 | using System.ComponentModel; 6 | using System.Reflection.PortableExecutable; 7 | 8 | namespace AssemblyRefScanner; 9 | 10 | internal class EmbeddedTypeScanner : ScannerBase 11 | { 12 | internal required string Path { get; init; } 13 | 14 | internal required IList EmbeddableAssemblies { get; init; } 15 | 16 | internal async Task Execute(CancellationToken cancellationToken) 17 | { 18 | HashSet embeddableTypeNames = new(); 19 | foreach (string assemblyPath in this.EmbeddableAssemblies) 20 | { 21 | CollectTypesFrom(embeddableTypeNames, assemblyPath); 22 | } 23 | 24 | var typeScanner = this.CreateProcessAssembliesBlock( 25 | mdReader => 26 | { 27 | MemberReferenceHandle? typeIdentifierCtor = GetTypeIdentifierAttributeCtor(mdReader); 28 | 29 | if (typeIdentifierCtor is null) 30 | { 31 | return ImmutableHashSet.Empty; 32 | } 33 | 34 | var embeddedTypeNames = ImmutableHashSet.CreateBuilder(); 35 | foreach (TypeDefinitionHandle typeDefHandle in mdReader.TypeDefinitions) 36 | { 37 | TypeDefinition typeDef = mdReader.GetTypeDefinition(typeDefHandle); 38 | foreach (CustomAttributeHandle attHandle in typeDef.GetCustomAttributes()) 39 | { 40 | CustomAttribute att = mdReader.GetCustomAttribute(attHandle); 41 | if (att.Constructor.Kind == HandleKind.MemberReference && typeIdentifierCtor.Value.Equals((MemberReferenceHandle)att.Constructor)) 42 | { 43 | string fullyQualifiedName = mdReader.GetString(typeDef.Namespace) + "." + mdReader.GetString(typeDef.Name); 44 | if (embeddableTypeNames.Contains(fullyQualifiedName)) 45 | { 46 | embeddedTypeNames.Add(fullyQualifiedName); 47 | } 48 | } 49 | } 50 | } 51 | 52 | return embeddedTypeNames.ToImmutable(); 53 | }, 54 | cancellationToken); 55 | var reporter = this.CreateReportBlock( 56 | typeScanner, 57 | (assemblyPath, results) => 58 | { 59 | if (!results.IsEmpty) 60 | { 61 | Console.WriteLine(TrimBasePath(assemblyPath, this.Path)); 62 | } 63 | }, 64 | cancellationToken); 65 | return await this.Scan(this.Path, typeScanner, reporter, cancellationToken); 66 | } 67 | 68 | private static void CollectTypesFrom(HashSet embeddableTypeNames, string assemblyPath) 69 | { 70 | using var assemblyStream = File.OpenRead(assemblyPath); 71 | var peReader = new PEReader(assemblyStream); 72 | var mdReader = peReader.GetMetadataReader(); 73 | 74 | MemberReferenceHandle? typeIdentifierCtor = GetTypeIdentifierAttributeCtor(mdReader); 75 | foreach (TypeDefinitionHandle tdh in mdReader.TypeDefinitions) 76 | { 77 | TypeDefinition td = mdReader.GetTypeDefinition(tdh); 78 | bool isEmbeddedType = false; 79 | if (typeIdentifierCtor.HasValue) 80 | { 81 | foreach (CustomAttributeHandle attHandle in td.GetCustomAttributes()) 82 | { 83 | CustomAttribute att = mdReader.GetCustomAttribute(attHandle); 84 | if (att.Constructor.Kind == HandleKind.MemberReference && typeIdentifierCtor.Value.Equals((MemberReferenceHandle)att.Constructor)) 85 | { 86 | isEmbeddedType = true; 87 | break; 88 | } 89 | } 90 | } 91 | 92 | if (!isEmbeddedType) 93 | { 94 | string fullyQualifiedName = mdReader.GetString(td.Namespace) + "." + mdReader.GetString(td.Name); 95 | embeddableTypeNames.Add(fullyQualifiedName); 96 | } 97 | } 98 | } 99 | 100 | private static MemberReferenceHandle? GetTypeIdentifierAttributeCtor(MetadataReader mdReader) 101 | { 102 | MemberReferenceHandle? typeIdentifierCtor = null; 103 | foreach (MemberReferenceHandle memberRefHandle in mdReader.MemberReferences) 104 | { 105 | MemberReference memberRef = mdReader.GetMemberReference(memberRefHandle); 106 | if (mdReader.StringComparer.Equals(memberRef.Name, ".ctor") && 107 | memberRef.Parent.Kind == HandleKind.TypeReference) 108 | { 109 | TypeReference tr = mdReader.GetTypeReference((TypeReferenceHandle)memberRef.Parent); 110 | if (mdReader.StringComparer.Equals(tr.Name, "TypeIdentifierAttribute")) 111 | { 112 | typeIdentifierCtor = memberRefHandle; 113 | break; 114 | } 115 | } 116 | } 117 | 118 | return typeIdentifierCtor; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/AssemblyRefScanner/MultiVersionOfOneAssemblyNameScanner.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace AssemblyRefScanner; 5 | 6 | /// 7 | /// Scans for assemblies that reference another assembly more than once, for purposes of referencing multiple versions simultaneously. 8 | /// 9 | internal class MultiVersionOfOneAssemblyNameScanner : ScannerBase 10 | { 11 | private static readonly HashSet RuntimeAssemblySimpleNames = new HashSet(StringComparer.OrdinalIgnoreCase) 12 | { 13 | "mscorlib", 14 | "system", 15 | "System.Private.CoreLib", 16 | "System.Drawing", 17 | "System.Collections", 18 | "System.Data", 19 | "System.Data.Odbc", 20 | "System.Windows.Forms", 21 | "System.Net.Http", 22 | "System.Net.Primitives", 23 | "System.IO.Compression", 24 | }; 25 | 26 | internal required string Path { get; init; } 27 | 28 | internal async Task Execute(CancellationToken cancellationToken) 29 | { 30 | var refReader = this.CreateProcessAssembliesBlock( 31 | mdReader => (from referenceHandle in mdReader.AssemblyReferences 32 | let reference = mdReader.GetAssemblyReference(referenceHandle).GetAssemblyName() 33 | group reference by reference.Name).ToImmutableDictionary(kv => kv.Key, kv => kv.ToImmutableArray(), StringComparer.OrdinalIgnoreCase), 34 | cancellationToken); 35 | var aggregator = this.CreateReportBlock( 36 | refReader, 37 | (assemblyPath, results) => 38 | { 39 | foreach (var referencesByName in results) 40 | { 41 | if (RuntimeAssemblySimpleNames.Contains(referencesByName.Key)) 42 | { 43 | // We're not interested in multiple versions referenced from mscorlib, etc. 44 | continue; 45 | } 46 | 47 | if (referencesByName.Value.Length > 1) 48 | { 49 | Console.WriteLine(TrimBasePath(assemblyPath, this.Path)); 50 | foreach (var reference in referencesByName.Value) 51 | { 52 | Console.WriteLine($"\t{reference}"); 53 | } 54 | 55 | Console.WriteLine(); 56 | } 57 | } 58 | }, 59 | cancellationToken); 60 | 61 | return await this.Scan(this.Path, refReader, aggregator, cancellationToken); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/AssemblyRefScanner/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.CommandLine; 5 | using System.CommandLine.Builder; 6 | using System.CommandLine.Invocation; 7 | using System.CommandLine.Parsing; 8 | 9 | namespace AssemblyRefScanner; 10 | 11 | internal class Program 12 | { 13 | private static async Task Main(string[] args) 14 | { 15 | Parser parser = BuildCommandLine(); 16 | return await parser.InvokeAsync(args); 17 | } 18 | 19 | private static Parser BuildCommandLine() 20 | { 21 | var searchDirOption = new Option("--path", () => Directory.GetCurrentDirectory(), "The path of the directory to search. This should be a full install of VS (i.e. all workloads) to produce complete results. If not specified, the current directory will be searched.").LegalFilePathsOnly(); 22 | 23 | Argument simpleAssemblyName = new("simpleAssemblyName", "The simple assembly name (e.g. \"StreamJsonRpc\") to search for in referenced assembly lists."); 24 | Command versions = new("assembly", "Searches for references to the assembly with the specified simple name.") 25 | { 26 | searchDirOption, 27 | simpleAssemblyName, 28 | }; 29 | versions.SetHandler( 30 | async context => context.ExitCode = await new AssemblyReferenceScanner 31 | { 32 | Path = context.ParseResult.GetValueForOption(searchDirOption)!, 33 | SimpleAssemblyName = context.ParseResult.GetValueForArgument(simpleAssemblyName), 34 | }.Execute(context.GetCancellationToken())); 35 | 36 | Command multiVersions = new("multiversions", "All assemblies that reference multiple versions of *any* assembly will be printed.") 37 | { 38 | searchDirOption, 39 | }; 40 | multiVersions.SetHandler( 41 | async context => context.ExitCode = await new MultiVersionOfOneAssemblyNameScanner 42 | { 43 | Path = context.ParseResult.GetValueForArgument(simpleAssemblyName), 44 | }.Execute(context.GetCancellationToken())); 45 | 46 | Argument> embeddableAssemblies = new("embeddableAssemblies") 47 | { 48 | Description = "The path to an embeddable assembly.", 49 | Arity = ArgumentArity.OneOrMore, 50 | }; 51 | Command embeddedSearch = new("embeddedTypes", "Searches for assemblies that have embedded types.") 52 | { 53 | searchDirOption, 54 | embeddableAssemblies, 55 | }; 56 | embeddedSearch.SetHandler( 57 | async context => context.ExitCode = await new EmbeddedTypeScanner 58 | { 59 | Path = context.ParseResult.GetValueForOption(searchDirOption)!, 60 | EmbeddableAssemblies = context.ParseResult.GetValueForArgument(embeddableAssemblies), 61 | }.Execute(context.GetCancellationToken())); 62 | 63 | Option declaringAssembly = new(new string[] { "--declaringAssembly", "-a" }, "The simple name of the assembly that declares the API whose references are to be found."); 64 | Option namespaceArg = new(new string[] { "--namespace", "-n" }, "The namespace of the type to find references to."); 65 | Argument typeName = new("typeName", "The simple name of the type to find references to.") { Arity = ArgumentArity.ExactlyOne }; 66 | Command typeRefSearch = new("type", "Searches for references to a given type.") 67 | { 68 | searchDirOption, 69 | declaringAssembly, 70 | namespaceArg, 71 | typeName, 72 | }; 73 | typeRefSearch.SetHandler( 74 | async context => context.ExitCode = await new TypeRefScanner 75 | { 76 | Path = context.ParseResult.GetValueForOption(searchDirOption)!, 77 | DeclaringAssembly = context.ParseResult.GetValueForOption(declaringAssembly), 78 | Namespace = context.ParseResult.GetValueForOption(namespaceArg), 79 | TypeName = context.ParseResult.GetValueForArgument(typeName), 80 | }.Execute(context.GetCancellationToken())); 81 | 82 | Argument docId = new("docID", "The DocID that identifies the API member to search for references to. A DocID for a given API may be obtained by compiling a C# program with GenerateDocumentationFile=true that references the API using and then inspecting the compiler-generated .xml file for that reference.") { Arity = ArgumentArity.OneOrMore }; 83 | Command apiRefSearch = new("api", "Searches for references to a given type or member.") 84 | { 85 | searchDirOption, 86 | declaringAssembly, 87 | docId, 88 | }; 89 | apiRefSearch.SetHandler( 90 | async context => context.ExitCode = await new ApiRefScanner 91 | { 92 | Path = context.ParseResult.GetValueForOption(searchDirOption)!, 93 | DeclaringAssembly = context.ParseResult.GetValueForOption(declaringAssembly), 94 | DocIds = context.ParseResult.GetValueForArgument(docId), 95 | }.Execute(context.GetCancellationToken())); 96 | 97 | Option json = new("--json", "The path to a .json file that will contain the raw output of all assemblies scanned."); 98 | Option dgml = new("--dgml", "The path to a .dgml file to be generated with all assemblies graphed with their dependencies and identified by TargetFramework."); 99 | Option includeRuntimeAssemblies = new("--include-runtime", "Includes runtime assemblies in the output."); 100 | Command targetFramework = new("targetFramework", "Groups all assemblies by TargetFramework.") 101 | { 102 | searchDirOption, 103 | dgml, 104 | json, 105 | includeRuntimeAssemblies, 106 | }; 107 | targetFramework.SetHandler( 108 | async context => context.ExitCode = await new TargetFrameworkScanner 109 | { 110 | Path = context.ParseResult.GetValueForOption(searchDirOption)!, 111 | Dgml = context.ParseResult.GetValueForOption(dgml), 112 | Json = context.ParseResult.GetValueForOption(json), 113 | IncludeRuntimeAssemblies = context.ParseResult.GetValueForOption(includeRuntimeAssemblies), 114 | }.Execute(context.GetCancellationToken())); 115 | 116 | Argument assemblyPath = new("assemblyPath", "The path to the assembly to search for assembly references."); 117 | Option transitive = new("--transitive", "Resolves transitive assembly references a = new(in addition to the default direct references)."); 118 | Option config = new("--config", "The path to an .exe.config or .dll.config file to use to resolve references."); 119 | Option baseDir = new("--base-dir", "The path to the directory to consider the app base directory for resolving assemblies and relative paths in the .config file. If not specified, the default is the directory that contains the .config file if specified, or the directory containing the entry assembly."); 120 | Option runtimeDir = new("--runtime-dir", "The path to a .NET runtime directory where assemblies may also be resolved from. May be used more than once."); 121 | Option excludeRuntime = new("--exclude-runtime", "Omits reporting assembly paths that are found in any of the specified runtime directories."); 122 | Command resolveAssemblyReferences = new("resolveReferences", "Lists paths to assemblies referenced by a given assembly.") 123 | { 124 | assemblyPath, 125 | transitive, 126 | config, 127 | baseDir, 128 | runtimeDir, 129 | excludeRuntime, 130 | }; 131 | resolveAssemblyReferences.SetHandler( 132 | context => new ResolveAssemblyReferences 133 | { 134 | AssemblyPath = context.ParseResult.GetValueForArgument(assemblyPath), 135 | Transitive = context.ParseResult.GetValueForOption(transitive), 136 | Config = context.ParseResult.GetValueForOption(config), 137 | BaseDir = context.ParseResult.GetValueForOption(baseDir), 138 | RuntimeDir = context.ParseResult.GetValueForOption(runtimeDir) ?? [], 139 | ExcludeRuntime = context.ParseResult.GetValueForOption(excludeRuntime), 140 | }.Execute(context.GetCancellationToken())); 141 | 142 | var root = new RootCommand($"{ThisAssembly.AssemblyTitle} v{ThisAssembly.AssemblyInformationalVersion}") 143 | { 144 | versions, 145 | multiVersions, 146 | embeddedSearch, 147 | apiRefSearch, 148 | typeRefSearch, 149 | targetFramework, 150 | resolveAssemblyReferences, 151 | }; 152 | root.Name = "refscanner"; 153 | return new CommandLineBuilder(root) 154 | .UseDefaults() 155 | .Build(); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/AssemblyRefScanner/ResolveAssemblyReferences.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Reflection.PortableExecutable; 5 | using Nerdbank.NetStandardBridge; 6 | 7 | namespace AssemblyRefScanner; 8 | 9 | internal class ResolveAssemblyReferences : ScannerBase 10 | { 11 | internal required string AssemblyPath { get; init; } 12 | 13 | internal required bool Transitive { get; init; } 14 | 15 | internal required string? Config { get; init; } 16 | 17 | internal required string? BaseDir { get; init; } 18 | 19 | internal required string[] RuntimeDir { get; init; } 20 | 21 | internal required bool ExcludeRuntime { get; init; } 22 | 23 | public void Execute(CancellationToken cancellationToken) 24 | { 25 | string baseDir = this.BaseDir ?? (this.Config is not null ? Path.GetDirectoryName(this.Config)! : Path.GetDirectoryName(this.AssemblyPath)!); 26 | TrimTrailingSlashes(this.RuntimeDir); 27 | 28 | NetFrameworkAssemblyResolver? alc = this.Config is null ? null : new(this.Config, baseDir); 29 | HashSet resolvedPaths = new(StringComparer.OrdinalIgnoreCase); 30 | HashSet unresolvedNames = new(StringComparer.OrdinalIgnoreCase); 31 | 32 | EnumerateAndReportReferences(this.AssemblyPath); 33 | 34 | void EnumerateAndReportReferences(string assemblyPath) 35 | { 36 | cancellationToken.ThrowIfCancellationRequested(); 37 | 38 | // The .NET runtime includes references to assemblies that are only needed to support .NET Framework-targeted assemblies 39 | // and are therefore expected to come from the app directory. Thus, any unresolved references coming *from* the runtime directory 40 | // will be considered By Design and not reported to stderr. 41 | string assemblyPathDirectory = Path.GetDirectoryName(assemblyPath)!.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); 42 | bool isThisUnderRuntimeFolder = this.RuntimeDir.Contains(assemblyPathDirectory, StringComparer.OrdinalIgnoreCase); 43 | 44 | foreach (AssemblyName reference in this.EnumerateReferences(assemblyPath)) 45 | { 46 | cancellationToken.ThrowIfCancellationRequested(); 47 | 48 | // Always try the runtime directories first, since no custom assembly resolver or .config processing 49 | // will apply at runtime when the assembly is found in the runtime folder. 50 | // When matching these, the .NET runtime disregards all details in the assembly name except the simple name, so we do too. 51 | if (this.RuntimeDir.Select(dir => Path.Combine(dir, reference.Name + ".dll")).FirstOrDefault(File.Exists) is string runtimeDirMatch) 52 | { 53 | ReportResolvedReference(runtimeDirMatch, !this.ExcludeRuntime); 54 | continue; 55 | } 56 | 57 | if (alc is not null) 58 | { 59 | try 60 | { 61 | AssemblyName? resolvedAssembly = alc.GetAssemblyNameByPolicy(reference); 62 | 63 | #pragma warning disable SYSLIB0044 // Type or member is obsolete 64 | if (resolvedAssembly?.CodeBase is not null && File.Exists(resolvedAssembly.CodeBase)) 65 | { 66 | ReportResolvedReference(resolvedAssembly.CodeBase); 67 | } 68 | else 69 | { 70 | ReportUnresolvedReference(resolvedAssembly ?? reference, !isThisUnderRuntimeFolder); 71 | } 72 | #pragma warning restore SYSLIB0044 // Type or member is obsolete 73 | } 74 | catch (InvalidOperationException ex) 75 | { 76 | Console.Error.WriteLine(ex.Message); 77 | ReportUnresolvedReference(reference, !isThisUnderRuntimeFolder); 78 | continue; 79 | } 80 | } 81 | else if (File.Exists(Path.Combine(baseDir, reference.Name + ".dll"))) 82 | { 83 | // We only find assemblies in the same directory if no config file was specified. 84 | ReportResolvedReference(Path.Combine(baseDir, reference.Name + ".dll")); 85 | continue; 86 | } 87 | else 88 | { 89 | ReportUnresolvedReference(reference, !isThisUnderRuntimeFolder); 90 | } 91 | } 92 | } 93 | 94 | void ReportResolvedReference(string path, bool emitToOutput = true) 95 | { 96 | if (resolvedPaths.Add(path)) 97 | { 98 | if (emitToOutput) 99 | { 100 | Console.WriteLine(path); 101 | } 102 | 103 | if (this.Transitive) 104 | { 105 | EnumerateAndReportReferences(path); 106 | } 107 | } 108 | } 109 | 110 | void ReportUnresolvedReference(AssemblyName reference, bool emitToOutput) 111 | { 112 | if (reference.Name is not null && unresolvedNames.Add(reference.Name)) 113 | { 114 | if (emitToOutput) 115 | { 116 | Console.Error.WriteLine($"Missing referenced assembly: {reference}"); 117 | } 118 | } 119 | } 120 | } 121 | 122 | private static void TrimTrailingSlashes(string[] paths) 123 | { 124 | for (int i = 0; i < paths.Length; i++) 125 | { 126 | paths[i] = paths[i].TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); 127 | } 128 | } 129 | 130 | private IEnumerable EnumerateReferences(string assemblyPath) 131 | { 132 | using (var assemblyStream = File.OpenRead(assemblyPath)) 133 | { 134 | using PEReader peReader = new(assemblyStream); 135 | MetadataReader mdReader = peReader.GetMetadataReader(); 136 | foreach (AssemblyReferenceHandle arh in mdReader.AssemblyReferences) 137 | { 138 | AssemblyReference ar = mdReader.GetAssemblyReference(arh); 139 | yield return ar.GetAssemblyName(); 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/AssemblyRefScanner/ScannerBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Diagnostics; 5 | using System.Reflection.PortableExecutable; 6 | using System.Threading.Tasks.Dataflow; 7 | 8 | namespace AssemblyRefScanner; 9 | 10 | internal abstract class ScannerBase 11 | { 12 | protected static string TrimBasePath(string absolutePath, string searchPath) 13 | { 14 | if (!searchPath.EndsWith('\\')) 15 | { 16 | searchPath += '\\'; 17 | } 18 | 19 | if (absolutePath.StartsWith(searchPath, StringComparison.OrdinalIgnoreCase)) 20 | { 21 | return absolutePath.Substring(searchPath.Length); 22 | } 23 | 24 | return absolutePath; 25 | } 26 | 27 | protected static bool HasAssemblyReference(MetadataReader reader, string simpleAssemblyName) 28 | { 29 | foreach (AssemblyReferenceHandle handle in reader.AssemblyReferences) 30 | { 31 | AssemblyReference reference = reader.GetAssemblyReference(handle); 32 | if (reader.StringComparer.Equals(reference.Name, simpleAssemblyName, ignoreCase: true)) 33 | { 34 | return true; 35 | } 36 | } 37 | 38 | return false; 39 | } 40 | 41 | protected TransformManyBlock CreateProcessAssembliesBlock(Func assemblyReader, CancellationToken cancellationToken) 42 | { 43 | return new TransformManyBlock( 44 | assemblyPath => 45 | { 46 | using (var assemblyStream = File.OpenRead(assemblyPath)) 47 | { 48 | try 49 | { 50 | using var peReader = new PEReader(assemblyStream); 51 | var mdReader = peReader.GetMetadataReader(); 52 | return new[] { (assemblyPath, assemblyReader(mdReader)) }; 53 | } 54 | catch (InvalidOperationException) 55 | { 56 | // Not a PE file. 57 | return Array.Empty<(string, T)>(); 58 | } 59 | } 60 | }, 61 | new ExecutionDataflowBlockOptions 62 | { 63 | MaxMessagesPerTask = 5, 64 | BoundedCapacity = Environment.ProcessorCount * 4, 65 | SingleProducerConstrained = true, 66 | CancellationToken = cancellationToken, 67 | MaxDegreeOfParallelism = Debugger.IsAttached ? 1 : Environment.ProcessorCount, 68 | }); 69 | } 70 | 71 | protected ITargetBlock<(string AssemblyPath, T Results)> CreateReportBlock(ISourceBlock<(string AssemblyPath, T Results)> previousBlock, Action report, CancellationToken cancellationToken) 72 | { 73 | var block = new ActionBlock<(string AssemblyPath, T Results)>( 74 | tuple => report(tuple.AssemblyPath, tuple.Results), 75 | new ExecutionDataflowBlockOptions 76 | { 77 | BoundedCapacity = 16, 78 | MaxMessagesPerTask = 5, 79 | CancellationToken = cancellationToken, 80 | }); 81 | previousBlock.LinkTo(block, new DataflowLinkOptions { PropagateCompletion = true }); 82 | return block; 83 | } 84 | 85 | /// 86 | /// Feeds all assemblies to a starting block and awaits completion of the terminal block. 87 | /// 88 | /// The path to scan for assemblies. 89 | /// The block that should receive paths to all assemblies. 90 | /// The block to await completion of. 91 | /// A cancellation token. 92 | /// The exit code to return from the calling command. 93 | protected async Task Scan(string path, ITargetBlock startingBlock, IDataflowBlock terminalBlock, CancellationToken cancellationToken) 94 | { 95 | var timer = Stopwatch.StartNew(); 96 | async Task PopulateStartingBlockAsync(CancellationToken cancellationToken) 97 | { 98 | int dllCount = 0; 99 | try 100 | { 101 | foreach (var file in Directory.EnumerateFiles(path, "*.dll", SearchOption.AllDirectories)) 102 | { 103 | await startingBlock.SendAsync(file, cancellationToken); 104 | dllCount++; 105 | } 106 | 107 | startingBlock.Complete(); 108 | } 109 | catch (Exception ex) 110 | { 111 | startingBlock.Fault(ex); 112 | } 113 | 114 | return dllCount; 115 | } 116 | 117 | using CancellationTokenSource faultLinkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); 118 | _ = terminalBlock.Completion.ContinueWith(_ => faultLinkedTokenSource.Cancel(), cancellationToken, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default); 119 | int dllCount = await PopulateStartingBlockAsync(faultLinkedTokenSource.Token); 120 | try 121 | { 122 | await terminalBlock.Completion; 123 | Console.WriteLine($"All done ({dllCount} assemblies scanned in {timer.Elapsed:g}, or {dllCount / timer.Elapsed.TotalSeconds:0,0} assemblies per second)!"); 124 | } 125 | catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) 126 | { 127 | Console.Error.WriteLine("Canceled."); 128 | return 2; 129 | } 130 | catch (Exception ex) 131 | { 132 | Console.Error.WriteLine("Fault encountered during scan: "); 133 | Console.Error.WriteLine(ex); 134 | return 3; 135 | } 136 | 137 | return 0; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/AssemblyRefScanner/SignatureTypeProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | #pragma warning disable SA1402 // File may only contain a single type 5 | #pragma warning disable SA1649 // File name should match first type name 6 | 7 | namespace AssemblyRefScanner; 8 | 9 | internal struct GenericContext 10 | { 11 | internal static readonly GenericContext Instance = default(GenericContext); 12 | } 13 | 14 | internal class SignatureTypeProvider : ISignatureTypeProvider 15 | { 16 | internal static readonly SignatureTypeProvider Instance = new(); 17 | 18 | private SignatureTypeProvider() 19 | { 20 | } 21 | 22 | public TypeHandleInfo GetArrayType(TypeHandleInfo elementType, ArrayShape shape) => new ArrayTypeHandleInfo(elementType, shape); 23 | 24 | public TypeHandleInfo GetByReferenceType(TypeHandleInfo elementType) => new ByRefTypeHandleInfo(elementType); 25 | 26 | public TypeHandleInfo GetFunctionPointerType(MethodSignature signature) => new FunctionPointerHandleInfo(signature); 27 | 28 | public TypeHandleInfo GetGenericInstantiation(TypeHandleInfo genericType, ImmutableArray typeArguments) => new GenericTypeHandleInfo((NamedTypeHandleInfo)genericType, typeArguments); 29 | 30 | public TypeHandleInfo GetGenericMethodParameter(GenericContext genericContext, int index) => new GenericMethodParameter(index); 31 | 32 | public TypeHandleInfo GetGenericTypeParameter(GenericContext genericContext, int index) => new GenericTypeParameter(index); 33 | 34 | public TypeHandleInfo GetModifiedType(TypeHandleInfo modifier, TypeHandleInfo unmodifiedType, bool isRequired) => unmodifiedType; 35 | 36 | public TypeHandleInfo GetPinnedType(TypeHandleInfo elementType) => throw new NotImplementedException(); 37 | 38 | public TypeHandleInfo GetPointerType(TypeHandleInfo elementType) => new PointerTypeHandleInfo(elementType); 39 | 40 | public TypeHandleInfo GetPrimitiveType(PrimitiveTypeCode typeCode) => new PrimitiveTypeHandleInfo(typeCode); 41 | 42 | public TypeHandleInfo GetSZArrayType(TypeHandleInfo elementType) => new ArrayTypeHandleInfo(elementType); 43 | 44 | public TypeHandleInfo GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) => new DefinitionTypeHandleInfo(reader, handle); 45 | 46 | public TypeHandleInfo GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) => new ReferenceTypeHandleInfo(reader, handle); 47 | 48 | public TypeHandleInfo GetTypeFromSpecification(MetadataReader reader, GenericContext genericContext, TypeSpecificationHandle handle, byte rawTypeKind) => reader.GetTypeSpecification(handle).DecodeSignature(SignatureTypeProvider.Instance, GenericContext.Instance); 49 | } 50 | 51 | internal abstract class TypeHandleInfo 52 | { 53 | } 54 | 55 | internal abstract class NamedTypeHandleInfo : TypeHandleInfo 56 | { 57 | /// 58 | /// Gets the full namespace of the type. 59 | /// 60 | internal abstract ReadOnlyMemory Namespace { get; } 61 | 62 | /// 63 | /// Gets the type name (without any arity suffix). 64 | /// 65 | internal ReadOnlyMemory NameWithoutArity => TrimAritySuffix(this.Name); 66 | 67 | /// 68 | /// Gets the type name. 69 | /// 70 | internal abstract ReadOnlyMemory Name { get; } 71 | 72 | /// 73 | /// Gets the type that nests this one. 74 | /// 75 | internal abstract TypeHandleInfo? NestingType { get; } 76 | 77 | private static ReadOnlyMemory TrimAritySuffix(ReadOnlyMemory name) 78 | => name.Span.LastIndexOf('`') is int index && index >= 0 ? name[..index] : name; 79 | } 80 | 81 | internal class DefinitionTypeHandleInfo(MetadataReader reader, TypeDefinitionHandle handle) : NamedTypeHandleInfo 82 | { 83 | private ReadOnlyMemory? @namespace; 84 | private ReadOnlyMemory? name; 85 | private NamedTypeHandleInfo? nestingType; 86 | 87 | internal override ReadOnlyMemory Namespace => this.@namespace ??= reader.GetString(reader.GetTypeDefinition(handle).Namespace).AsMemory(); 88 | 89 | internal override ReadOnlyMemory Name => this.name ??= reader.GetString(reader.GetTypeDefinition(handle).Name).AsMemory(); 90 | 91 | #if NET 92 | internal override NamedTypeHandleInfo? NestingType 93 | #else 94 | internal override TypeHandleInfo? NestingType 95 | #endif 96 | { 97 | get 98 | { 99 | if (this.nestingType is null && reader.GetTypeDefinition(handle).GetDeclaringType() is { IsNil: false } nestingTypeHandle) 100 | { 101 | this.nestingType = new DefinitionTypeHandleInfo(reader, nestingTypeHandle); 102 | } 103 | 104 | return this.nestingType; 105 | } 106 | } 107 | } 108 | 109 | internal class ReferenceTypeHandleInfo(MetadataReader reader, TypeReferenceHandle handle) : NamedTypeHandleInfo 110 | { 111 | private ReadOnlyMemory? @namespace; 112 | private ReadOnlyMemory? name; 113 | private TypeHandleInfo? nestingType; 114 | 115 | internal override ReadOnlyMemory Namespace => this.@namespace ??= reader.GetString(reader.GetTypeReference(handle).Namespace).AsMemory(); 116 | 117 | internal override ReadOnlyMemory Name => this.name ??= reader.GetString(reader.GetTypeReference(handle).Name).AsMemory(); 118 | 119 | internal override TypeHandleInfo? NestingType 120 | { 121 | get 122 | { 123 | if (this.nestingType is null && reader.GetTypeReference(handle) is { ResolutionScope: { IsNil: false, Kind: HandleKind.TypeReference or HandleKind.TypeDefinition } scope }) 124 | { 125 | this.nestingType = new ReferenceTypeHandleInfo(reader, (TypeReferenceHandle)scope); 126 | } 127 | 128 | return this.nestingType; 129 | } 130 | } 131 | } 132 | 133 | internal class PrimitiveTypeHandleInfo(PrimitiveTypeCode typeCode) : NamedTypeHandleInfo 134 | { 135 | internal override ReadOnlyMemory Namespace => "System".AsMemory(); 136 | 137 | internal override ReadOnlyMemory Name => typeCode switch 138 | { 139 | PrimitiveTypeCode.Int16 => "Int16".AsMemory(), 140 | PrimitiveTypeCode.Int32 => "Int32".AsMemory(), 141 | PrimitiveTypeCode.Int64 => "Int64".AsMemory(), 142 | PrimitiveTypeCode.UInt16 => "UInt16".AsMemory(), 143 | PrimitiveTypeCode.UInt32 => "UInt32".AsMemory(), 144 | PrimitiveTypeCode.UInt64 => "UInt64".AsMemory(), 145 | PrimitiveTypeCode.Single => "Single".AsMemory(), 146 | PrimitiveTypeCode.Double => "Double".AsMemory(), 147 | PrimitiveTypeCode.Boolean => "Boolean".AsMemory(), 148 | PrimitiveTypeCode.Char => "Char".AsMemory(), 149 | PrimitiveTypeCode.Byte => "Byte".AsMemory(), 150 | PrimitiveTypeCode.SByte => "SByte".AsMemory(), 151 | PrimitiveTypeCode.IntPtr => "IntPtr".AsMemory(), 152 | PrimitiveTypeCode.UIntPtr => "UIntPtr".AsMemory(), 153 | PrimitiveTypeCode.Object => "Object".AsMemory(), 154 | PrimitiveTypeCode.String => "String".AsMemory(), 155 | PrimitiveTypeCode.Void => "Void".AsMemory(), 156 | PrimitiveTypeCode.TypedReference => "TypedReference".AsMemory(), 157 | _ => throw new NotImplementedException($"{typeCode}"), 158 | }; 159 | 160 | internal override TypeHandleInfo? NestingType => null; 161 | } 162 | 163 | internal class PointerTypeHandleInfo(TypeHandleInfo elementType) : TypeHandleInfo 164 | { 165 | internal TypeHandleInfo ElementType => elementType; 166 | } 167 | 168 | internal class ByRefTypeHandleInfo(TypeHandleInfo elementType) : TypeHandleInfo 169 | { 170 | internal TypeHandleInfo ElementType => elementType; 171 | } 172 | 173 | internal class ArrayTypeHandleInfo(TypeHandleInfo elementType, ArrayShape? shape = null) : TypeHandleInfo 174 | { 175 | internal TypeHandleInfo ElementType => elementType; 176 | 177 | internal ArrayShape? Shape => shape; 178 | } 179 | 180 | internal class GenericTypeHandleInfo(NamedTypeHandleInfo genericType, ImmutableArray typeArguments) : TypeHandleInfo 181 | { 182 | internal NamedTypeHandleInfo GenericType => genericType; 183 | 184 | internal ImmutableArray TypeArguments => typeArguments; 185 | } 186 | 187 | internal class GenericMethodParameter(int index) : TypeHandleInfo 188 | { 189 | internal int Position => index; 190 | } 191 | 192 | internal class GenericTypeParameter(int index) : TypeHandleInfo 193 | { 194 | internal int Position => index; 195 | } 196 | 197 | internal class FunctionPointerHandleInfo(MethodSignature signature) : TypeHandleInfo 198 | { 199 | internal MethodSignature Signature => signature; 200 | } 201 | -------------------------------------------------------------------------------- /src/AssemblyRefScanner/TargetFrameworkScanner.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Runtime.Versioning; 5 | using System.Text; 6 | using System.Text.Json; 7 | using System.Xml.Linq; 8 | 9 | namespace AssemblyRefScanner; 10 | 11 | internal class TargetFrameworkScanner : ScannerBase 12 | { 13 | private const string DgmlNamespace = "http://schemas.microsoft.com/vs/2009/dgml"; 14 | private static readonly ReadOnlyMemory[] DotnetRuntimePublicKeyTokens = new ReadOnlyMemory[] 15 | { 16 | new byte[] { 0xb7, 0x7a, 0x5c, 0x56, 0x19, 0x34, 0xe0, 0x89 }, // b77a5c561934e089 17 | new byte[] { 0x31, 0xbf, 0x38, 0x56, 0xad, 0x36, 0x4e, 0x35 }, // 31bf3856ad364e35 18 | new byte[] { 0x89, 0x84, 0x5d, 0xcd, 0x80, 0x80, 0xcc, 0x91 }, // 89845dcd8080cc91 19 | new byte[] { 0xcc, 0x7b, 0x13, 0xff, 0xcd, 0x2d, 0xdd, 0x51 }, // cc7b13ffcd2ddd51 20 | }; 21 | 22 | private enum TargetFrameworkIdentifiers 23 | { 24 | // These are sorted in order of increasing preference. 25 | Unknown, 26 | NETFramework, 27 | NETPortable, 28 | NETStandard, 29 | NETCore, 30 | } 31 | 32 | internal required string Path { get; init; } 33 | 34 | internal required string? Dgml { get; init; } 35 | 36 | internal required string? Json { get; init; } 37 | 38 | internal required bool IncludeRuntimeAssemblies { get; init; } 39 | 40 | public async Task Execute(CancellationToken cancellationToken) 41 | { 42 | CustomAttributeTypeProvider customAttributeTypeProvider = new(); 43 | var scanner = this.CreateProcessAssembliesBlock( 44 | mdReader => 45 | { 46 | string assemblyName = mdReader.GetString(mdReader.GetAssemblyDefinition().Name); 47 | bool isRuntimeAssembly = IsRuntimeAssemblyPublicKeyToken(mdReader.GetAssemblyDefinition().GetAssemblyName().GetPublicKeyToken()) || IsRuntimeAssemblyName(assemblyName); 48 | 49 | FrameworkName? targetFramework = null; 50 | foreach (CustomAttributeHandle attHandle in mdReader.CustomAttributes) 51 | { 52 | CustomAttribute att = mdReader.GetCustomAttribute(attHandle); 53 | if (att.Parent.Kind == HandleKind.AssemblyDefinition) 54 | { 55 | if (att.Constructor.Kind == HandleKind.MemberReference) 56 | { 57 | MemberReference memberReference = mdReader.GetMemberReference((MemberReferenceHandle)att.Constructor); 58 | if (memberReference.Parent.Kind == HandleKind.TypeReference) 59 | { 60 | TypeReference typeReference = mdReader.GetTypeReference((TypeReferenceHandle)memberReference.Parent); 61 | if (mdReader.StringComparer.Equals(typeReference.Name, "TargetFrameworkAttribute")) 62 | { 63 | CustomAttributeValue value = att.DecodeValue(customAttributeTypeProvider); 64 | if (value.FixedArguments[0].Value is string tfm) 65 | { 66 | targetFramework = new(tfm); 67 | } 68 | 69 | break; 70 | } 71 | } 72 | } 73 | } 74 | } 75 | 76 | List referencesList = new(); 77 | foreach (AssemblyReferenceHandle refHandle in mdReader.AssemblyReferences) 78 | { 79 | AssemblyReference assemblyReference = mdReader.GetAssemblyReference(refHandle); 80 | 81 | string referencedAssemblyName = mdReader.GetString(assemblyReference.Name); 82 | if (!IsRuntimeAssemblyName(referencedAssemblyName)) 83 | { 84 | referencesList.Add(referencedAssemblyName); 85 | } 86 | } 87 | 88 | bool IsRuntimeAssemblyPublicKeyToken(ReadOnlyMemory publicKeyToken) 89 | { 90 | return DotnetRuntimePublicKeyTokens.Any(m => Equals(m.Span, publicKeyToken.Span)); 91 | } 92 | 93 | static bool IsRuntimeAssemblyName(string assemblyName) => assemblyName == "System" || assemblyName.StartsWith("System.", StringComparison.Ordinal); 94 | 95 | return new AssemblyInfo(assemblyName, targetFramework, referencesList, isRuntimeAssembly); 96 | }, 97 | cancellationToken); 98 | Dictionary bestTargetFrameworkPerAssembly = new(StringComparer.OrdinalIgnoreCase); 99 | var report = this.CreateReportBlock( 100 | scanner, 101 | (assemblyPath, result) => 102 | { 103 | if (result.AssemblyName.EndsWith(".resources", StringComparison.OrdinalIgnoreCase)) 104 | { 105 | return; 106 | } 107 | 108 | if (!this.IncludeRuntimeAssemblies && result.IsRuntimeAssembly) 109 | { 110 | return; 111 | } 112 | 113 | result.AssemblyPath = assemblyPath; 114 | 115 | if (!bestTargetFrameworkPerAssembly.TryGetValue(result.AssemblyName, out AssemblyInfo? lastBestFound) || lastBestFound.TargetFrameworkIdentifier < result.TargetFrameworkIdentifier) 116 | { 117 | bestTargetFrameworkPerAssembly[result.AssemblyName] = result; 118 | } 119 | }, 120 | cancellationToken); 121 | 122 | int exitCode = await this.Scan(this.Path, scanner, report, cancellationToken); 123 | 124 | cancellationToken.ThrowIfCancellationRequested(); 125 | Dictionary targetFrameworkPopularity = new(); 126 | 127 | if (this.Json is not null) 128 | { 129 | var serializedResults = JsonSerializer.Serialize(bestTargetFrameworkPerAssembly); 130 | File.WriteAllText(this.Json, serializedResults); 131 | } 132 | 133 | var groupedByTFM = from item in bestTargetFrameworkPerAssembly 134 | orderby item.Value.AssemblyName 135 | group item.Value by item.Value.TargetFrameworkIdentifier into groups 136 | orderby groups.Key 137 | select groups; 138 | foreach (var item in groupedByTFM) 139 | { 140 | Console.WriteLine(item.Key); 141 | int count = 0; 142 | foreach (AssemblyInfo assembly in item) 143 | { 144 | count++; 145 | Console.WriteLine($"\t{assembly.AssemblyName}"); 146 | } 147 | 148 | if (item.Key.HasValue) 149 | { 150 | targetFrameworkPopularity.Add(item.Key.Value, count); 151 | } 152 | } 153 | 154 | Console.WriteLine("Summary:"); 155 | foreach (KeyValuePair item in targetFrameworkPopularity.OrderByDescending(kv => kv.Value)) 156 | { 157 | Console.WriteLine($"{item.Key,-25}{item.Value,4} ({item.Value * 100 / bestTargetFrameworkPerAssembly.Count,3}%)"); 158 | } 159 | 160 | Console.WriteLine($"Total:{bestTargetFrameworkPerAssembly.Count,23}"); 161 | 162 | if (this.Dgml is not null) 163 | { 164 | const string RuntimeAssemblyCategory = "IsRuntimeAssembly"; 165 | static XElement TFICategory(TargetFrameworkIdentifiers identifier, string color) => new(XName.Get("Category", DgmlNamespace), new XAttribute("Id", identifier), new XAttribute("Background", color)); 166 | 167 | XElement nodesElement = new(XName.Get("Nodes", DgmlNamespace)); 168 | XElement linksElement = new(XName.Get("Links", DgmlNamespace)); 169 | XElement categoriesElement = new( 170 | XName.Get("Categories", DgmlNamespace), 171 | TFICategory(TargetFrameworkIdentifiers.Unknown, "Red"), 172 | TFICategory(TargetFrameworkIdentifiers.NETFramework, "Red"), 173 | TFICategory(TargetFrameworkIdentifiers.NETCore, "Green"), 174 | TFICategory(TargetFrameworkIdentifiers.NETStandard, "LightGreen"), 175 | TFICategory(TargetFrameworkIdentifiers.NETPortable, "Lime"), 176 | new XElement(XName.Get("Category", DgmlNamespace), new XAttribute("Id", RuntimeAssemblyCategory), new XAttribute("Icon", "pack://application:,,,/Microsoft.VisualStudio.Progression.GraphControl;component/Icons/Library.png"))); 177 | 178 | foreach (KeyValuePair item in bestTargetFrameworkPerAssembly) 179 | { 180 | XElement node = new( 181 | XName.Get("Node", DgmlNamespace), 182 | new XAttribute("Id", item.Value.AssemblyName), 183 | new XAttribute("Category", item.Value.TargetFrameworkIdentifier?.ToString() ?? item.Value.TargetFramework?.Identifier ?? string.Empty)); 184 | if (item.Value.IsRuntimeAssembly) 185 | { 186 | node.Add(new XElement(XName.Get("Category", DgmlNamespace), new XAttribute("Ref", RuntimeAssemblyCategory))); 187 | } 188 | 189 | nodesElement.Add(node); 190 | 191 | foreach (string reference in item.Value.References) 192 | { 193 | // Only create the edge if the target node is an assembly that was scanned. 194 | if (bestTargetFrameworkPerAssembly.ContainsKey(reference)) 195 | { 196 | linksElement.Add(new XElement( 197 | XName.Get("Link", DgmlNamespace), 198 | new XAttribute("Source", item.Value.AssemblyName), 199 | new XAttribute("Target", reference))); 200 | } 201 | } 202 | } 203 | 204 | XElement root = new( 205 | XName.Get("DirectedGraph", DgmlNamespace), 206 | new XAttribute("Title", "Assembly dependency graph with TargetFrameworks"), 207 | nodesElement, 208 | linksElement, 209 | categoriesElement); 210 | using FileStream dgmlFile = new(this.Dgml, FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: true); 211 | await root.SaveAsync(dgmlFile, SaveOptions.None, cancellationToken); 212 | } 213 | 214 | return exitCode; 215 | } 216 | 217 | private static bool Equals(ReadOnlySpan array1, ReadOnlySpan array2) 218 | { 219 | if (array1.Length != array2.Length) 220 | { 221 | return false; 222 | } 223 | 224 | for (int i = 0; i < array1.Length; i++) 225 | { 226 | if (array1[i] != array2[i]) 227 | { 228 | return false; 229 | } 230 | } 231 | 232 | return true; 233 | } 234 | 235 | /// 236 | /// Used to invoke from the debugger to formulate the string to include in . 237 | /// 238 | private static string ByteArrayToCSharp(ReadOnlySpan buffer) 239 | { 240 | StringBuilder builder = new(); 241 | builder.Append("new byte[] { "); 242 | for (int i = 0; i < buffer.Length; i++) 243 | { 244 | if (i > 0) 245 | { 246 | builder.Append(", "); 247 | } 248 | 249 | builder.Append($"0x{buffer[i]:x2}"); 250 | } 251 | 252 | builder.Append(" }, // "); 253 | 254 | for (int i = 0; i < buffer.Length; i++) 255 | { 256 | builder.Append($"{buffer[i]:x2}"); 257 | } 258 | 259 | return builder.ToString(); 260 | } 261 | 262 | private record AssemblyInfo(string AssemblyName, FrameworkName? TargetFramework, List References, bool IsRuntimeAssembly) 263 | { 264 | public string? AssemblyPath { get; set; } 265 | 266 | internal TargetFrameworkIdentifiers? TargetFrameworkIdentifier 267 | { 268 | get 269 | { 270 | return 271 | this.TargetFramework is null ? TargetFrameworkIdentifiers.NETFramework : 272 | ".NETFramework".Equals(this.TargetFramework.Identifier, StringComparison.OrdinalIgnoreCase) ? TargetFrameworkIdentifiers.NETFramework : 273 | ".NETStandard".Equals(this.TargetFramework.Identifier, StringComparison.OrdinalIgnoreCase) ? TargetFrameworkIdentifiers.NETStandard : 274 | ".NETCoreApp".Equals(this.TargetFramework.Identifier, StringComparison.OrdinalIgnoreCase) ? TargetFrameworkIdentifiers.NETCore : 275 | ".NETPortable".Equals(this.TargetFramework.Identifier, StringComparison.OrdinalIgnoreCase) ? TargetFrameworkIdentifiers.NETPortable : 276 | null; 277 | } 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/AssemblyRefScanner/TypeRefScanner.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace AssemblyRefScanner; 5 | 6 | internal class TypeRefScanner : ScannerBase 7 | { 8 | internal required string Path { get; init; } 9 | 10 | internal required string? DeclaringAssembly { get; init; } 11 | 12 | internal required string? Namespace { get; init; } 13 | 14 | internal required string TypeName { get; init; } 15 | 16 | internal async Task Execute(CancellationToken cancellationToken) 17 | { 18 | var scanner = this.CreateProcessAssembliesBlock( 19 | mdReader => 20 | { 21 | if (GetBreakingChangedTypeReference(mdReader, this.DeclaringAssembly, this.Namespace, this.TypeName) is TypeReferenceHandle interestingTypeHandle) 22 | { 23 | return true; 24 | } 25 | 26 | return false; 27 | }, 28 | cancellationToken); 29 | var report = this.CreateReportBlock( 30 | scanner, 31 | (assemblyPath, result) => 32 | { 33 | if (result) 34 | { 35 | Console.WriteLine(TrimBasePath(assemblyPath, this.Path)); 36 | } 37 | }, 38 | cancellationToken); 39 | return await this.Scan(this.Path, scanner, report, cancellationToken); 40 | } 41 | 42 | private static TypeReferenceHandle? GetBreakingChangedTypeReference(MetadataReader mdReader, string? declaringAssembly, string? typeNamespace, string typeName) 43 | { 44 | if (declaringAssembly is not null && !HasAssemblyReference(mdReader, declaringAssembly)) 45 | { 46 | return null; 47 | } 48 | 49 | foreach (TypeReferenceHandle typeRefHandle in mdReader.TypeReferences) 50 | { 51 | TypeReference typeRef = mdReader.GetTypeReference(typeRefHandle); 52 | if (mdReader.StringComparer.Equals(typeRef.Name, typeName) && 53 | (typeNamespace is null || mdReader.StringComparer.Equals(typeRef.Namespace, typeNamespace))) 54 | { 55 | return typeRefHandle; 56 | } 57 | } 58 | 59 | return null; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | README.md 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /strongname.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AArnott/AssemblyRefScanner/4f6a4c693d1c6bac0b0caaa233e77b39817b224e/strongname.snk -------------------------------------------------------------------------------- /stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "documentationRules": { 5 | "companyName": "Andrew Arnott", 6 | "copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the {licenseName} license. See {licenseFile} file in the project root for full license information.", 7 | "variables": { 8 | "licenseName": "MIT", 9 | "licenseFile": "LICENSE" 10 | }, 11 | "fileNamingConvention": "metadata", 12 | "xmlHeader": false 13 | }, 14 | "orderingRules": { 15 | "usingDirectivesPlacement": "outsideNamespace" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # SA1600: Elements should be documented 4 | dotnet_diagnostic.SA1600.severity = silent 5 | 6 | # SA1601: Partial elements should be documented 7 | dotnet_diagnostic.SA1601.severity = silent 8 | 9 | # SA1602: Enumeration items should be documented 10 | dotnet_diagnostic.SA1602.severity = silent 11 | 12 | # SA1615: Element return value should be documented 13 | dotnet_diagnostic.SA1615.severity = silent 14 | 15 | # VSTHRD103: Call async methods when in an async method 16 | dotnet_diagnostic.VSTHRD103.severity = silent 17 | 18 | # VSTHRD111: Use .ConfigureAwait(bool) 19 | dotnet_diagnostic.VSTHRD111.severity = none 20 | 21 | # VSTHRD200: Use Async suffix for async methods 22 | dotnet_diagnostic.VSTHRD200.severity = silent 23 | 24 | # CA1014: Mark assemblies with CLSCompliant 25 | dotnet_diagnostic.CA1014.severity = none 26 | 27 | # CA1050: Declare types in namespaces 28 | dotnet_diagnostic.CA1050.severity = none 29 | 30 | # CA1303: Do not pass literals as localized parameters 31 | dotnet_diagnostic.CA1303.severity = none 32 | 33 | # CS1591: Missing XML comment for publicly visible type or member 34 | dotnet_diagnostic.CS1591.severity = silent 35 | 36 | # CA1707: Identifiers should not contain underscores 37 | dotnet_diagnostic.CA1707.severity = silent 38 | 39 | # CA1062: Validate arguments of public methods 40 | dotnet_diagnostic.CA1062.severity = suggestion 41 | 42 | # CA1063: Implement IDisposable Correctly 43 | dotnet_diagnostic.CA1063.severity = silent 44 | 45 | # CA1816: Dispose methods should call SuppressFinalize 46 | dotnet_diagnostic.CA1816.severity = silent 47 | 48 | # CA2007: Consider calling ConfigureAwait on the awaited task 49 | dotnet_diagnostic.CA2007.severity = none 50 | 51 | # SA1401: Fields should be private 52 | dotnet_diagnostic.SA1401.severity = silent 53 | 54 | # SA1133: Do not combine attributes 55 | dotnet_diagnostic.SA1133.severity = silent 56 | -------------------------------------------------------------------------------- /test/AssemblyRefScanner.Tests/AssemblyRefScanner.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | Exe 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/AssemblyRefScanner.Tests/DocIdBuilderTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Reflection; 5 | using System.Reflection.Metadata; 6 | using System.Reflection.PortableExecutable; 7 | using AssemblyRefScanner; 8 | 9 | public class DocIdBuilderTests : IDisposable 10 | { 11 | private readonly ITestOutputHelper logger; 12 | private readonly FileStream assemblyStream; 13 | private readonly PEReader peReader; 14 | private readonly MetadataReader reader; 15 | private readonly DocIdBuilder docIdBuilder; 16 | 17 | public DocIdBuilderTests(ITestOutputHelper logger) 18 | { 19 | this.logger = logger; 20 | 21 | try 22 | { 23 | this.assemblyStream = File.OpenRead(Assembly.GetExecutingAssembly().Location); 24 | this.peReader = new(this.assemblyStream); 25 | this.reader = this.peReader.GetMetadataReader(); 26 | this.docIdBuilder = new(this.reader); 27 | } 28 | catch 29 | { 30 | this.assemblyStream?.Dispose(); 31 | this.peReader?.Dispose(); 32 | throw; 33 | } 34 | } 35 | 36 | public void Dispose() 37 | { 38 | this.peReader.Dispose(); 39 | this.assemblyStream.Dispose(); 40 | } 41 | 42 | [Fact] 43 | public void Types() 44 | { 45 | string[] expected = [ 46 | "T:DocIdSamples.ColorA", 47 | "T:DocIdSamples.IProcess", 48 | "T:DocIdSamples.ValueType", 49 | "T:DocIdSamples.Widget", 50 | "T:DocIdSamples.MyList`1", 51 | "T:DocIdSamples.UseList", 52 | "T:DocIdSamples.Widget.NestedClass", 53 | "T:DocIdSamples.Widget.IMenuItem", 54 | "T:DocIdSamples.Widget.Del", 55 | "T:DocIdSamples.Widget.Direction", 56 | "T:DocIdSamples.MyList`1.Helper`2", 57 | ]; 58 | this.AssertMatchingDocIds(expected, this.reader.TypeDefinitions.Select(h => (EntityHandle)h)); 59 | } 60 | 61 | [Fact] 62 | public void Fields() 63 | { 64 | string[] expected = [ 65 | "F:DocIdSamples.ColorA.value__", 66 | "F:DocIdSamples.ColorA.Red", 67 | "F:DocIdSamples.ColorA.Blue", 68 | "F:DocIdSamples.ColorA.Green", 69 | "F:DocIdSamples.ValueType.total", 70 | "F:DocIdSamples.Widget.AnEvent", 71 | "F:DocIdSamples.Widget.message", 72 | "F:DocIdSamples.Widget.defaultColor", 73 | "F:DocIdSamples.Widget.PI", 74 | "F:DocIdSamples.Widget.monthlyAverage", 75 | "F:DocIdSamples.Widget.array1", 76 | "F:DocIdSamples.Widget.array2", 77 | "F:DocIdSamples.Widget.pCount", 78 | "F:DocIdSamples.Widget.ppValues", 79 | "F:DocIdSamples.Widget.Direction.value__", 80 | "F:DocIdSamples.Widget.Direction.North", 81 | "F:DocIdSamples.Widget.Direction.South", 82 | "F:DocIdSamples.Widget.Direction.East", 83 | "F:DocIdSamples.Widget.Direction.West", 84 | ]; 85 | this.AssertMatchingDocIds(expected, this.reader.FieldDefinitions.Select(h => (EntityHandle)h)); 86 | } 87 | 88 | [Fact] 89 | public void Methods() 90 | { 91 | string[] expected = [ 92 | "M:DocIdSamples.ValueType.M(System.Int32)", 93 | "M:DocIdSamples.ValueType.P_AnEvent(System.Int32)", 94 | "M:DocIdSamples.Widget.#cctor", 95 | "M:DocIdSamples.Widget.#ctor", 96 | "M:DocIdSamples.Widget.#ctor(System.String)", 97 | "M:DocIdSamples.Widget.Finalize", 98 | "M:DocIdSamples.Widget.op_Addition(DocIdSamples.Widget,DocIdSamples.Widget)", 99 | "M:DocIdSamples.Widget.op_Explicit(DocIdSamples.Widget)", 100 | "M:DocIdSamples.Widget.op_Implicit(DocIdSamples.Widget)", 101 | "M:DocIdSamples.Widget.add_AnEvent(DocIdSamples.Widget.Del)", 102 | "M:DocIdSamples.Widget.remove_AnEvent(DocIdSamples.Widget.Del)", 103 | "M:DocIdSamples.Widget.M0", 104 | "M:DocIdSamples.Widget.M1(System.Char,System.Single@,DocIdSamples.ValueType@,System.Int32@)", 105 | "M:DocIdSamples.Widget.M2(System.Int16[],System.Int32[0:,0:],System.Int64[][])", 106 | "M:DocIdSamples.Widget.M3(System.Int64[][],DocIdSamples.Widget[0:,0:,0:][])", 107 | "M:DocIdSamples.Widget.M4(System.Char*,System.Drawing.Color**)", 108 | "M:DocIdSamples.Widget.M5(System.Void*,System.Double*[0:,0:][])", 109 | "M:DocIdSamples.Widget.M6(System.Int32,System.Object[])", 110 | "M:DocIdSamples.Widget.M7(System.ReadOnlySpan{System.Char})", 111 | "M:DocIdSamples.Widget.get_Width", 112 | "M:DocIdSamples.Widget.set_Width(System.Int32)", 113 | "M:DocIdSamples.Widget.get_Item(System.Int32)", 114 | "M:DocIdSamples.Widget.set_Item(System.Int32,System.Int32)", 115 | "M:DocIdSamples.Widget.get_Item(System.String,System.Int32)", 116 | "M:DocIdSamples.Widget.set_Item(System.String,System.Int32,System.Int32)", 117 | "M:DocIdSamples.MyList`1.Test(`0)", 118 | "M:DocIdSamples.MyList`1.#ctor", 119 | "M:DocIdSamples.UseList.Process(DocIdSamples.MyList{System.Int32})", 120 | "M:DocIdSamples.UseList.GetValues``1(``0)", 121 | "M:DocIdSamples.UseList.#ctor", 122 | "M:DocIdSamples.Widget.NestedClass.M(System.Int32)", 123 | "M:DocIdSamples.Widget.NestedClass.#ctor", 124 | "M:DocIdSamples.Widget.Del.#ctor(System.Object,System.IntPtr)", 125 | "M:DocIdSamples.Widget.Del.Invoke(System.Int32)", 126 | "M:DocIdSamples.Widget.Del.BeginInvoke(System.Int32,System.AsyncCallback,System.Object)", 127 | "M:DocIdSamples.Widget.Del.EndInvoke(System.IAsyncResult)", 128 | "M:DocIdSamples.MyList`1.Helper`2.#ctor", 129 | ]; 130 | this.AssertMatchingDocIds(expected, this.reader.MethodDefinitions.Select(h => (EntityHandle)h)); 131 | } 132 | 133 | [Fact] 134 | public void Events() 135 | { 136 | string[] expected = [ 137 | "E:DocIdSamples.Widget.Del.AnEvent", 138 | ]; 139 | this.AssertMatchingDocIds(expected, this.reader.EventDefinitions.Select(e => (EntityHandle)e)); 140 | } 141 | 142 | [Fact] 143 | public void Properties() 144 | { 145 | string[] expected = [ 146 | "P:DocIdSamples.Widget.Width", 147 | "P:DocIdSamples.Widget.Item(System.Int32)", 148 | "P:DocIdSamples.Widget.Item(System.String,System.Int32)", 149 | ]; 150 | this.AssertMatchingDocIds(expected, this.reader.PropertyDefinitions.Select(h => (EntityHandle)h)); 151 | } 152 | 153 | [Fact] 154 | public void NoNamespace() 155 | { 156 | TypeDefinitionHandle selfHandle = this.reader.TypeDefinitions.Single(h => this.reader.StringComparer.Equals(this.reader.GetTypeDefinition(h).Name, nameof(DocIdBuilderTests))); 157 | Assert.Equal("T:DocIdBuilderTests", this.docIdBuilder.GetDocumentationCommentId(selfHandle)); 158 | } 159 | 160 | private void AssertMatchingDocIds(string[] expectedDocIds, IEnumerable apis) 161 | { 162 | List actualDocIds = new(); 163 | foreach (EntityHandle handle in apis) 164 | { 165 | string? docId = this.docIdBuilder.GetDocumentationCommentId(handle); 166 | if (docId?.Contains("DocIdSamples") is true) 167 | { 168 | actualDocIds.Add(docId); 169 | this.logger.WriteLine(docId); 170 | } 171 | } 172 | 173 | Array.Sort(expectedDocIds, StringComparer.Ordinal); 174 | actualDocIds.Sort(StringComparer.Ordinal); 175 | 176 | Assert.Equal(expectedDocIds, actualDocIds); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /test/AssemblyRefScanner.Tests/DocIdParserTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Reflection; 5 | using System.Reflection.Metadata; 6 | using System.Reflection.PortableExecutable; 7 | 8 | public class DocIdParserTests : IDisposable 9 | { 10 | private readonly ITestOutputHelper logger; 11 | private readonly FileStream assemblyStream; 12 | private readonly PEReader peReader; 13 | private readonly MetadataReader reader; 14 | private readonly DocIdBuilder docIdBuilder; 15 | 16 | public DocIdParserTests(ITestOutputHelper logger) 17 | { 18 | this.logger = logger; 19 | 20 | try 21 | { 22 | this.assemblyStream = File.OpenRead(Assembly.GetExecutingAssembly().Location); 23 | this.peReader = new(this.assemblyStream); 24 | this.reader = this.peReader.GetMetadataReader(); 25 | this.docIdBuilder = new(this.reader); 26 | } 27 | catch 28 | { 29 | this.assemblyStream?.Dispose(); 30 | this.peReader?.Dispose(); 31 | throw; 32 | } 33 | } 34 | 35 | public void Dispose() 36 | { 37 | this.peReader.Dispose(); 38 | this.assemblyStream.Dispose(); 39 | } 40 | 41 | [Fact] 42 | public void Parse_IsMatch_TypeDefinitions() 43 | { 44 | Dictionary dict = this.reader.TypeDefinitions.ToDictionary( 45 | h => h, 46 | h => this.docIdBuilder.GetDocumentationCommentId(h)); 47 | foreach ((TypeDefinitionHandle h, string docId) in dict) 48 | { 49 | this.logger.WriteLine(docId); 50 | DocId.Descriptor match = DocId.Parse(docId); 51 | Assert.Equal(DocId.ApiKind.Type, match.Kind); 52 | foreach ((TypeDefinitionHandle candidateHandle, string candidateDocId) in dict) 53 | { 54 | Assert.Equal(candidateHandle.Equals(h), match.IsMatch(candidateHandle, this.reader)); 55 | } 56 | } 57 | } 58 | 59 | [Fact] 60 | public void Parse_IsMatch_MethodDefinitions() 61 | { 62 | Dictionary dict = this.reader.MethodDefinitions.ToDictionary( 63 | h => h, 64 | h => this.docIdBuilder.GetDocumentationCommentId(h)); 65 | foreach ((MethodDefinitionHandle h, string docId) in dict) 66 | { 67 | this.logger.WriteLine(docId); 68 | DocId.Descriptor match = DocId.Parse(docId); 69 | Assert.Equal(DocId.ApiKind.Method, match.Kind); 70 | foreach ((MethodDefinitionHandle candidateHandle, string candidateDocId) in dict) 71 | { 72 | Assert.Equal(candidateHandle.Equals(h), match.IsMatch(candidateHandle, this.reader)); 73 | } 74 | } 75 | } 76 | 77 | [Fact] 78 | public void Parse_IsMatch_PropertyDefinitions() 79 | { 80 | Dictionary dict = this.reader.PropertyDefinitions.ToDictionary( 81 | h => h, 82 | h => this.docIdBuilder.GetDocumentationCommentId(h)); 83 | foreach ((PropertyDefinitionHandle h, string docId) in dict) 84 | { 85 | this.logger.WriteLine(docId); 86 | DocId.Descriptor match = DocId.Parse(docId); 87 | Assert.Equal(DocId.ApiKind.Property, match.Kind); 88 | foreach ((PropertyDefinitionHandle candidateHandle, string candidateDocId) in dict) 89 | { 90 | Assert.Equal(candidateHandle.Equals(h), match.IsMatch(candidateHandle, this.reader)); 91 | } 92 | } 93 | } 94 | 95 | [Fact] 96 | public void Parse_IsMatch_EventDefinitions() 97 | { 98 | Dictionary dict = this.reader.EventDefinitions.ToDictionary( 99 | h => h, 100 | h => this.docIdBuilder.GetDocumentationCommentId(h)); 101 | foreach ((EventDefinitionHandle h, string docId) in dict) 102 | { 103 | this.logger.WriteLine(docId); 104 | DocId.Descriptor match = DocId.Parse(docId); 105 | Assert.Equal(DocId.ApiKind.Event, match.Kind); 106 | foreach ((EventDefinitionHandle candidateHandle, string candidateDocId) in dict) 107 | { 108 | Assert.Equal(candidateHandle.Equals(h), match.IsMatch(candidateHandle, this.reader)); 109 | } 110 | } 111 | } 112 | 113 | [Fact] 114 | public void Parse_IsMatch_FieldDefinitions() 115 | { 116 | Dictionary dict = this.reader.FieldDefinitions.ToDictionary( 117 | h => h, 118 | h => this.docIdBuilder.GetDocumentationCommentId(h)); 119 | foreach ((FieldDefinitionHandle h, string docId) in dict) 120 | { 121 | this.logger.WriteLine(docId); 122 | DocId.Descriptor match = DocId.Parse(docId); 123 | Assert.Equal(DocId.ApiKind.Field, match.Kind); 124 | foreach ((FieldDefinitionHandle candidateHandle, string candidateDocId) in dict) 125 | { 126 | Assert.Equal(candidateHandle.Equals(h), match.IsMatch(candidateHandle, this.reader)); 127 | } 128 | } 129 | } 130 | 131 | [Fact] 132 | public void Parse_IsMatch_TypeReferences() 133 | { 134 | Dictionary dict = this.reader.TypeReferences.ToDictionary( 135 | h => h, 136 | h => this.docIdBuilder.GetDocumentationCommentId(h)); 137 | foreach ((TypeReferenceHandle h, string docId) in dict) 138 | { 139 | this.logger.WriteLine(docId); 140 | DocId.Descriptor match = DocId.Parse(docId); 141 | Assert.Equal(DocId.ApiKind.Type, match.Kind); 142 | foreach ((TypeReferenceHandle candidateHandle, string candidateDocId) in dict) 143 | { 144 | bool expectedMatch = this.docIdBuilder.GetDocumentationCommentId(candidateHandle) == docId; 145 | Assert.Equal(expectedMatch, match.IsMatch(candidateHandle, this.reader)); 146 | } 147 | } 148 | } 149 | 150 | [Fact] 151 | public void Parse_IsMatch_MemberReferences() 152 | { 153 | Dictionary dict = this.reader.MemberReferences.ToDictionary( 154 | h => h, 155 | h => this.docIdBuilder.GetDocumentationCommentId(h)); 156 | foreach ((MemberReferenceHandle h, string docId) in dict) 157 | { 158 | this.logger.WriteLine(docId); 159 | DocId.Descriptor match = DocId.Parse(docId); 160 | Assert.NotEqual(DocId.ApiKind.Type, match.Kind); 161 | foreach ((MemberReferenceHandle candidateHandle, string candidateDocId) in dict) 162 | { 163 | bool expectedMatch = this.docIdBuilder.GetDocumentationCommentId(candidateHandle) == docId; 164 | Assert.Equal(expectedMatch, match.IsMatch(candidateHandle, this.reader)); 165 | } 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /test/AssemblyRefScanner.Tests/DocIdSamples.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | // Copyright (c) Microsoft. All rights reserved. 5 | #nullable disable 6 | 7 | #pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type 8 | #pragma warning disable CS0169,CS0067 // unused fields and events 9 | #pragma warning disable SA1136 // Enum values should be on separate lines 10 | #pragma warning disable SA1201 // Elements should appear in the correct order 11 | #pragma warning disable SA1202 // Elements should be ordered by access 12 | #pragma warning disable SA1203 // Constants should appear before fields 13 | #pragma warning disable SA1204 // Static elements should appear before instance elements 14 | #pragma warning disable SA1314 // Type parameter names should begin with T 15 | #pragma warning disable SA1400 // Access modifier should be declared 16 | #pragma warning disable SA1402 // File may only contain a single type 17 | #pragma warning disable SA1502 // Element should not be on a single line 18 | #pragma warning disable SA1649 // File name should match first type name 19 | 20 | using System.Drawing; 21 | 22 | // This file contains a variety of API shapes to test the DocID creation code. 23 | // Changes to this file require updates to the expected DocIDs in the tests. 24 | namespace DocIdSamples; 25 | 26 | enum ColorA { Red, Blue, Green } 27 | 28 | public interface IProcess 29 | { 30 | } 31 | 32 | public struct ValueType 33 | { 34 | private int total; 35 | 36 | public void M(int i) 37 | { 38 | Widget p = new(); 39 | p.AnEvent += this.P_AnEvent; 40 | } 41 | 42 | private void P_AnEvent(int i) => throw new NotImplementedException(); 43 | } 44 | 45 | public class Widget : IProcess 46 | { 47 | // ctors 48 | static Widget() { } 49 | 50 | public Widget() { } 51 | 52 | public Widget(string s) { } 53 | 54 | ~Widget() { } 55 | 56 | // operators 57 | public static Widget operator +(Widget x1, Widget x2) => null; 58 | 59 | public static explicit operator int(Widget x) => 0; 60 | 61 | public static implicit operator long(Widget x) => 0; 62 | 63 | // events 64 | public event Del AnEvent; 65 | 66 | // fields 67 | private string message; 68 | private static ColorA defaultColor; 69 | private const double PI = 3.14159; 70 | protected readonly double monthlyAverage; 71 | private long[] array1; 72 | private Widget[,] array2; 73 | private unsafe int* pCount; 74 | private unsafe float** ppValues; 75 | 76 | // methods 77 | public static void M0() { } 78 | 79 | public void M1(char c, out float f, ref ValueType v, in int i) => f = 0; 80 | 81 | public void M2(short[] x1, int[,] x2, long[][] x3) { } 82 | 83 | public void M3(long[][] x3, Widget[][,,] x4) { } 84 | 85 | public unsafe void M4(char* pc, Color** pf) { } 86 | 87 | public unsafe void M5(void* pv, double*[][,] pd) { } 88 | 89 | public void M6(int i, params object[] args) { } 90 | 91 | public void M7(ReadOnlySpan x) { } 92 | 93 | // properties and indexes 94 | public int Width { get => 0; set { } } 95 | 96 | public int this[int i] { get => 0; set { } } 97 | 98 | public int this[string s, int i] { get => 0; set { } } 99 | 100 | // nested types 101 | public class NestedClass 102 | { 103 | public void M(int i) { } 104 | } 105 | 106 | public interface IMenuItem { } 107 | 108 | public delegate void Del(int i); 109 | 110 | public enum Direction { North, South, East, West } 111 | } 112 | 113 | public class MyList 114 | { 115 | public void Test(T t) { } 116 | 117 | class Helper { } 118 | } 119 | 120 | public class UseList 121 | { 122 | public void Process(MyList list) { } 123 | 124 | public MyList GetValues(T value) => null; 125 | } 126 | -------------------------------------------------------------------------------- /test/AssemblyRefScanner.Tests/EmbeddedTypeScannerTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using Xunit; 5 | 6 | public class EmbeddedTypeScannerTests 7 | { 8 | public EmbeddedTypeScannerTests() 9 | { 10 | } 11 | 12 | [Fact] 13 | public void AddOrSubtract() 14 | { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/AssemblyRefScanner.Tests/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | false 7 | true 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tools/Check-DotNetRuntime.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Checks whether a given .NET Core runtime is installed. 4 | #> 5 | [CmdletBinding()] 6 | Param ( 7 | [Parameter()] 8 | [ValidateSet('Microsoft.AspNetCore.App','Microsoft.NETCore.App')] 9 | [string]$Runtime='Microsoft.NETCore.App', 10 | [Parameter(Mandatory=$true)] 11 | [Version]$Version 12 | ) 13 | 14 | $dotnet = Get-Command dotnet -ErrorAction SilentlyContinue 15 | if (!$dotnet) { 16 | # Nothing is installed. 17 | Write-Output $false 18 | exit 1 19 | } 20 | 21 | Function IsVersionMatch { 22 | Param( 23 | [Parameter()] 24 | $actualVersion 25 | ) 26 | return $actualVersion -and 27 | $Version.Major -eq $actualVersion.Major -and 28 | $Version.Minor -eq $actualVersion.Minor -and 29 | (($Version.Build -eq -1) -or ($Version.Build -eq $actualVersion.Build)) -and 30 | (($Version.Revision -eq -1) -or ($Version.Revision -eq $actualVersion.Revision)) 31 | } 32 | 33 | $installedRuntimes = dotnet --list-runtimes |? { $_.Split()[0] -ieq $Runtime } |% { $v = $null; [Version]::tryparse($_.Split()[1], [ref] $v); $v } 34 | $matchingRuntimes = $installedRuntimes |? { IsVersionMatch -actualVersion $_ } 35 | if (!$matchingRuntimes) { 36 | Write-Output $false 37 | exit 1 38 | } 39 | 40 | Write-Output $true 41 | exit 0 42 | -------------------------------------------------------------------------------- /tools/Check-DotNetSdk.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Checks whether the .NET Core SDK required by this repo is installed. 4 | #> 5 | [CmdletBinding()] 6 | Param ( 7 | ) 8 | 9 | $dotnet = Get-Command dotnet -ErrorAction SilentlyContinue 10 | if (!$dotnet) { 11 | # Nothing is installed. 12 | Write-Output $false 13 | exit 1 14 | } 15 | 16 | # We need to set the current directory so dotnet considers the SDK required by our global.json file. 17 | Push-Location "$PSScriptRoot\.." 18 | try { 19 | dotnet -h 2>&1 | Out-Null 20 | if (($LASTEXITCODE -eq 129) -or # On Linux 21 | ($LASTEXITCODE -eq -2147450751) # On Windows 22 | ) { 23 | # These exit codes indicate no matching SDK exists. 24 | Write-Output $false 25 | exit 2 26 | } 27 | 28 | # The required SDK is already installed! 29 | Write-Output $true 30 | exit 0 31 | } catch { 32 | # I don't know why, but on some build agents (e.g. MicroBuild), an exception is thrown from the `dotnet` invocation when a match is not found. 33 | Write-Output $false 34 | exit 3 35 | } finally { 36 | Pop-Location 37 | } 38 | -------------------------------------------------------------------------------- /tools/Get-ArtifactsStagingDirectory.ps1: -------------------------------------------------------------------------------- 1 | Param( 2 | [switch]$CleanIfLocal 3 | ) 4 | if ($env:BUILD_ARTIFACTSTAGINGDIRECTORY) { 5 | $ArtifactStagingFolder = $env:BUILD_ARTIFACTSTAGINGDIRECTORY 6 | } elseif ($env:RUNNER_TEMP) { 7 | $ArtifactStagingFolder = "$env:RUNNER_TEMP\_artifacts" 8 | } else { 9 | $ArtifactStagingFolder = [System.IO.Path]::GetFullPath("$PSScriptRoot/../obj/_artifacts") 10 | if ($CleanIfLocal -and (Test-Path $ArtifactStagingFolder)) { 11 | Remove-Item $ArtifactStagingFolder -Recurse -Force 12 | } 13 | } 14 | 15 | $ArtifactStagingFolder 16 | -------------------------------------------------------------------------------- /tools/Get-CodeCovTool.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Downloads the CodeCov.io uploader tool and returns the path to it. 4 | .PARAMETER AllowSkipVerify 5 | Allows skipping signature verification of the downloaded tool if gpg is not installed. 6 | #> 7 | [CmdletBinding()] 8 | Param( 9 | [switch]$AllowSkipVerify 10 | ) 11 | 12 | if ($IsMacOS) { 13 | $codeCovUrl = "https://uploader.codecov.io/latest/macos/codecov" 14 | $toolName = 'codecov' 15 | } 16 | elseif ($IsLinux) { 17 | $codeCovUrl = "https://uploader.codecov.io/latest/linux/codecov" 18 | $toolName = 'codecov' 19 | } 20 | else { 21 | $codeCovUrl = "https://uploader.codecov.io/latest/windows/codecov.exe" 22 | $toolName = 'codecov.exe' 23 | } 24 | 25 | $shaSuffix = ".SHA256SUM" 26 | $sigSuffix = $shaSuffix + ".sig" 27 | 28 | Function Get-FileFromWeb([Uri]$Uri, $OutDir) { 29 | $OutFile = Join-Path $OutDir $Uri.Segments[-1] 30 | if (!(Test-Path $OutFile)) { 31 | Write-Verbose "Downloading $Uri..." 32 | if (!(Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null } 33 | try { 34 | (New-Object System.Net.WebClient).DownloadFile($Uri, $OutFile) 35 | } finally { 36 | # This try/finally causes the script to abort 37 | } 38 | } 39 | 40 | $OutFile 41 | } 42 | 43 | $toolsPath = & "$PSScriptRoot\Get-TempToolsPath.ps1" 44 | $binaryToolsPath = Join-Path $toolsPath codecov 45 | $testingPath = Join-Path $binaryToolsPath unverified 46 | $finalToolPath = Join-Path $binaryToolsPath $toolName 47 | 48 | if (!(Test-Path $finalToolPath)) { 49 | if (Test-Path $testingPath) { 50 | Remove-Item -Recurse -Force $testingPath # ensure we download all matching files 51 | } 52 | $tool = Get-FileFromWeb $codeCovUrl $testingPath 53 | $sha = Get-FileFromWeb "$codeCovUrl$shaSuffix" $testingPath 54 | $sig = Get-FileFromWeb "$codeCovUrl$sigSuffix" $testingPath 55 | $key = Get-FileFromWeb https://keybase.io/codecovsecurity/pgp_keys.asc $testingPath 56 | 57 | if ((Get-Command gpg -ErrorAction SilentlyContinue)) { 58 | Write-Host "Importing codecov key" -ForegroundColor Yellow 59 | gpg --import $key 60 | Write-Host "Verifying signature on codecov hash" -ForegroundColor Yellow 61 | gpg --verify $sig $sha 62 | } else { 63 | if ($AllowSkipVerify) { 64 | Write-Warning "gpg not found. Unable to verify hash signature." 65 | } else { 66 | throw "gpg not found. Unable to verify hash signature. Install gpg or add -AllowSkipVerify to override." 67 | } 68 | } 69 | 70 | Write-Host "Verifying hash on downloaded tool" -ForegroundColor Yellow 71 | $actualHash = (Get-FileHash -LiteralPath $tool -Algorithm SHA256).Hash 72 | $expectedHash = (Get-Content $sha).Split()[0] 73 | if ($actualHash -ne $expectedHash) { 74 | # Validation failed. Delete the tool so we can't execute it. 75 | #Remove-Item $codeCovPath 76 | throw "codecov uploader tool failed signature validation." 77 | } 78 | 79 | Copy-Item $tool $finalToolPath 80 | 81 | if ($IsMacOS -or $IsLinux) { 82 | chmod u+x $finalToolPath 83 | } 84 | } 85 | 86 | return $finalToolPath 87 | -------------------------------------------------------------------------------- /tools/Get-LibTemplateBasis.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Returns the name of the well-known branch in the Library.Template repository upon which HEAD is based. 4 | #> 5 | [CmdletBinding(SupportsShouldProcess = $true)] 6 | Param( 7 | [switch]$ErrorIfNotRelated 8 | ) 9 | 10 | # This list should be sorted in order of decreasing specificity. 11 | $branchMarkers = @( 12 | @{ commit = 'fd0a7b25ccf030bbd16880cca6efe009d5b1fffc'; branch = 'microbuild' }; 13 | @{ commit = '05f49ce799c1f9cc696d53eea89699d80f59f833'; branch = 'main' }; 14 | ) 15 | 16 | foreach ($entry in $branchMarkers) { 17 | if (git rev-list HEAD | Select-String -Pattern $entry.commit) { 18 | return $entry.branch 19 | } 20 | } 21 | 22 | if ($ErrorIfNotRelated) { 23 | Write-Error "Library.Template has not been previously merged with this repo. Please review https://github.com/AArnott/Library.Template/tree/main?tab=readme-ov-file#readme for instructions." 24 | exit 1 25 | } 26 | -------------------------------------------------------------------------------- /tools/Get-NuGetTool.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Downloads the NuGet.exe tool and returns the path to it. 4 | .PARAMETER NuGetVersion 5 | The version of the NuGet tool to acquire. 6 | #> 7 | Param( 8 | [Parameter()] 9 | [string]$NuGetVersion='6.4.0' 10 | ) 11 | 12 | $toolsPath = & "$PSScriptRoot\Get-TempToolsPath.ps1" 13 | $binaryToolsPath = Join-Path $toolsPath $NuGetVersion 14 | if (!(Test-Path $binaryToolsPath)) { $null = mkdir $binaryToolsPath } 15 | $nugetPath = Join-Path $binaryToolsPath nuget.exe 16 | 17 | if (!(Test-Path $nugetPath)) { 18 | Write-Host "Downloading nuget.exe $NuGetVersion..." -ForegroundColor Yellow 19 | (New-Object System.Net.WebClient).DownloadFile("https://dist.nuget.org/win-x86-commandline/v$NuGetVersion/NuGet.exe", $nugetPath) 20 | } 21 | 22 | return (Resolve-Path $nugetPath).Path 23 | -------------------------------------------------------------------------------- /tools/Get-ProcDump.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Downloads 32-bit and 64-bit procdump executables and returns the path to where they were installed. 4 | #> 5 | $version = '0.0.1' 6 | $baseDir = "$PSScriptRoot\..\obj\tools" 7 | $procDumpToolPath = "$baseDir\procdump.$version\bin" 8 | if (-not (Test-Path $procDumpToolPath)) { 9 | if (-not (Test-Path $baseDir)) { New-Item -Type Directory -Path $baseDir | Out-Null } 10 | $baseDir = (Resolve-Path $baseDir).Path # Normalize it 11 | & (& $PSScriptRoot\Get-NuGetTool.ps1) install procdump -version $version -PackageSaveMode nuspec -OutputDirectory $baseDir -Source https://api.nuget.org/v3/index.json | Out-Null 12 | } 13 | 14 | (Resolve-Path $procDumpToolPath).Path 15 | -------------------------------------------------------------------------------- /tools/Get-SymbolFiles.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Collect the list of PDBs built in this repo. 4 | .PARAMETER Path 5 | The directory to recursively search for PDBs. 6 | .PARAMETER Tests 7 | A switch indicating to find PDBs only for test binaries instead of only for shipping shipping binaries. 8 | #> 9 | [CmdletBinding()] 10 | param ( 11 | [parameter(Mandatory=$true)] 12 | [string]$Path, 13 | [switch]$Tests 14 | ) 15 | 16 | $ActivityName = "Collecting symbols from $Path" 17 | Write-Progress -Activity $ActivityName -CurrentOperation "Discovery PDB files" 18 | $PDBs = Get-ChildItem -rec "$Path/*.pdb" 19 | 20 | # Filter PDBs to product OR test related. 21 | $testregex = "unittest|tests|\.test\." 22 | 23 | Write-Progress -Activity $ActivityName -CurrentOperation "De-duplicating symbols" 24 | $PDBsByHash = @{} 25 | $i = 0 26 | $PDBs |% { 27 | Write-Progress -Activity $ActivityName -CurrentOperation "De-duplicating symbols" -PercentComplete (100 * $i / $PDBs.Length) 28 | $hash = Get-FileHash $_ 29 | $i++ 30 | Add-Member -InputObject $_ -MemberType NoteProperty -Name Hash -Value $hash.Hash 31 | Write-Output $_ 32 | } | Sort-Object CreationTime |% { 33 | # De-dupe based on hash. Prefer the first match so we take the first built copy. 34 | if (-not $PDBsByHash.ContainsKey($_.Hash)) { 35 | $PDBsByHash.Add($_.Hash, $_.FullName) 36 | Write-Output $_ 37 | } 38 | } |? { 39 | if ($Tests) { 40 | $_.FullName -match $testregex 41 | } else { 42 | $_.FullName -notmatch $testregex 43 | } 44 | } |% { 45 | # Collect the DLLs/EXEs as well. 46 | $rootName = "$($_.Directory)/$($_.BaseName)" 47 | if ($rootName.EndsWith('.ni')) { 48 | $rootName = $rootName.Substring(0, $rootName.Length - 3) 49 | } 50 | 51 | $dllPath = "$rootName.dll" 52 | $exePath = "$rootName.exe" 53 | if (Test-Path $dllPath) { 54 | $BinaryImagePath = $dllPath 55 | } elseif (Test-Path $exePath) { 56 | $BinaryImagePath = $exePath 57 | } else { 58 | Write-Warning "`"$_`" found with no matching binary file." 59 | $BinaryImagePath = $null 60 | } 61 | 62 | if ($BinaryImagePath) { 63 | Write-Output $BinaryImagePath 64 | Write-Output $_.FullName 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tools/Get-TempToolsPath.ps1: -------------------------------------------------------------------------------- 1 | if ($env:AGENT_TEMPDIRECTORY) { 2 | $path = "$env:AGENT_TEMPDIRECTORY\$env:BUILD_BUILDID" 3 | } elseif ($env:localappdata) { 4 | $path = "$env:localappdata\gitrepos\tools" 5 | } else { 6 | $path = "$PSScriptRoot\..\obj\tools" 7 | } 8 | 9 | if (!(Test-Path $path)) { 10 | New-Item -ItemType Directory -Path $Path | Out-Null 11 | } 12 | 13 | (Resolve-Path $path).Path 14 | -------------------------------------------------------------------------------- /tools/Install-NuGetCredProvider.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | <# 4 | .SYNOPSIS 5 | Downloads and installs the Microsoft Artifacts Credential Provider 6 | from https://github.com/microsoft/artifacts-credprovider 7 | to assist in authenticating to Azure Artifact feeds in interactive development 8 | or unattended build agents. 9 | .PARAMETER Force 10 | Forces install of the CredProvider plugin even if one already exists. This is useful to upgrade an older version. 11 | .PARAMETER AccessToken 12 | An optional access token for authenticating to Azure Artifacts authenticated feeds. 13 | #> 14 | [CmdletBinding()] 15 | Param ( 16 | [Parameter()] 17 | [switch]$Force, 18 | [Parameter()] 19 | [string]$AccessToken 20 | ) 21 | 22 | $envVars = @{} 23 | 24 | $toolsPath = & "$PSScriptRoot\Get-TempToolsPath.ps1" 25 | 26 | if ($IsMacOS -or $IsLinux) { 27 | $installerScript = "installcredprovider.sh" 28 | $sourceUrl = "https://raw.githubusercontent.com/microsoft/artifacts-credprovider/master/helpers/installcredprovider.sh" 29 | } else { 30 | $installerScript = "installcredprovider.ps1" 31 | $sourceUrl = "https://raw.githubusercontent.com/microsoft/artifacts-credprovider/master/helpers/installcredprovider.ps1" 32 | } 33 | 34 | $installerScript = Join-Path $toolsPath $installerScript 35 | 36 | if (!(Test-Path $installerScript) -or $Force) { 37 | Invoke-WebRequest $sourceUrl -OutFile $installerScript 38 | } 39 | 40 | $installerScript = (Resolve-Path $installerScript).Path 41 | 42 | if ($IsMacOS -or $IsLinux) { 43 | chmod u+x $installerScript 44 | } 45 | 46 | & $installerScript -Force:$Force -AddNetfx -InstallNet8 47 | 48 | if ($AccessToken) { 49 | $endpoints = @() 50 | 51 | $endpointURIs = @() 52 | Get-ChildItem "$PSScriptRoot\..\nuget.config" -Recurse |% { 53 | $nugetConfig = [xml](Get-Content -LiteralPath $_) 54 | 55 | $nugetConfig.configuration.packageSources.add |? { ($_.value -match '^https://pkgs\.dev\.azure\.com/') -or ($_.value -match '^https://[\w\-]+\.pkgs\.visualstudio\.com/') } |% { 56 | if ($endpointURIs -notcontains $_.Value) { 57 | $endpointURIs += $_.Value 58 | $endpoint = New-Object -TypeName PSObject 59 | Add-Member -InputObject $endpoint -MemberType NoteProperty -Name endpoint -Value $_.value 60 | Add-Member -InputObject $endpoint -MemberType NoteProperty -Name username -Value ado 61 | Add-Member -InputObject $endpoint -MemberType NoteProperty -Name password -Value $AccessToken 62 | $endpoints += $endpoint 63 | } 64 | } 65 | } 66 | 67 | $auth = New-Object -TypeName PSObject 68 | Add-Member -InputObject $auth -MemberType NoteProperty -Name endpointCredentials -Value $endpoints 69 | 70 | $authJson = ConvertTo-Json -InputObject $auth 71 | $envVars += @{ 72 | 'VSS_NUGET_EXTERNAL_FEED_ENDPOINTS'=$authJson; 73 | } 74 | } 75 | 76 | & "$PSScriptRoot/Set-EnvVars.ps1" -Variables $envVars | Out-Null 77 | -------------------------------------------------------------------------------- /tools/MergeFrom-Template.ps1: -------------------------------------------------------------------------------- 1 | 2 | <# 3 | .SYNOPSIS 4 | Merges the latest changes from Library.Template into HEAD of this repo. 5 | .PARAMETER LocalBranch 6 | The name of the local branch to create at HEAD and use to merge into from Library.Template. 7 | #> 8 | [CmdletBinding(SupportsShouldProcess = $true)] 9 | Param( 10 | [string]$LocalBranch = "dev/$($env:USERNAME)/libtemplateUpdate" 11 | ) 12 | 13 | Function Spawn-Tool($command, $commandArgs, $workingDirectory, $allowFailures) { 14 | if ($workingDirectory) { 15 | Push-Location $workingDirectory 16 | } 17 | try { 18 | if ($env:TF_BUILD) { 19 | Write-Host "$pwd >" 20 | Write-Host "##[command]$command $commandArgs" 21 | } 22 | else { 23 | Write-Host "$command $commandArgs" -ForegroundColor Yellow 24 | } 25 | if ($commandArgs) { 26 | & $command @commandArgs 27 | } else { 28 | Invoke-Expression $command 29 | } 30 | if ((!$allowFailures) -and ($LASTEXITCODE -ne 0)) { exit $LASTEXITCODE } 31 | } 32 | finally { 33 | if ($workingDirectory) { 34 | Pop-Location 35 | } 36 | } 37 | } 38 | 39 | $remoteBranch = & $PSScriptRoot\Get-LibTemplateBasis.ps1 -ErrorIfNotRelated 40 | if ($LASTEXITCODE -ne 0) { 41 | exit $LASTEXITCODE 42 | } 43 | 44 | $LibTemplateUrl = 'https://github.com/aarnott/Library.Template' 45 | Spawn-Tool 'git' ('fetch', $LibTemplateUrl, $remoteBranch) 46 | $SourceCommit = Spawn-Tool 'git' ('rev-parse', 'FETCH_HEAD') 47 | $BaseBranch = Spawn-Tool 'git' ('branch', '--show-current') 48 | $SourceCommitUrl = "$LibTemplateUrl/commit/$SourceCommit" 49 | 50 | # To reduce the odds of merge conflicts at this stage, we always move HEAD to the last successful merge. 51 | $basis = Spawn-Tool 'git' ('rev-parse', 'HEAD') # TODO: consider improving this later 52 | 53 | Write-Host "Merging the $remoteBranch branch of Library.Template ($SourceCommit) into local repo $basis" -ForegroundColor Green 54 | 55 | Spawn-Tool 'git' ('checkout', '-b', $LocalBranch, $basis) $null $true 56 | if ($LASTEXITCODE -eq 128) { 57 | Spawn-Tool 'git' ('checkout', $LocalBranch) 58 | Spawn-Tool 'git' ('merge', $basis) 59 | } 60 | 61 | Spawn-Tool 'git' ('merge', 'FETCH_HEAD', '--no-ff', '-m', "Merge the $remoteBranch branch from $LibTemplateUrl`n`nSpecifically, this merges [$SourceCommit from that repo]($SourceCommitUrl).") 62 | if ($LASTEXITCODE -eq 1) { 63 | Write-Error "Merge conflict detected. Manual resolution required." 64 | exit 1 65 | } 66 | elseif ($LASTEXITCODE -ne 0) { 67 | Write-Error "Merge failed with exit code $LASTEXITCODE." 68 | exit $LASTEXITCODE 69 | } 70 | 71 | $result = New-Object PSObject -Property @{ 72 | BaseBranch = $BaseBranch # The original branch that was checked out when the script ran. 73 | LocalBranch = $LocalBranch # The name of the local branch that was created before the merge. 74 | SourceCommit = $SourceCommit # The commit from Library.Template that was merged in. 75 | SourceBranch = $remoteBranch # The branch from Library.Template that was merged in. 76 | } 77 | 78 | Write-Host $result 79 | Write-Output $result 80 | -------------------------------------------------------------------------------- /tools/Set-EnvVars.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Set environment variables in the environment. 4 | Azure Pipeline and CMD environments are considered. 5 | .PARAMETER Variables 6 | A hashtable of variables to be set. 7 | .PARAMETER PrependPath 8 | A set of paths to prepend to the PATH environment variable. 9 | .OUTPUTS 10 | A boolean indicating whether the environment variables can be expected to propagate to the caller's environment. 11 | .DESCRIPTION 12 | The CmdEnvScriptPath environment variable may be optionally set to a path to a cmd shell script to be created (or appended to if it already exists) that will set the environment variables in cmd.exe that are set within the PowerShell environment. 13 | This is used by init.cmd in order to reapply any new environment variables to the parent cmd.exe process that were set in the powershell child process. 14 | #> 15 | [CmdletBinding(SupportsShouldProcess=$true)] 16 | Param( 17 | [Parameter(Mandatory=$true, Position=1)] 18 | $Variables, 19 | [string[]]$PrependPath 20 | ) 21 | 22 | if ($Variables.Count -eq 0) { 23 | return $true 24 | } 25 | 26 | $cmdInstructions = !$env:TF_BUILD -and !$env:GITHUB_ACTIONS -and !$env:CmdEnvScriptPath -and ($env:PS1UnderCmd -eq '1') 27 | if ($cmdInstructions) { 28 | Write-Warning "Environment variables have been set that will be lost because you're running under cmd.exe" 29 | Write-Host "Environment variables that must be set manually:" -ForegroundColor Blue 30 | } else { 31 | Write-Host "Environment variables set:" -ForegroundColor Blue 32 | Write-Host ($Variables | Out-String) 33 | if ($PrependPath) { 34 | Write-Host "Paths prepended to PATH: $PrependPath" 35 | } 36 | } 37 | 38 | if ($env:TF_BUILD) { 39 | Write-Host "Azure Pipelines detected. Logging commands will be used to propagate environment variables and prepend path." 40 | } 41 | 42 | if ($env:GITHUB_ACTIONS) { 43 | Write-Host "GitHub Actions detected. Logging commands will be used to propagate environment variables and prepend path." 44 | } 45 | 46 | $CmdEnvScript = '' 47 | $Variables.GetEnumerator() |% { 48 | Set-Item -LiteralPath env:$($_.Key) -Value $_.Value 49 | 50 | # If we're running in a cloud CI, set these environment variables so they propagate. 51 | if ($env:TF_BUILD) { 52 | Write-Host "##vso[task.setvariable variable=$($_.Key);]$($_.Value)" 53 | } 54 | if ($env:GITHUB_ACTIONS) { 55 | Add-Content -LiteralPath $env:GITHUB_ENV -Value "$($_.Key)=$($_.Value)" 56 | } 57 | 58 | if ($cmdInstructions) { 59 | Write-Host "SET $($_.Key)=$($_.Value)" 60 | } 61 | 62 | $CmdEnvScript += "SET $($_.Key)=$($_.Value)`r`n" 63 | } 64 | 65 | $pathDelimiter = ';' 66 | if ($IsMacOS -or $IsLinux) { 67 | $pathDelimiter = ':' 68 | } 69 | 70 | if ($PrependPath) { 71 | $PrependPath |% { 72 | $newPathValue = "$_$pathDelimiter$env:PATH" 73 | Set-Item -LiteralPath env:PATH -Value $newPathValue 74 | if ($cmdInstructions) { 75 | Write-Host "SET PATH=$newPathValue" 76 | } 77 | 78 | if ($env:TF_BUILD) { 79 | Write-Host "##vso[task.prependpath]$_" 80 | } 81 | if ($env:GITHUB_ACTIONS) { 82 | Add-Content -LiteralPath $env:GITHUB_PATH -Value $_ 83 | } 84 | 85 | $CmdEnvScript += "SET PATH=$_$pathDelimiter%PATH%" 86 | } 87 | } 88 | 89 | if ($env:CmdEnvScriptPath) { 90 | if (Test-Path $env:CmdEnvScriptPath) { 91 | $CmdEnvScript = (Get-Content -LiteralPath $env:CmdEnvScriptPath) + $CmdEnvScript 92 | } 93 | 94 | Set-Content -LiteralPath $env:CmdEnvScriptPath -Value $CmdEnvScript 95 | } 96 | 97 | return !$cmdInstructions 98 | -------------------------------------------------------------------------------- /tools/artifacts/Variables.ps1: -------------------------------------------------------------------------------- 1 | # This artifact captures all variables defined in the ..\variables folder. 2 | # It "snaps" the values of these variables where we can compute them during the build, 3 | # and otherwise captures the scripts to run later during an Azure Pipelines environment release. 4 | 5 | $RepoRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot/../..") 6 | $ArtifactBasePath = "$RepoRoot/obj/_artifacts" 7 | $VariablesArtifactPath = Join-Path $ArtifactBasePath variables 8 | if (-not (Test-Path $VariablesArtifactPath)) { New-Item -ItemType Directory -Path $VariablesArtifactPath | Out-Null } 9 | 10 | # Copy variables, either by value if the value is calculable now, or by script 11 | Get-ChildItem "$PSScriptRoot/../variables" |% { 12 | $value = $null 13 | if (-not $_.BaseName.StartsWith('_')) { # Skip trying to interpret special scripts 14 | # First check the environment variables in case the variable was set in a queued build 15 | # Always use all caps for env var access because Azure Pipelines converts variables to upper-case for env vars, 16 | # and on non-Windows env vars are case sensitive. 17 | $envVarName = $_.BaseName.ToUpper() 18 | if (Test-Path env:$envVarName) { 19 | $value = Get-Content "env:$envVarName" 20 | } 21 | 22 | # If that didn't give us anything, try executing the script right now from its original position 23 | if (-not $value) { 24 | $value = & $_.FullName 25 | } 26 | 27 | if ($value) { 28 | # We got something, so wrap it with quotes so it's treated like a literal value. 29 | $value = "'$value'" 30 | } 31 | } 32 | 33 | # If that didn't get us anything, just copy the script itself 34 | if (-not $value) { 35 | $value = Get-Content -LiteralPath $_.FullName 36 | } 37 | 38 | Set-Content -LiteralPath "$VariablesArtifactPath/$($_.Name)" -Value $value 39 | } 40 | 41 | @{ 42 | "$VariablesArtifactPath" = (Get-ChildItem $VariablesArtifactPath -Recurse); 43 | } 44 | -------------------------------------------------------------------------------- /tools/artifacts/_all.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | <# 4 | .SYNOPSIS 5 | This script returns all the artifacts that should be collected after a build. 6 | Each powershell artifact is expressed as an object with these properties: 7 | Source - the full path to the source file 8 | ArtifactName - the name of the artifact to upload to 9 | ContainerFolder - the relative path within the artifact in which the file should appear 10 | Each artifact aggregating .ps1 script should return a hashtable: 11 | Key = path to the directory from which relative paths within the artifact should be calculated 12 | Value = an array of paths (absolute or relative to the BaseDirectory) to files to include in the artifact. 13 | FileInfo objects are also allowed. 14 | .PARAMETER Force 15 | Executes artifact scripts even if they have already been staged. 16 | #> 17 | 18 | [CmdletBinding(SupportsShouldProcess = $true)] 19 | param ( 20 | [string]$ArtifactNameSuffix, 21 | [switch]$Force 22 | ) 23 | 24 | Function EnsureTrailingSlash($path) { 25 | if ($path.length -gt 0 -and !$path.EndsWith('\') -and !$path.EndsWith('/')) { 26 | $path = $path + [IO.Path]::DirectorySeparatorChar 27 | } 28 | 29 | $path.Replace('\', [IO.Path]::DirectorySeparatorChar) 30 | } 31 | 32 | Function Test-ArtifactStaged($artifactName) { 33 | $varName = "ARTIFACTSTAGED_$($artifactName.ToUpper())" 34 | Test-Path "env:$varName" 35 | } 36 | 37 | Get-ChildItem "$PSScriptRoot\*.ps1" -Exclude "_*" -Recurse | % { 38 | $ArtifactName = $_.BaseName 39 | if ($Force -or !(Test-ArtifactStaged($ArtifactName + $ArtifactNameSuffix))) { 40 | $totalFileCount = 0 41 | Write-Verbose "Collecting file list for artifact $($_.BaseName)" 42 | $fileGroups = & $_ 43 | if ($fileGroups) { 44 | $fileGroups.GetEnumerator() | % { 45 | $BaseDirectory = New-Object Uri ((EnsureTrailingSlash $_.Key.ToString()), [UriKind]::Absolute) 46 | $_.Value | ? { $_ } | % { 47 | if ($_.GetType() -eq [IO.FileInfo] -or $_.GetType() -eq [IO.DirectoryInfo]) { 48 | $_ = $_.FullName 49 | } 50 | 51 | $artifact = New-Object -TypeName PSObject 52 | Add-Member -InputObject $artifact -MemberType NoteProperty -Name ArtifactName -Value $ArtifactName 53 | 54 | $SourceFullPath = New-Object Uri ($BaseDirectory, $_) 55 | Add-Member -InputObject $artifact -MemberType NoteProperty -Name Source -Value $SourceFullPath.LocalPath 56 | 57 | $RelativePath = [Uri]::UnescapeDataString($BaseDirectory.MakeRelative($SourceFullPath)) 58 | Add-Member -InputObject $artifact -MemberType NoteProperty -Name ContainerFolder -Value (Split-Path $RelativePath) 59 | 60 | Write-Output $artifact 61 | $totalFileCount += 1 62 | } 63 | } 64 | } 65 | 66 | if ($totalFileCount -eq 0) { 67 | Write-Warning "No files found for the `"$ArtifactName`" artifact." 68 | } 69 | } else { 70 | Write-Host "Skipping $ArtifactName because it has already been staged." -ForegroundColor DarkGray 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tools/artifacts/_stage_all.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script links all the artifacts described by _all.ps1 4 | into a staging directory, reading for uploading to a cloud build artifact store. 5 | It returns a sequence of objects with Name and Path properties. 6 | #> 7 | 8 | [CmdletBinding()] 9 | param ( 10 | [string]$ArtifactNameSuffix, 11 | [switch]$AvoidSymbolicLinks 12 | ) 13 | 14 | $ArtifactStagingFolder = & "$PSScriptRoot/../../tools/Get-ArtifactsStagingDirectory.ps1" -CleanIfLocal 15 | 16 | function Create-SymbolicLink { 17 | param ( 18 | $Link, 19 | $Target 20 | ) 21 | 22 | if ($Link -eq $Target) { 23 | return 24 | } 25 | 26 | if (Test-Path $Link) { Remove-Item $Link } 27 | $LinkContainer = Split-Path $Link -Parent 28 | if (!(Test-Path $LinkContainer)) { mkdir $LinkContainer } 29 | if ($IsMacOS -or $IsLinux) { 30 | ln $Target $Link | Out-Null 31 | } else { 32 | cmd /c "mklink `"$Link`" `"$Target`"" | Out-Null 33 | } 34 | 35 | if ($LASTEXITCODE -ne 0) { 36 | # Windows requires admin privileges to create symbolic links 37 | # unless Developer Mode has been enabled. 38 | throw "Failed to create symbolic link at $Link that points to $Target" 39 | } 40 | } 41 | 42 | # Stage all artifacts 43 | $Artifacts = & "$PSScriptRoot\_all.ps1" -ArtifactNameSuffix $ArtifactNameSuffix 44 | $Artifacts |% { 45 | $DestinationFolder = [System.IO.Path]::GetFullPath("$ArtifactStagingFolder/$($_.ArtifactName)$ArtifactNameSuffix/$($_.ContainerFolder)").TrimEnd('\') 46 | $Name = "$(Split-Path $_.Source -Leaf)" 47 | 48 | #Write-Host "$($_.Source) -> $($_.ArtifactName)\$($_.ContainerFolder)" -ForegroundColor Yellow 49 | 50 | if (-not (Test-Path $DestinationFolder)) { New-Item -ItemType Directory -Path $DestinationFolder | Out-Null } 51 | if (Test-Path -PathType Leaf $_.Source) { # skip folders 52 | $TargetPath = Join-Path $DestinationFolder $Name 53 | if ($AvoidSymbolicLinks) { 54 | Copy-Item -LiteralPath $_.Source -Destination $TargetPath 55 | } else { 56 | Create-SymbolicLink -Link $TargetPath -Target $_.Source 57 | } 58 | } 59 | } 60 | 61 | $ArtifactNames = $Artifacts |% { "$($_.ArtifactName)$ArtifactNameSuffix" } 62 | $ArtifactNames += Get-ChildItem env:ARTIFACTSTAGED_* |% { 63 | # Return from ALLCAPS to the actual capitalization used for the artifact. 64 | $artifactNameAllCaps = "$($_.Name.Substring('ARTIFACTSTAGED_'.Length))" 65 | (Get-ChildItem $ArtifactStagingFolder\$artifactNameAllCaps* -Filter $artifactNameAllCaps).Name 66 | } 67 | $ArtifactNames | Get-Unique |% { 68 | $artifact = New-Object -TypeName PSObject 69 | Add-Member -InputObject $artifact -MemberType NoteProperty -Name Name -Value $_ 70 | Add-Member -InputObject $artifact -MemberType NoteProperty -Name Path -Value (Join-Path $ArtifactStagingFolder $_) 71 | Write-Output $artifact 72 | } 73 | -------------------------------------------------------------------------------- /tools/artifacts/build_logs.ps1: -------------------------------------------------------------------------------- 1 | $ArtifactStagingFolder = & "$PSScriptRoot/../../tools/Get-ArtifactsStagingDirectory.ps1" 2 | 3 | if (!(Test-Path $ArtifactStagingFolder/build_logs)) { return } 4 | 5 | @{ 6 | "$ArtifactStagingFolder/build_logs" = (Get-ChildItem -Recurse "$ArtifactStagingFolder/build_logs") 7 | } 8 | -------------------------------------------------------------------------------- /tools/artifacts/coverageResults.ps1: -------------------------------------------------------------------------------- 1 | $RepoRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..") 2 | 3 | $coverageFiles = @(Get-ChildItem "$RepoRoot/test/*.cobertura.xml" -Recurse | Where {$_.FullName -notlike "*/In/*" -and $_.FullName -notlike "*\In\*" }) 4 | 5 | # Prepare code coverage reports for merging on another machine 6 | $repoRoot = $env:SYSTEM_DEFAULTWORKINGDIRECTORY 7 | if (!$repoRoot) { $repoRoot = $env:GITHUB_WORKSPACE } 8 | if ($repoRoot) { 9 | Write-Host "Substituting $repoRoot with `"{reporoot}`"" 10 | $coverageFiles |% { 11 | $content = Get-Content -LiteralPath $_ |% { $_ -Replace [regex]::Escape($repoRoot), "{reporoot}" } 12 | Set-Content -LiteralPath $_ -Value $content -Encoding UTF8 13 | } 14 | } else { 15 | Write-Warning "coverageResults: Cloud build not detected. Machine-neutral token replacement skipped." 16 | } 17 | 18 | if (!((Test-Path $RepoRoot\bin) -and (Test-Path $RepoRoot\obj))) { return } 19 | 20 | @{ 21 | $RepoRoot = ( 22 | $coverageFiles + 23 | (Get-ChildItem "$RepoRoot\obj\*.cs" -Recurse) 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /tools/artifacts/deployables.ps1: -------------------------------------------------------------------------------- 1 | $RepoRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..") 2 | $BuildConfiguration = $env:BUILDCONFIGURATION 3 | if (!$BuildConfiguration) { 4 | $BuildConfiguration = 'Debug' 5 | } 6 | 7 | $PackagesRoot = "$RepoRoot/bin/Packages/$BuildConfiguration" 8 | 9 | if (!(Test-Path $PackagesRoot)) { return } 10 | 11 | @{ 12 | "$PackagesRoot" = (Get-ChildItem $PackagesRoot -Recurse) 13 | } 14 | -------------------------------------------------------------------------------- /tools/artifacts/projectAssetsJson.ps1: -------------------------------------------------------------------------------- 1 | $ObjRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..\obj") 2 | 3 | if (!(Test-Path $ObjRoot)) { return } 4 | 5 | @{ 6 | "$ObjRoot" = ( 7 | (Get-ChildItem "$ObjRoot\project.assets.json" -Recurse) 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /tools/artifacts/symbols.ps1: -------------------------------------------------------------------------------- 1 | $BinPath = [System.IO.Path]::GetFullPath("$PSScriptRoot/../../bin") 2 | if (!(Test-Path $BinPath)) { return } 3 | $symbolfiles = & "$PSScriptRoot/../Get-SymbolFiles.ps1" -Path $BinPath | Get-Unique 4 | 5 | @{ 6 | "$BinPath" = $SymbolFiles; 7 | } 8 | -------------------------------------------------------------------------------- /tools/artifacts/testResults.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | Param( 3 | ) 4 | 5 | $result = @{} 6 | 7 | $testRoot = Resolve-Path "$PSScriptRoot\..\..\test" 8 | $result[$testRoot] = (Get-ChildItem "$testRoot\TestResults" -Recurse -Directory | Get-ChildItem -Recurse -File) 9 | 10 | $testlogsPath = "$env:BUILD_ARTIFACTSTAGINGDIRECTORY\test_logs" 11 | if (Test-Path $testlogsPath) { 12 | $result[$testlogsPath] = Get-ChildItem "$testlogsPath\*"; 13 | } 14 | 15 | $result 16 | -------------------------------------------------------------------------------- /tools/artifacts/test_symbols.ps1: -------------------------------------------------------------------------------- 1 | $BinPath = [System.IO.Path]::GetFullPath("$PSScriptRoot/../../bin") 2 | if (!(Test-Path $BinPath)) { return } 3 | $symbolfiles = & "$PSScriptRoot/../Get-SymbolFiles.ps1" -Path $BinPath -Tests | Get-Unique 4 | 5 | @{ 6 | "$BinPath" = $SymbolFiles; 7 | } 8 | -------------------------------------------------------------------------------- /tools/dotnet-test-cloud.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | <# 4 | .SYNOPSIS 5 | Runs tests as they are run in cloud test runs. 6 | .PARAMETER Configuration 7 | The configuration within which to run tests 8 | .PARAMETER Agent 9 | The name of the agent. This is used in preparing test run titles. 10 | .PARAMETER PublishResults 11 | A switch to publish results to Azure Pipelines. 12 | .PARAMETER x86 13 | A switch to run the tests in an x86 process. 14 | .PARAMETER dotnet32 15 | The path to a 32-bit dotnet executable to use. 16 | #> 17 | [CmdletBinding()] 18 | Param( 19 | [string]$Configuration='Debug', 20 | [string]$Agent='Local', 21 | [switch]$PublishResults, 22 | [switch]$x86, 23 | [string]$dotnet32 24 | ) 25 | 26 | $RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path 27 | $ArtifactStagingFolder = & "$PSScriptRoot/Get-ArtifactsStagingDirectory.ps1" 28 | 29 | $dotnet = 'dotnet' 30 | if ($x86) { 31 | $x86RunTitleSuffix = ", x86" 32 | if ($dotnet32) { 33 | $dotnet = $dotnet32 34 | } else { 35 | $dotnet32Possibilities = "$PSScriptRoot\../obj/tools/x86/.dotnet/dotnet.exe", "$env:AGENT_TOOLSDIRECTORY/x86/dotnet/dotnet.exe", "${env:ProgramFiles(x86)}\dotnet\dotnet.exe" 36 | $dotnet32Matches = $dotnet32Possibilities |? { Test-Path $_ } 37 | if ($dotnet32Matches) { 38 | $dotnet = Resolve-Path @($dotnet32Matches)[0] 39 | Write-Host "Running tests using `"$dotnet`"" -ForegroundColor DarkGray 40 | } else { 41 | Write-Error "Unable to find 32-bit dotnet.exe" 42 | return 1 43 | } 44 | } 45 | } 46 | 47 | & $dotnet test $RepoRoot ` 48 | --no-build ` 49 | -c $Configuration ` 50 | --filter "TestCategory!=FailsInCloudTest" ` 51 | --collect "Code Coverage;Format=cobertura" ` 52 | --settings "$PSScriptRoot/test.runsettings" ` 53 | --blame-hang-timeout 60s ` 54 | --blame-crash ` 55 | -bl:"$ArtifactStagingFolder/build_logs/test.binlog" ` 56 | --diag "$ArtifactStagingFolder/test_logs/diag.log;TraceLevel=info" ` 57 | --logger trx ` 58 | 59 | $unknownCounter = 0 60 | Get-ChildItem -Recurse -Path $RepoRoot\test\*.trx |% { 61 | Copy-Item $_ -Destination $ArtifactStagingFolder/test_logs/ 62 | 63 | if ($PublishResults) { 64 | $x = [xml](Get-Content -LiteralPath $_) 65 | $runTitle = $null 66 | if ($x.TestRun.TestDefinitions -and $x.TestRun.TestDefinitions.GetElementsByTagName('UnitTest')) { 67 | $storage = $x.TestRun.TestDefinitions.GetElementsByTagName('UnitTest')[0].storage -replace '\\','/' 68 | if ($storage -match '/(?net[^/]+)/(?:(?[^/]+)/)?(?[^/]+)\.dll$') { 69 | if ($matches.rid) { 70 | $runTitle = "$($matches.lib) ($($matches.tfm), $($matches.rid), $Agent)" 71 | } else { 72 | $runTitle = "$($matches.lib) ($($matches.tfm)$x86RunTitleSuffix, $Agent)" 73 | } 74 | } 75 | } 76 | if (!$runTitle) { 77 | $unknownCounter += 1; 78 | $runTitle = "unknown$unknownCounter ($Agent$x86RunTitleSuffix)"; 79 | } 80 | 81 | Write-Host "##vso[results.publish type=VSTest;runTitle=$runTitle;publishRunAttachments=true;resultFiles=$_;failTaskOnFailedTests=true;testRunSystem=VSTS - PTR;]" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tools/publish-CodeCov.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Uploads code coverage to codecov.io 4 | .PARAMETER CodeCovToken 5 | Code coverage token to use 6 | .PARAMETER PathToCodeCoverage 7 | Path to root of code coverage files 8 | .PARAMETER Name 9 | Name to upload with codecoverge 10 | .PARAMETER Flags 11 | Flags to upload with codecoverge 12 | #> 13 | [CmdletBinding()] 14 | Param ( 15 | [Parameter(Mandatory=$true)] 16 | [string]$CodeCovToken, 17 | [Parameter(Mandatory=$true)] 18 | [string]$PathToCodeCoverage, 19 | [string]$Name, 20 | [string]$Flags 21 | ) 22 | 23 | $RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path 24 | 25 | Get-ChildItem -Recurse -LiteralPath $PathToCodeCoverage -Filter "*.cobertura.xml" | % { 26 | $relativeFilePath = Resolve-Path -relative $_.FullName 27 | 28 | Write-Host "Uploading: $relativeFilePath" -ForegroundColor Yellow 29 | & (& "$PSScriptRoot/Get-CodeCovTool.ps1") -t $CodeCovToken -f $relativeFilePath -R $RepoRoot -F $Flags -n $Name 30 | } 31 | -------------------------------------------------------------------------------- /tools/test.runsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | \.dll$ 10 | \.exe$ 11 | 12 | 13 | xunit\..* 14 | 15 | 16 | 17 | 18 | ^System\.Diagnostics\.DebuggerHiddenAttribute$ 19 | ^System\.Diagnostics\.DebuggerNonUserCodeAttribute$ 20 | ^System\.CodeDom\.Compiler\.GeneratedCodeAttribute$ 21 | ^System\.Diagnostics\.CodeAnalysis\.ExcludeFromCodeCoverageAttribute$ 22 | 23 | 24 | 25 | 26 | True 27 | 28 | True 29 | 30 | True 31 | 32 | False 33 | 34 | False 35 | 36 | False 37 | 38 | True 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tools/variables/DotNetSdkVersion.ps1: -------------------------------------------------------------------------------- 1 | $globalJson = Get-Content -LiteralPath "$PSScriptRoot\..\..\global.json" | ConvertFrom-Json 2 | $globalJson.sdk.version 3 | -------------------------------------------------------------------------------- /tools/variables/_all.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | <# 4 | .SYNOPSIS 5 | This script returns a hashtable of build variables that should be set 6 | at the start of a build or release definition's execution. 7 | #> 8 | 9 | [CmdletBinding(SupportsShouldProcess = $true)] 10 | param ( 11 | ) 12 | 13 | $vars = @{} 14 | 15 | Get-ChildItem "$PSScriptRoot\*.ps1" -Exclude "_*" |% { 16 | Write-Host "Computing $($_.BaseName) variable" 17 | $vars[$_.BaseName] = & $_ 18 | } 19 | 20 | $vars 21 | -------------------------------------------------------------------------------- /tools/variables/_define.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script translates the variables returned by the _all.ps1 script 4 | into commands that instruct Azure Pipelines to actually set those variables for other pipeline tasks to consume. 5 | 6 | The build or release definition may have set these variables to override 7 | what the build would do. So only set them if they have not already been set. 8 | #> 9 | 10 | [CmdletBinding()] 11 | param ( 12 | ) 13 | 14 | (& "$PSScriptRoot\_all.ps1").GetEnumerator() |% { 15 | # Always use ALL CAPS for env var names since Azure Pipelines converts variable names to all caps and on non-Windows OS, env vars are case sensitive. 16 | $keyCaps = $_.Key.ToUpper() 17 | if ((Test-Path "env:$keyCaps") -and (Get-Content "env:$keyCaps")) { 18 | Write-Host "Skipping setting $keyCaps because variable is already set to '$(Get-Content env:$keyCaps)'." -ForegroundColor Cyan 19 | } else { 20 | Write-Host "$keyCaps=$($_.Value)" -ForegroundColor Yellow 21 | if ($env:TF_BUILD) { 22 | # Create two variables: the first that can be used by its simple name and accessible only within this job. 23 | Write-Host "##vso[task.setvariable variable=$keyCaps]$($_.Value)" 24 | # and the second that works across jobs and stages but must be fully qualified when referenced. 25 | Write-Host "##vso[task.setvariable variable=$keyCaps;isOutput=true]$($_.Value)" 26 | } elseif ($env:GITHUB_ACTIONS) { 27 | Add-Content -LiteralPath $env:GITHUB_ENV -Value "$keyCaps=$($_.Value)" 28 | } 29 | Set-Item -LiteralPath "env:$keyCaps" -Value $_.Value 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", 3 | "version": "1.0", 4 | "publicReleaseRefSpec": [ 5 | "^refs/heads/main$", 6 | "^refs/heads/v\\d+(?:\\.\\d+)?$" 7 | ], 8 | "cloudBuild": { 9 | "setVersionVariables": false 10 | } 11 | } 12 | --------------------------------------------------------------------------------