@groma/scanner-csharp 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +229 -55
  2. package/dist/index.js +195 -47
  3. package/package.json +24 -8
  4. package/dist/worker/BuildHost-net472/Microsoft.Bcl.AsyncInterfaces.dll +0 -0
  5. package/dist/worker/BuildHost-net472/Microsoft.Build.Locator.dll +0 -0
  6. package/dist/worker/BuildHost-net472/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.exe +0 -0
  7. package/dist/worker/BuildHost-net472/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.exe.config +0 -68
  8. package/dist/worker/BuildHost-net472/Microsoft.CodeAnalysis.Workspaces.MSBuild.Contracts.dll +0 -0
  9. package/dist/worker/BuildHost-net472/Microsoft.CodeAnalysis.Workspaces.MSBuild.Contracts.pdb +0 -0
  10. package/dist/worker/BuildHost-net472/Microsoft.IO.Redist.dll +0 -0
  11. package/dist/worker/BuildHost-net472/System.Buffers.dll +0 -0
  12. package/dist/worker/BuildHost-net472/System.IO.Pipelines.dll +0 -0
  13. package/dist/worker/BuildHost-net472/System.Memory.dll +0 -0
  14. package/dist/worker/BuildHost-net472/System.Numerics.Vectors.dll +0 -0
  15. package/dist/worker/BuildHost-net472/System.Runtime.CompilerServices.Unsafe.dll +0 -0
  16. package/dist/worker/BuildHost-net472/System.Text.Encodings.Web.dll +0 -0
  17. package/dist/worker/BuildHost-net472/System.Text.Json.dll +0 -0
  18. package/dist/worker/BuildHost-net472/System.Threading.Tasks.Extensions.dll +0 -0
  19. package/dist/worker/BuildHost-netcore/Microsoft.Build.Locator.dll +0 -0
  20. package/dist/worker/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.deps.json +0 -96
  21. package/dist/worker/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll +0 -0
  22. package/dist/worker/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.runtimeconfig.json +0 -14
  23. package/dist/worker/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.Contracts.dll +0 -0
  24. package/dist/worker/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.Contracts.pdb +0 -0
  25. package/dist/worker/Groma.CSharpScanner.deps.json +0 -594
  26. package/dist/worker/Groma.CSharpScanner.dll +0 -0
  27. package/dist/worker/Groma.CSharpScanner.pdb +0 -0
  28. package/dist/worker/Groma.CSharpScanner.runtimeconfig.json +0 -13
  29. package/dist/worker/Humanizer.dll +0 -0
  30. package/dist/worker/Microsoft.Build.Locator.dll +0 -0
  31. package/dist/worker/Microsoft.CodeAnalysis.CSharp.Workspaces.dll +0 -0
  32. package/dist/worker/Microsoft.CodeAnalysis.CSharp.dll +0 -0
  33. package/dist/worker/Microsoft.CodeAnalysis.ExternalAccess.RazorCompiler.dll +0 -0
  34. package/dist/worker/Microsoft.CodeAnalysis.Workspaces.MSBuild.Contracts.dll +0 -0
  35. package/dist/worker/Microsoft.CodeAnalysis.Workspaces.MSBuild.dll +0 -0
  36. package/dist/worker/Microsoft.CodeAnalysis.Workspaces.dll +0 -0
  37. package/dist/worker/Microsoft.CodeAnalysis.dll +0 -0
  38. package/dist/worker/Microsoft.Extensions.DependencyInjection.Abstractions.dll +0 -0
  39. package/dist/worker/Microsoft.Extensions.DependencyInjection.dll +0 -0
  40. package/dist/worker/Microsoft.Extensions.Logging.Abstractions.dll +0 -0
  41. package/dist/worker/Microsoft.Extensions.Logging.dll +0 -0
  42. package/dist/worker/Microsoft.Extensions.Options.dll +0 -0
  43. package/dist/worker/Microsoft.Extensions.Primitives.dll +0 -0
  44. package/dist/worker/Microsoft.VisualStudio.SolutionPersistence.dll +0 -0
  45. package/dist/worker/System.Composition.AttributedModel.dll +0 -0
  46. package/dist/worker/System.Composition.Convention.dll +0 -0
  47. package/dist/worker/System.Composition.Hosting.dll +0 -0
  48. package/dist/worker/System.Composition.Runtime.dll +0 -0
  49. package/dist/worker/System.Composition.TypedParts.dll +0 -0
  50. package/dist/worker/cs/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
  51. package/dist/worker/cs/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  52. package/dist/worker/cs/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
  53. package/dist/worker/cs/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
  54. package/dist/worker/cs/Microsoft.CodeAnalysis.resources.dll +0 -0
  55. package/dist/worker/de/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
  56. package/dist/worker/de/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  57. package/dist/worker/de/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
  58. package/dist/worker/de/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
  59. package/dist/worker/de/Microsoft.CodeAnalysis.resources.dll +0 -0
  60. package/dist/worker/es/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
  61. package/dist/worker/es/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  62. package/dist/worker/es/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
  63. package/dist/worker/es/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
  64. package/dist/worker/es/Microsoft.CodeAnalysis.resources.dll +0 -0
  65. package/dist/worker/fr/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
  66. package/dist/worker/fr/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  67. package/dist/worker/fr/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
  68. package/dist/worker/fr/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
  69. package/dist/worker/fr/Microsoft.CodeAnalysis.resources.dll +0 -0
  70. package/dist/worker/it/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
  71. package/dist/worker/it/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  72. package/dist/worker/it/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
  73. package/dist/worker/it/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
  74. package/dist/worker/it/Microsoft.CodeAnalysis.resources.dll +0 -0
  75. package/dist/worker/ja/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
  76. package/dist/worker/ja/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  77. package/dist/worker/ja/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
  78. package/dist/worker/ja/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
  79. package/dist/worker/ja/Microsoft.CodeAnalysis.resources.dll +0 -0
  80. package/dist/worker/ko/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
  81. package/dist/worker/ko/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  82. package/dist/worker/ko/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
  83. package/dist/worker/ko/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
  84. package/dist/worker/ko/Microsoft.CodeAnalysis.resources.dll +0 -0
  85. package/dist/worker/pl/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
  86. package/dist/worker/pl/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  87. package/dist/worker/pl/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
  88. package/dist/worker/pl/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
  89. package/dist/worker/pl/Microsoft.CodeAnalysis.resources.dll +0 -0
  90. package/dist/worker/pt-BR/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
  91. package/dist/worker/pt-BR/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  92. package/dist/worker/pt-BR/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
  93. package/dist/worker/pt-BR/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
  94. package/dist/worker/pt-BR/Microsoft.CodeAnalysis.resources.dll +0 -0
  95. package/dist/worker/ru/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
  96. package/dist/worker/ru/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  97. package/dist/worker/ru/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
  98. package/dist/worker/ru/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
  99. package/dist/worker/ru/Microsoft.CodeAnalysis.resources.dll +0 -0
  100. package/dist/worker/tr/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
  101. package/dist/worker/tr/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  102. package/dist/worker/tr/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
  103. package/dist/worker/tr/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
  104. package/dist/worker/tr/Microsoft.CodeAnalysis.resources.dll +0 -0
  105. package/dist/worker/zh-Hans/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
  106. package/dist/worker/zh-Hans/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  107. package/dist/worker/zh-Hans/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
  108. package/dist/worker/zh-Hans/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
  109. package/dist/worker/zh-Hans/Microsoft.CodeAnalysis.resources.dll +0 -0
  110. package/dist/worker/zh-Hant/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
  111. package/dist/worker/zh-Hant/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
  112. package/dist/worker/zh-Hant/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
  113. package/dist/worker/zh-Hant/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
  114. package/dist/worker/zh-Hant/Microsoft.CodeAnalysis.resources.dll +0 -0
