├── .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 | [](https://www.nuget.org/packages/assemblyrefscanner)
4 | [](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 | [](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 |
--------------------------------------------------------------------------------