package/README.md CHANGED
@@ -1,65 +1,239 @@
1
1
  # C# scanner
2
2
 
3
- The optional `@groma/scanner-csharp` package uses a prebuilt Roslyn worker and the project's installed .NET SDK. Install the package in Groma, select the project, restore it with normal .NET tooling, then scan. Scanning never installs an SDK, builds the worker, or restores packages.
3
+ The C# scanner installs Roslyn and a self-contained .NET runtime for the host
4
+ platform. Install the scanner and scan source without a project SDK, NuGet restore, generated output
5
+ or application build. `global.json` does not select the scanner's runtime.
4
6
 
5
- ## Tooling and preparation
6
-
7
- The worker requires the .NET 10 runtime. Its Roslyn dependencies are 5.9.0 and MSBuild Locator is 1.11.2. The project separately requires the SDK and reference packs selected by its `global.json`. Both may be installed together: running the worker on .NET 10 does not select SDK 10 for the project.
8
-
9
- Use `dotnet` on PATH, or set `DOTNET_HOST_PATH` to the installed dotnet executable. The scanner uses the selected input directory for SDK lookup. [Microsoft's SDK selection documentation](https://learn.microsoft.com/en-us/dotnet/core/versions/selection) explains how `global.json` is found. An unavailable pinned SDK fails instead of using a different SDK.
10
-
11
- For a nested project, set `settings` on the existing `csharp` entry in the
12
- shared `scanners.json` inside `groma/` or `.groma/`. Preserve its installed source:
13
-
14
- ```json
15
- {
16
- "id": "csharp",
17
- "source": "./tools/csharp-scanner-package",
18
- "settings": { "input": "src/Library/Library.csproj" }
19
- }
7
+ ```sh
8
+ groma scanner add @groma/scanner-csharp
9
+ groma scan
20
10
  ```
21
11
 
22
- When `input` is omitted, the scanner selects a single root solution, or a
23
- single root project when no solution exists. Multiple candidates require an
24
- explicit `settings.input`. Paths are relative to the repository root. Run a
25
- new scan or restart the active viewer or watch session after editing settings.
26
- See the [shared configuration contract](../creating-a-plugin.md#scanner-settings).
27
-
28
- Run `dotnet restore Library.csproj` from that input directory, then run `groma scan` from the repository root. Restore uses the project's normal NuGet settings and credentials. On macOS, use a relative restore input from its directory so `/var` and `/private/var` aliases do not enter one restore graph as separate project paths.
29
-
30
- Install a prepared local package with `groma scanner add /path/to/csharp-scanner-package`. Release publication is separate from local qualification. The package also exports `checkCSharpReadiness(repositoryRoot, { input })`, used through the same input/tool checks as scanning. It returns the selected input and SDK version; successful readiness does not prove dependency restore or compilation. A missing worker means the package needs preparation; missing tooling or compilation errors must be fixed before scanning can replace architecture evidence.
31
-
32
- ## Supported input and evidence
33
-
34
- The selected qualification example is FluentValidation 12.0.0, commit `5365d9294812c8a5c5a7f4d7447c9a65b79a025b`, selecting `src/FluentValidation/FluentValidation.csproj` with its original SDK selection. Minimal fixtures also exercise in-repository project references and the standard Web SDK header.
35
-
36
- Inputs are SDK-style `.csproj`, `.sln`, and `.slnx` files. When input is omitted, one root solution takes precedence over root projects. Multiple candidates require explicit selection. Standard .NET, Web and Worker SDK project headers are accepted, but this is not general framework qualification. Mixed-language graphs, legacy project formats, multiple target-framework contexts, and a file shared by multiple loaded project contexts are rejected.
37
-
38
- Each physical C# source has one inventory record. Generated `bin` and `obj` sources are excluded. Roslyn resolves overloads, generic methods, extensions, partial implementations and direct calls. Virtual, interface and delegate dispatch retain uncertainty where the runtime provider is not established. Implicit calls, initializers, generated operations, receiver/delegate value flow, dependency injection and network protocols are not resolved.
39
-
40
- Solutions and projects are source roots, with each project linked to its solution
41
- when a solution is selected. File records identify their project root. A directly
42
- selected project and its loaded project references are top-level source roots.
43
- Root membership and operations are temporary evidence. They are not C4 components or proof of business collaborations. Core owns curated source membership and relationship selection; ordinary Markdown readers retain readable descriptions and links under the existing OKF application profile. This plugin introduces no architecture metadata or map level.
44
-
45
- `settings` also accepts `configuration` (default `Debug`), `maxProjects` (128), `maxFiles` (20000), and `timeoutSeconds` (120). Exceeded limits fail the scan; they never publish truncated evidence. Any enabled scanner failure prevents reconciliation and preserves the prior map.
46
-
47
- ## Contributor package build
48
-
49
- With .NET 10 SDK installed, restore `plugins/scanners/csharp/dotnet/Groma.CSharpScanner.csproj`, then run:
12
+ Maintainers build the package with the .NET 10 SDK and Bun:
50
13
 
51
14
  ```sh
52
15
  bun scripts/package-csharp-scanner.ts
53
16
  ```
54
17
 
55
- The build bundles the adapter and publishes a framework-dependent worker into the package. It has no installation scripts and does not include a private SDK. Run the Roslyn tests with `dotnet test plugins/scanners/csharp/dotnet/test/Groma.CSharpScanner.Tests.csproj`.
56
-
57
- ## Nested projects
58
-
59
- From the repository root, the scanner finds tracked and unignored `.sln`, `.slnx`
60
- and `.csproj` files. Solutions are loaded first. Project roots returned by Roslyn
61
- prevent a solution member or project reference from being scanned again as a
62
- standalone input. Independent projects are scanned as additional inputs. Each
63
- input selects its own installed SDK; all evidence paths stay relative to the
64
- repository. `settings.input` still selects one explicit solution or project.
65
- Any selected input failure fails the complete scan.
18
+ The public `@groma/scanner-csharp` package contains the adapter. Its exact
19
+ optional dependencies select `@groma/scanner-csharp-<platform>-<architecture>`
20
+ for the current host. Each runtime package contains the complete executable
21
+ worker, .NET runtime and upstream notices. Consumers need optional dependencies
22
+ enabled, but do not need `dotnet` or an installation script. Keeping host
23
+ runtimes separate avoids npm's upload-size rejection of the combined package.
24
+
25
+ The maintainer build installs its host runtime under the scanner's local
26
+ `node_modules` and copies it into the staged package. Both source development
27
+ and local package scans use normal module resolution. The release assembler
28
+ emits one runtime package per built host and a small adapter referencing all
29
+ of them; runtimes publish before the adapter. This changes package delivery
30
+ only: scanner evidence, OKF records and C4 boundaries are unchanged.
31
+
32
+ ## Source inputs
33
+
34
+ The scanner discovers tracked and unignored SDK-style `.csproj`, `.sln` and
35
+ `.slnx` files. Solutions are selected before their projects; local project
36
+ references are loaded once. Set `settings.input` on the existing C# scanner
37
+ entry to select one project or solution relative to the repository root.
38
+
39
+ An in-memory Roslyn workspace reads project XML and authored C# files.
40
+ Unconditional language options, compile includes/removes, implicit usings and
41
+ local project references supply context. The scanner's runtime supplies base
42
+ .NET declarations. It does not run MSBuild, evaluate imported or conditional
43
+ build settings, restore packages, or execute source generators. Standard .NET,
44
+ Web and Worker SDK headers are accepted; this is not general framework analysis.
45
+
46
+ Generated `bin` and `obj` files are excluded. A physical source file cannot
47
+ belong to several selected project contexts. Project references must stay
48
+ inside the repository. Mixed-language graphs and legacy projects are unsupported.
49
+ Invalid syntax fails a scan. Missing external types produce diagnostics while
50
+ local declarations and supported operations remain available.
51
+
52
+ ## Evidence
53
+
54
+ Partial class declarations in separate authored files are proposed as one source
55
+ unit when Roslyn identifies the same class in the same project. Each file must
56
+ contain only that type, and every declaration must qualify. Same-name classes
57
+ in other namespaces or projects are independent. Files containing additional
58
+ types remain separate source evidence. This does not associate Razor, XAML, or
59
+ generated source.
60
+
61
+ Core creates one component with all member Code references, attaches newly
62
+ discovered unowned partial files, and retains curated ownership on repeat scans.
63
+ Conflicting owners produce review diagnostics.
64
+ The existing C# source watcher includes new partial files. No project restore,
65
+ build, or source-generator execution is needed.
66
+
67
+ Roslyn resolves local overloads, generic methods, extensions, partial
68
+ implementations and direct calls. Calls with type errors, virtual dispatch,
69
+ interfaces and delegates retain uncertainty. Implicit calls, initializers,
70
+ generated operations, receiver/delegate value flow, dependency injection and
71
+ network protocols are not resolved.
72
+
73
+ Solutions and projects supply source roots and file membership. They are not
74
+ C4 components or proof of business collaborations. Core owns curated membership
75
+ and relationship selection; ordinary Markdown readers see the existing OKF
76
+ records and Code links. The scanner adds no map level or architecture metadata.
77
+
78
+ Settings also accept `configuration` (default `Debug`), `maxProjects` (128),
79
+ `maxFiles` (20000) and `timeoutSeconds` (120). Configuration selects the source
80
+ DEBUG/TRACE symbols; it does not execute a build configuration. Limits fail
81
+ rather than publish truncated evidence. Failed scanners retain their saved
82
+ evidence while other scanners can update the map.
83
+
84
+ Run `dotnet test plugins/scanners/csharp/dotnet/test/Groma.CSharpScanner.Tests.csproj`
85
+ for Roslyn tests. Fixtures are scanned without restore. The packaged test
86
+ removes language tools from PATH; see
87
+ [fresh-checkout validation](../fresh-checkout-validation.md).
88
+
89
+ ## Source outline
90
+
91
+ Components list the declarations of their C# files under the
92
+ [shared outline contract](../creating-a-plugin.md#source-outline). The worker
93
+ parses each requested file with Roslyn syntax alone, without loading its
94
+ project, restoring packages or building.
95
+
96
+ - Classes, structs, records, interfaces, enums and delegates declared directly
97
+ in the file or inside its namespaces are types. Enums and delegates have no
98
+ members.
99
+ - A type's members are the methods, interface method signatures, constructors,
100
+ finalizers and operators declared in its body, each overload separately.
101
+ Constructors are named after the type and finalizers `~Type`; operators are
102
+ named like `operator +` or `implicit operator int`.
103
+
104
+ Nested types, record and class primary constructors, fields, properties,
105
+ indexers, events, accessors and top-level statements are not listed. Each file
106
+ of a partial type lists the type with the members that file declares. A
107
+ declaration's line is its name's line; an operator's line is its `operator`
108
+ keyword's line. A member is an entry only when a Code link names it as
109
+ `Type.Member`.
110
+
111
+ The outline parses without preprocessor symbols, while a scan uses the
112
+ project's. Code inside `#if DEBUG` is therefore skipped, and `#else` branches
113
+ are listed.
114
+
115
+ Visibility follows the contract's C# row: `protected internal` and
116
+ `private protected` are `protected`, and a `file` type is `private`. Without an
117
+ access modifier, top-level types are `internal`, interface members `public`
118
+ and other members `private`. A partial declaration without a modifier reports
119
+ that default, even when another part declares its access.
120
+
121
+ ## Compared operations
122
+
123
+ `groma lint` and scan findings compare C# operations under the
124
+ [shared rule](../../architecture-findings.md#compared-operations). The scanner
125
+ attaches a source range and body tokens to every implemented:
126
+
127
+ - method, constructor, finalizer and operator;
128
+ - property, indexer and event accessor, including an expression-bodied getter;
129
+ - local function, including one declared among top-level statements.
130
+
131
+ Other lambdas and anonymous methods, such as those passed as arguments, are
132
+ anonymous callbacks. Top-level statements and field and property initializers
133
+ are not compared.
134
+
135
+ These named operations are not compared:
136
+
137
+ - lambdas and anonymous methods assigned to a variable, field or property, or
138
+ written in an object initializer.
139
+
140
+ Parameter and local names become slots. Parameters, an indexer's included, are
141
+ numbered in declaration order, so an indexer that swaps its parameters is a
142
+ different body. Declared types remain tokens, so copies that differ only in
143
+ those types are not identical.
144
+ Operators remain. Parentheses that group an expression or a pattern remain
145
+ unless they wrap a name, member access, element access, call, literal or `this`,
146
+ so `(a + b) * c` and `a + b * c` are different bodies while `(a) + b` and
147
+ `a + b` are not.
148
+
149
+ ## HTTP endpoints and requests
150
+
151
+ The scanner reports the [HTTP facts](../evidence.md#http-endpoints-and-requests)
152
+ that core joins into derived relationships. It reads attributes and calls from
153
+ syntax, because ASP.NET Core and client packages are never restored.
154
+
155
+ Endpoints:
156
+
157
+ - attribute-routed controllers: `[Route]` on the class with `[HttpGet]`,
158
+ `[HttpPost]`, `[HttpPut]`, `[HttpDelete]`, `[HttpPatch]`, `[HttpHead]`,
159
+ `[HttpOptions]` or `[Route]` on an action. As in ASP.NET Core, each `[Route]`
160
+ and each verb attribute with a template, `Name` or `Order` is one route. A
161
+ verb attribute's route serves only its own method, so
162
+ `[HttpGet("a")] [HttpPost("b")]` serves GET `a` and POST `b`. A `[Route]`
163
+ serves the methods of the verb attributes without a template, or every method
164
+ when there are none. Verb attributes without a template that no `[Route]`
165
+ took serve the class prefix, so `[HttpPost] [HttpPut("{id?}")]` serves PUT
166
+ `{id?}` and POST on the prefix. An action without routing attributes
167
+ reports nothing. `[controller]` becomes the class name without its
168
+ `Controller` suffix, and `[action]` the literal `[ActionName]` or else the
169
+ method name without a trailing `Async`. When a source file may set
170
+ `SuppressAsyncSuffixInActionNames` to anything but `true`, the `[action]`
171
+ path of a method ending in `Async` reports nothing, and when the source names
172
+ `RouteTokenTransformerConvention`, every `[controller]` or `[action]` path
173
+ reports nothing. A class `[Route]` starting with `~/` starts at the root.
174
+ - a controller is a public, top-level, non-generic, non-abstract class declared
175
+ in one place, named `*Controller`, marked `[Controller]` or deriving from
176
+ `ControllerBase` or `Controller`, and not `[NonController]`, counting the
177
+ attributes of its bases. A controller also serves the public methods and
178
+ `[Route]` prefixes of its bases, so one with a base between it and
179
+ `ControllerBase` that declares a public method or a `[Route]`, or that the
180
+ source does not declare, reports nothing. An action is a public, non-static,
181
+ non-generic method with a body and without `[NonAction]`.
182
+ - minimal APIs: `MapGet`, `MapPost`, `MapPut`, `MapDelete`, `MapPatch`, and
183
+ `MapMethods` with literal methods, on a `MapGroup` chain or on the
184
+ application: the result of `Build()` or `WebApplication.Create()`, or any
185
+ name declared as `WebApplication`, such as a parameter. The handler is a
186
+ lambda or a method the call names.
187
+
188
+ Requests:
189
+
190
+ - `HttpClient` calls, recognized by the receiver's own type: the `GetAsync`,
191
+ `GetStringAsync`, `GetFromJsonAsync`, `PostAsync`, `PostAsJsonAsync`,
192
+ `PutAsync`, `PatchAsync` and `DeleteAsync` families, and `SendAsync` with a
193
+ `new HttpRequestMessage(HttpMethod.Get, url)`.
194
+ - declarative client interfaces such as Refit: `[Get]`, `[Post]`, `[Put]`,
195
+ `[Delete]`, `[Patch]`, `[Head]` and `[Options]` on an interface method. Such
196
+ a method has no body, so its request declares the operation itself.
197
+
198
+ Literal routes and URLs and values the compiler proves constant, such as a
199
+ `const` field, become facts. A local, or a readonly field whose type is declared
200
+ entirely in the file that uses it, stands for its initializer when that file
201
+ never assigns it again. These report nothing: conventional routing; an action
202
+ with `[AcceptVerbs]`; `[area]` and other route tokens; `IApplicationBuilder.Map`
203
+ middleware branches; framework constants such as `HttpMethods.Get`; and a route
204
+ on a group that is reassigned or carries a computed prefix, or on an
205
+ `IEndpointRouteBuilder` that arrives as a parameter.
206
+
207
+ The [producer checklist](../evidence.md#producer-checklist) for C#:
208
+
209
+ 1. **Which prefixes belong in the path.** The class `[Route]` prefix and every
210
+ `MapGroup` in the chain. A `~/` or leading `/` action template replaces the
211
+ class prefix, while a group prefix always stays. A group or prefix that is
212
+ not literal reports nothing.
213
+ 2. **Whether the construct is an endpoint.** Controller actions and mapped
214
+ handlers serve. Middleware branches, `MapControllers`, filters and
215
+ authorization policies do not, and a declarative client interface reports
216
+ requests instead.
217
+ 3. **Dynamic or unknown.** `$"/talks/{id}"` and `"/talks/" + id` fill one whole
218
+ segment, so they are dynamic. `$"/talks/find-{term}"` and a URL the source
219
+ computes as a whole are unknown.
220
+ 4. **The local helper.** Not supported. A request is reported at the operation
221
+ that calls the client, so a helper that receives the path reports an unknown
222
+ path and its callers report nothing.
223
+ 5. **The base.** A leading `/` has no base. A relative path such as
224
+ `"api/talks"`, which `HttpClient` resolves against its base address, and
225
+ every declarative template set `configured`, and so does a configuration
226
+ read followed by a path starting with `/`: an `IConfiguration` indexer,
227
+ `GetValue`, `GetConnectionString`, a section's `Value`, or
228
+ `Environment.GetEnvironmentVariable`. `"https://api.example.com/talks"`, a
229
+ parameter, a configuration read continued without `/`, and any other computed
230
+ URL report a leading `unknown` segment.
231
+ 6. **Which operation a file-location route names.** C# declares no routes by
232
+ file location, so the scanner names no operation that way.
233
+ 7. **Which segments are constrained.** A route constraint makes its parameter
234
+ or catch-all constrained: `{id:int}`, `{id:int?}` and
235
+ `{**path:regex(...)}`. A segment mixing text with placeholders, such as
236
+ `v{version}` or `{id}.{format}`, is one constrained parameter named after its
237
+ first placeholder. A default value or a final `?` makes a parameter optional.
238
+ 8. **Registration order.** ASP.NET Core prefers the most specific route, so the
239
+ scanner reports no `order`.
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // @bun
2
2
  // plugins/scanners/csharp/src/index.ts
3
+ import { readFile } from "fs/promises";
3
4
  import path4 from "path";
4
5
 
5
6
  // plugins/scanners/csharp/src/config.ts
@@ -28,9 +29,12 @@ var excluded = new Set([
28
29
  "groma",
29
30
  ".groma"
30
31
  ]);
31
- async function projectFiles(root, matches) {
32
+ async function repositoryFiles(root, matches) {
32
33
  const { stdout } = await execute("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { maxBuffer: 64 * 1024 * 1024 });
33
- return [...new Set(stdout.split("\x00").filter((file) => file && matches(file) && !file.split("/").some((part) => excluded.has(part)) && existsSync(path.join(root, file))))].sort();
34
+ return [...new Set(stdout.split("\x00").filter((file) => file && matches(file) && existsSync(path.join(root, file))))].sort();
35
+ }
36
+ function projectFiles(root, matches) {
37
+ return repositoryFiles(root, (file) => matches(file) && !file.split("/").some((part) => excluded.has(part)));
34
38
  }
35
39
 
36
40
  // plugins/scanners/csharp/src/config.ts
@@ -82,6 +86,116 @@ async function csharpInputs(root, settings) {
82
86
  return [...files.filter((file) => /\.slnx?$/i.test(file)), ...files.filter((file) => /\.csproj$/i.test(file))].map((file) => path2.resolve(root, file));
83
87
  }
84
88
 
89
+ // packages/scanner/src/values.ts
90
+ function object(value, label) {
91
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
92
+ throw new Error(`${label} must be an object`);
93
+ }
94
+ return value;
95
+ }
96
+ function string(value, label) {
97
+ if (typeof value !== "string")
98
+ throw new Error(`${label} must be a string`);
99
+ return value;
100
+ }
101
+ function array(value, label) {
102
+ if (!Array.isArray(value))
103
+ throw new Error(`${label} must be an array`);
104
+ return value;
105
+ }
106
+
107
+ // packages/scanner/src/http.ts
108
+ var pathText = /^[A-Za-z0-9\-._~!$&'()*+,;=:@%]+$/;
109
+ var methodToken = /^[A-Z][A-Z-]*$/;
110
+ function fail(message) {
111
+ throw new Error(message);
112
+ }
113
+ function text(value, label) {
114
+ if (typeof value !== "string" || !pathText.test(value))
115
+ fail(`${label} must be nonempty URL path characters`);
116
+ return value;
117
+ }
118
+ function method(value, label, anyMethod) {
119
+ if (typeof value !== "string" || !(methodToken.test(value) || anyMethod && value === "*")) {
120
+ fail(`${label} must be an uppercase HTTP method${anyMethod ? " or *" : ""}`);
121
+ }
122
+ return value;
123
+ }
124
+ function operation(value, operations) {
125
+ if (typeof value !== "string" || !operations.has(value))
126
+ fail("HTTP fact references unknown operation");
127
+ return value;
128
+ }
129
+ function endpointSegment(value, last) {
130
+ const segment = object(value, "HTTP endpoint segment");
131
+ if (segment.kind === "literal")
132
+ return { kind: "literal", value: text(segment.value, "HTTP literal segment") };
133
+ if (segment.kind !== "parameter" && segment.kind !== "catch-all")
134
+ fail(`unknown HTTP endpoint segment kind: ${String(segment.kind)}`);
135
+ if (segment.kind === "catch-all" && !last)
136
+ fail("an HTTP catch-all segment must be last");
137
+ for (const flag of ["optional", "constrained"]) {
138
+ if (segment[flag] !== undefined && typeof segment[flag] !== "boolean")
139
+ fail(`HTTP segment ${flag} must be a boolean`);
140
+ }
141
+ return {
142
+ kind: segment.kind,
143
+ name: text(segment.name, "HTTP segment name"),
144
+ ...segment.optional ? { optional: true } : {},
145
+ ...segment.constrained ? { constrained: true } : {}
146
+ };
147
+ }
148
+ function requestSegment(value) {
149
+ const segment = object(value, "HTTP request segment");
150
+ if (segment.kind === "literal")
151
+ return { kind: "literal", value: text(segment.value, "HTTP literal segment") };
152
+ if (segment.kind === "dynamic" || segment.kind === "unknown")
153
+ return { kind: segment.kind };
154
+ return fail(`unknown HTTP request segment kind: ${String(segment.kind)}`);
155
+ }
156
+ function order(value) {
157
+ const fact = object(value, "HTTP endpoint order");
158
+ const application = string(fact.application, "HTTP endpoint order application");
159
+ if (application === "")
160
+ fail("HTTP endpoint order application must be nonempty");
161
+ const position = fact.position;
162
+ if (typeof position !== "number" || !Number.isInteger(position) || position < 0) {
163
+ fail("HTTP endpoint order position must be a nonnegative integer");
164
+ }
165
+ return { application, position };
166
+ }
167
+ function endpoint(value, operations) {
168
+ const fact = object(value, "HTTP endpoint");
169
+ const path = array(fact.path, "HTTP endpoint path");
170
+ return {
171
+ operation: operation(fact.operation, operations),
172
+ method: method(fact.method, "HTTP endpoint method", true),
173
+ path: path.map((segment, index) => endpointSegment(segment, index === path.length - 1)),
174
+ ...fact.order === undefined ? {} : { order: order(fact.order) }
175
+ };
176
+ }
177
+ function request(value, operations) {
178
+ const fact = object(value, "HTTP request");
179
+ if (fact.configured !== undefined && typeof fact.configured !== "boolean")
180
+ fail("HTTP request configured must be a boolean");
181
+ return {
182
+ operation: operation(fact.operation, operations),
183
+ ...fact.method === undefined ? {} : { method: method(fact.method, "HTTP request method", false) },
184
+ ...fact.configured ? { configured: true } : {},
185
+ path: array(fact.path, "HTTP request path").map(requestSegment)
186
+ };
187
+ }
188
+ function httpEvidence(endpoints, requests, operations) {
189
+ if (endpoints === undefined && requests === undefined)
190
+ return {};
191
+ if (operations === undefined)
192
+ fail("HTTP facts require operation declarations");
193
+ return {
194
+ ...endpoints === undefined ? {} : { httpEndpoints: array(endpoints, "httpEndpoints").map((fact) => endpoint(fact, operations)) },
195
+ ...requests === undefined ? {} : { httpRequests: array(requests, "httpRequests").map((fact) => request(fact, operations)) }
196
+ };
197
+ }
198
+
85
199
  // packages/scanner/src/index.ts
86
200
  function compare(...values) {
87
201
  return values.join("\x00");
@@ -125,7 +239,9 @@ function createScanObservation(input) {
125
239
  scanner: input.scanner,
126
240
  roots,
127
241
  files,
242
+ ...sourceUnits(input.sourceUnits, filePaths),
128
243
  ...operationEvidence(input, filePaths),
244
+ ...httpEvidence(input.httpEndpoints, input.httpRequests, input.operations && new Set(input.operations.map((operation) => operation.id))),
129
245
  diagnostics: [...new Map(input.diagnostics.map((diagnostic) => [
130
246
  diagnosticKey(diagnostic),
131
247
  diagnostic
@@ -134,6 +250,23 @@ function createScanObservation(input) {
134
250
  })
135
251
  };
136
252
  }
253
+ function sourceUnits(input, paths) {
254
+ if (input === undefined)
255
+ return {};
256
+ const units = array(input, "sourceUnits").map((entry) => {
257
+ const unit = object(entry, "source unit");
258
+ const primary = string(unit.primary, "source unit.primary");
259
+ const files = [...new Set(array(unit.files, "source unit.files").map((file) => string(file, "source unit.file")))].sort();
260
+ if (!files.includes(primary))
261
+ throw new Error(`source unit omits primary file: ${primary}`);
262
+ for (const file of files) {
263
+ if (!paths.has(file))
264
+ throw new Error(`source unit references unknown file: ${file}`);
265
+ }
266
+ return { primary, files };
267
+ });
268
+ return { sourceUnits: [...new Map(units.map((unit) => [compare(unit.primary, ...unit.files), unit])).values()].sort((a, b) => compare(a.primary, ...a.files).localeCompare(compare(b.primary, ...b.files))) };
269
+ }
137
270
  function validateRoots(input) {
138
271
  const roots = uniqueBy(input, (root) => root.id, "root id");
139
272
  const byId = new Map(roots.map((root) => [root.id, root]));
@@ -164,22 +297,6 @@ function diagnosticLocation(value) {
164
297
  ...value.line === undefined ? {} : { line: Number(value.line) }
165
298
  };
166
299
  }
167
- function object(value, label) {
168
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
169
- throw new Error(`${label} must be an object`);
170
- }
171
- return value;
172
- }
173
- function string(value, label) {
174
- if (typeof value !== "string")
175
- throw new Error(`${label} must be a string`);
176
- return value;
177
- }
178
- function array(value, label) {
179
- if (!Array.isArray(value))
180
- throw new Error(`${label} must be an array`);
181
- return value;
182
- }
183
300
  function parseScanObservation(source) {
184
301
  const value = object(JSON.parse(source), "observation");
185
302
  if (value.schemaVersion !== 1)
@@ -193,6 +310,9 @@ function parseScanObservation(source) {
193
310
  engineVersion: string(scanner.engineVersion, "scanner.engineVersion")
194
311
  },
195
312
  ...parseOperations(value),
313
+ httpEndpoints: value.httpEndpoints,
314
+ httpRequests: value.httpRequests,
315
+ ...sourceUnits(value.sourceUnits, new Set(array(value.files, "files").map((entry) => string(object(entry, "file").file, "file.file")))),
196
316
  roots: array(value.roots, "roots").map((entry) => {
197
317
  const root = object(entry, "root");
198
318
  return {
@@ -345,6 +465,9 @@ function combineObservations(parts) {
345
465
  const operations = [];
346
466
  const invocations = [];
347
467
  const diagnostics = [];
468
+ const sourceUnits = [];
469
+ const httpEndpoints = [];
470
+ const httpRequests = [];
348
471
  for (const { key, observation } of parts) {
349
472
  const id = (value) => JSON.stringify([key, value]);
350
473
  roots.push(...observation.roots.map((root) => ({ ...root, id: id(root.id), ...root.parent ? { parent: id(root.parent) } : {} })));
@@ -356,7 +479,10 @@ function combineObservations(parts) {
356
479
  }
357
480
  operations.push(...(observation.operations ?? []).map((operation) => ({ ...operation, id: id(operation.id) })));
358
481
  invocations.push(...(observation.invocations ?? []).map((call) => ({ ...call, source: id(call.source), targets: call.targets.map(id) })));
482
+ httpEndpoints.push(...(observation.httpEndpoints ?? []).map((endpoint) => ({ ...endpoint, operation: id(endpoint.operation) })));
483
+ httpRequests.push(...(observation.httpRequests ?? []).map((request) => ({ ...request, operation: id(request.operation) })));
359
484
  diagnostics.push(...observation.diagnostics);
485
+ sourceUnits.push(...observation.sourceUnits ?? []);
360
486
  }
361
487
  return createScanObservation({
362
488
  scanner: parts[0].observation.scanner,
@@ -364,14 +490,17 @@ function combineObservations(parts) {
364
490
  files: [...files.values()],
365
491
  operations,
366
492
  invocations,
367
- diagnostics
493
+ diagnostics,
494
+ ...parts.some((part) => part.observation.sourceUnits !== undefined) ? { sourceUnits } : {},
495
+ ...parts.some((part) => part.observation.httpEndpoints !== undefined) ? { httpEndpoints } : {},
496
+ ...parts.some((part) => part.observation.httpRequests !== undefined) ? { httpRequests } : {}
368
497
  });
369
498
  }
370
499
 
371
500
  // plugins/scanners/csharp/src/adapter.ts
372
501
  import { access } from "fs/promises";
373
502
  import path3 from "path";
374
- import { fileURLToPath } from "url";
503
+ import { createRequire } from "module";
375
504
 
376
505
  // plugins/scanners/csharp/src/process.ts
377
506
  import { spawn } from "child_process";
@@ -395,7 +524,7 @@ function run(command, args, options) {
395
524
  const child = spawn(script ? process.execPath : command, script ? [command, ...args] : [...args], {
396
525
  cwd: options.cwd,
397
526
  env: { ...process.env, DOTNET_CLI_TELEMETRY_OPTOUT: "1", DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_DISABLE: "1", ...options.env },
398
- stdio: ["ignore", "pipe", "pipe"],
527
+ stdio: ["pipe", "pipe", "pipe"],
399
528
  detached: process.platform !== "win32",
400
529
  windowsHide: true
401
530
  });
@@ -429,6 +558,8 @@ function run(command, args, options) {
429
558
  clearTimeout(timer);
430
559
  reject(error);
431
560
  });
561
+ child.stdin.on("error", () => {});
562
+ child.stdin.end(options.input);
432
563
  child.on("close", (code) => {
433
564
  clearTimeout(timer);
434
565
  if (failure)
@@ -442,41 +573,32 @@ function run(command, args, options) {
442
573
  }
443
574
 
444
575
  // plugins/scanners/csharp/src/adapter.ts
445
- var workerDll = fileURLToPath(new URL("../dist/worker/Groma.CSharpScanner.dll", import.meta.url));
446
- async function requireWorker(file = workerDll) {
576
+ async function requireWorker(file) {
447
577
  try {
448
- await access(file);
578
+ const manifest = file ? undefined : createRequire(import.meta.url).resolve(`@groma/scanner-csharp-${process.platform}-${process.arch}/package.json`);
579
+ const worker = file ?? path3.join(path3.dirname(manifest), "worker", `Groma.CSharpScanner${process.platform === "win32" ? ".exe" : ""}`);
580
+ await access(worker);
581
+ return worker;
449
582
  } catch {
450
- throw new Error("C# worker is not prepared. Install a prebuilt scanner package; contributors run bun scripts/package-csharp-scanner.ts.");
583
+ throw new Error("C# runtime package is missing. Reinstall the scanner with optional dependencies enabled; contributors run bun scripts/package-csharp-scanner.ts.");
451
584
  }
452
585
  }
453
- async function prepareCSharpScan(repositoryRoot, settings, dotnet, worker) {
586
+ async function prepareCSharpScan(repositoryRoot, settings, worker) {
454
587
  const root = path3.resolve(repositoryRoot);
455
588
  const config = parseCSharpSettings(settings);
456
589
  if (!config.input)
457
590
  throw new Error("C# analysis requires a selected project or solution input.");
458
591
  const input = await validateInput(root, config.input);
459
- await requireWorker(worker);
460
- const options = { cwd: path3.dirname(input), timeoutSeconds: config.timeoutSeconds };
461
- let sdkVersion;
462
- try {
463
- sdkVersion = (await run(dotnet, ["--version"], options)).stdout.trim();
464
- } catch (error) {
465
- throw new Error(`C# project SDK is unavailable. Install the SDK selected by global.json and ensure dotnet is on PATH (or set DOTNET_HOST_PATH). ${String(error)}`);
466
- }
467
- const runtimes = (await run(dotnet, ["--list-runtimes"], options)).stdout;
468
- if (!/^Microsoft\.NETCore\.App 10\./m.test(runtimes))
469
- throw new Error("The C# scanner worker requires the .NET 10 runtime alongside the project SDK.");
470
- return { root, config, input, sdkVersion };
471
- }
472
- async function checkCSharpReadiness(repositoryRoot, settings = {}, dotnet = process.env.DOTNET_HOST_PATH ?? "dotnet", worker = workerDll) {
473
- const { input, sdkVersion } = await prepareCSharpScan(repositoryRoot, settings, dotnet, worker);
474
- return { input, sdkVersion };
475
- }
476
- async function scanCSharpSource(repositoryRoot, settings = {}, dotnet = process.env.DOTNET_HOST_PATH ?? "dotnet", worker = workerDll) {
477
- const { root, config, input } = await prepareCSharpScan(repositoryRoot, settings, dotnet, worker);
478
- const { stdout } = await run(dotnet, [
479
- worker,
592
+ const executable = await requireWorker(worker);
593
+ return { root, config, input, executable };
594
+ }
595
+ async function checkCSharpReadiness(repositoryRoot, settings = {}, worker) {
596
+ const { input } = await prepareCSharpScan(repositoryRoot, settings, worker);
597
+ return { input };
598
+ }
599
+ async function scanCSharpSource(repositoryRoot, settings = {}, worker) {
600
+ const { root, config, input, executable } = await prepareCSharpScan(repositoryRoot, settings, worker);
601
+ const { stdout } = await run(executable, [
480
602
  input,
481
603
  "--root",
482
604
  root,
@@ -489,8 +611,24 @@ async function scanCSharpSource(repositoryRoot, settings = {}, dotnet = process.
489
611
  ], { cwd: path3.dirname(input), timeoutSeconds: config.timeoutSeconds });
490
612
  return parseScanObservation(stdout);
491
613
  }
614
+ async function readCSharpOutline(repositoryRoot, references, settings = {}) {
615
+ const worker = await requireWorker();
616
+ const root = path3.resolve(repositoryRoot);
617
+ const { stdout } = await run(worker, ["--outline"], {
618
+ cwd: root,
619
+ timeoutSeconds: parseCSharpSettings(settings).timeoutSeconds,
620
+ input: JSON.stringify({ root, references })
621
+ });
622
+ return JSON.parse(stdout);
623
+ }
492
624
 
493
625
  // plugins/scanners/csharp/src/index.ts
626
+ async function solutionProjects(input) {
627
+ if (/\.csproj$/i.test(input))
628
+ return [input];
629
+ const source = await readFile(input, "utf8");
630
+ return [...source.matchAll(/"([^"]+\.csproj)"|Path="([^"]+\.csproj)"/gi)].map((match) => path4.resolve(path4.dirname(input), (match[1] ?? match[2]).split("\\").join("/")));
631
+ }
494
632
  var scanner = {
495
633
  id: "csharp",
496
634
  watch: {
@@ -514,6 +652,16 @@ var scanner = {
514
652
  for (const input of inputs)
515
653
  await checkCSharpReadiness(root, { ...settings, input });
516
654
  },
655
+ readCodeStructure: readCSharpOutline,
656
+ listSourceFiles: async (root, settings = {}) => {
657
+ const directories = [];
658
+ for (const input of await csharpInputs(root, settings)) {
659
+ for (const project of await solutionProjects(input)) {
660
+ directories.push(path4.relative(root, path4.dirname(project)).split(path4.sep).join("/"));
661
+ }
662
+ }
663
+ return projectFiles(root, (file) => /\.cs$/i.test(file) && !file.split("/").some((part) => /^(?:bin|obj)$/i.test(part)) && directories.some((directory) => directory === "" || file.startsWith(`${directory}/`)));
664
+ },
517
665
  scan: async (root, settings = {}) => {
518
666
  const parts = [];
519
667
  const covered = new Set;