@groma/scanner-csharp 0.1.0

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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +55 -0
  3. package/dist/index.js +458 -0
  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 +68 -0
  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 +96 -0
  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 +14 -0
  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 +594 -0
  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 +13 -0
  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
  115. package/package.json +51 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alex Gavrilescu
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.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # C# scanner
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.
4
+
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
+ }
20
+ ```
21
+
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, settings)`, 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:
50
+
51
+ ```sh
52
+ bun scripts/package-csharp-scanner.ts
53
+ ```
54
+
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`.
package/dist/index.js ADDED
@@ -0,0 +1,458 @@
1
+ // @bun
2
+ // plugins/scanners/csharp/src/adapter.ts
3
+ import { access } from "fs/promises";
4
+ import path2 from "path";
5
+ import { fileURLToPath } from "url";
6
+
7
+ // packages/scanner/src/index.ts
8
+ function compare(...values) {
9
+ return values.join("\x00");
10
+ }
11
+ function uniqueBy(values, key, label) {
12
+ const seen = new Set;
13
+ for (const value of values) {
14
+ const id = key(value);
15
+ if (seen.has(id))
16
+ throw new Error(`duplicate ${label}: ${id}`);
17
+ seen.add(id);
18
+ }
19
+ return [...values].sort((left, right) => key(left).localeCompare(key(right)));
20
+ }
21
+ function createScanObservation(input) {
22
+ const roots = validateRoots(input.roots);
23
+ const rootIds = new Set(roots.map((root) => root.id));
24
+ const files = uniqueBy(input.files.map((file) => ({
25
+ ...file,
26
+ roots: [...new Set(file.roots)].sort(),
27
+ symbols: [...new Map(file.symbols.map((symbol) => [
28
+ compare(symbol.id, symbol.kind),
29
+ symbol
30
+ ])).values()].sort((left, right) => {
31
+ return compare(left.id, left.kind).localeCompare(compare(right.id, right.kind));
32
+ })
33
+ })), (file) => file.file, "file path");
34
+ const filePaths = new Set(files.map((file) => file.file));
35
+ for (const file of files) {
36
+ if (file.roots.length === 0)
37
+ throw new Error(`file has no root: ${file.file}`);
38
+ for (const root of file.roots) {
39
+ if (!rootIds.has(root))
40
+ throw new Error(`file references unknown root: ${root}`);
41
+ }
42
+ }
43
+ for (const diagnostic of input.diagnostics)
44
+ diagnosticLocation(diagnostic);
45
+ return {
46
+ schemaVersion: 1,
47
+ scanner: input.scanner,
48
+ roots,
49
+ files,
50
+ ...operationEvidence(input, filePaths),
51
+ diagnostics: [...new Map(input.diagnostics.map((diagnostic) => [
52
+ diagnosticKey(diagnostic),
53
+ diagnostic
54
+ ])).values()].sort((left, right) => {
55
+ return diagnosticKey(left).localeCompare(diagnosticKey(right));
56
+ })
57
+ };
58
+ }
59
+ function validateRoots(input) {
60
+ const roots = uniqueBy(input, (root) => root.id, "root id");
61
+ const byId = new Map(roots.map((root) => [root.id, root]));
62
+ for (const root of roots) {
63
+ const visited = new Set([root.id]);
64
+ let parent = root.parent;
65
+ while (parent !== undefined) {
66
+ if (visited.has(parent))
67
+ throw new Error(`root hierarchy contains a cycle: ${parent}`);
68
+ visited.add(parent);
69
+ const ancestor = byId.get(parent);
70
+ if (ancestor === undefined)
71
+ throw new Error(`root references unknown parent: ${parent}`);
72
+ parent = ancestor.parent;
73
+ }
74
+ }
75
+ return roots;
76
+ }
77
+ function diagnosticKey(diagnostic) {
78
+ return compare(diagnostic.severity, diagnostic.code, diagnostic.message, diagnostic.file ?? "", String(diagnostic.line ?? ""));
79
+ }
80
+ function diagnosticLocation(value) {
81
+ if (value.line !== undefined && (!Number.isInteger(value.line) || Number(value.line) < 1)) {
82
+ throw new Error("diagnostic.line must be a positive integer");
83
+ }
84
+ return {
85
+ ...value.file === undefined ? {} : { file: string(value.file, "diagnostic.file") },
86
+ ...value.line === undefined ? {} : { line: Number(value.line) }
87
+ };
88
+ }
89
+ function object(value, label) {
90
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
91
+ throw new Error(`${label} must be an object`);
92
+ }
93
+ return value;
94
+ }
95
+ function string(value, label) {
96
+ if (typeof value !== "string")
97
+ throw new Error(`${label} must be a string`);
98
+ return value;
99
+ }
100
+ function array(value, label) {
101
+ if (!Array.isArray(value))
102
+ throw new Error(`${label} must be an array`);
103
+ return value;
104
+ }
105
+ function parseScanObservation(source) {
106
+ const value = object(JSON.parse(source), "observation");
107
+ if (value.schemaVersion !== 1)
108
+ throw new Error("unsupported scanner schema");
109
+ const scanner = object(value.scanner, "scanner");
110
+ return createScanObservation({
111
+ scanner: {
112
+ id: string(scanner.id, "scanner.id"),
113
+ technology: string(scanner.technology, "scanner.technology"),
114
+ engine: string(scanner.engine, "scanner.engine"),
115
+ engineVersion: string(scanner.engineVersion, "scanner.engineVersion")
116
+ },
117
+ ...parseOperations(value),
118
+ roots: array(value.roots, "roots").map((entry) => {
119
+ const root = object(entry, "root");
120
+ return {
121
+ id: string(root.id, "root.id"),
122
+ kind: string(root.kind, "root.kind"),
123
+ name: string(root.name, "root.name"),
124
+ ...root.file === undefined ? {} : { file: string(root.file, "root.file") },
125
+ ...root.parent === undefined ? {} : { parent: string(root.parent, "root.parent") }
126
+ };
127
+ }),
128
+ files: array(value.files, "files").map((entry, index) => {
129
+ const file = object(entry, `files[${index}]`);
130
+ return {
131
+ file: string(file.file, `files[${index}].file`),
132
+ roots: array(file.roots, "file.roots").map((root) => string(root, "file.root")),
133
+ symbols: array(file.symbols, `files[${index}].symbols`).map((entry, symbolIndex) => {
134
+ const symbol = object(entry, `files[${index}].symbols[${symbolIndex}]`);
135
+ return {
136
+ id: string(symbol.id, "symbol.id"),
137
+ name: string(symbol.name, "symbol.name"),
138
+ kind: string(symbol.kind, "symbol.kind")
139
+ };
140
+ })
141
+ };
142
+ }),
143
+ diagnostics: array(value.diagnostics, "diagnostics").map((entry, index) => {
144
+ const diagnostic = object(entry, `diagnostics[${index}]`);
145
+ return {
146
+ severity: string(diagnostic.severity, `diagnostics[${index}].severity`),
147
+ code: string(diagnostic.code, `diagnostics[${index}].code`),
148
+ message: string(diagnostic.message, `diagnostics[${index}].message`),
149
+ ...diagnosticLocation(diagnostic)
150
+ };
151
+ })
152
+ });
153
+ }
154
+ function parseOperations(value) {
155
+ if (value.operations === undefined) {
156
+ if (value.invocations !== undefined)
157
+ throw new Error("invocations require operation declarations");
158
+ return {};
159
+ }
160
+ const operations = array(value.operations, "operations").map((entry) => {
161
+ const operation = object(entry, "operation");
162
+ return {
163
+ id: string(operation.id, "operation.id"),
164
+ file: string(operation.file, "operation.file"),
165
+ name: string(operation.name, "operation.name"),
166
+ ...sourcePosition(operation.position),
167
+ ...operationTokens(operation)
168
+ };
169
+ });
170
+ const invocations = array(value.invocations, "invocations").map((entry) => {
171
+ const invocation = object(entry, "invocation");
172
+ if (typeof invocation.unresolved !== "boolean")
173
+ throw new Error("invocation.unresolved must be a boolean");
174
+ if (!Number.isInteger(invocation.line) || Number(invocation.line) < 1)
175
+ throw new Error("invocation.line must be a positive integer");
176
+ const binding = invocation.binding === undefined ? undefined : object(invocation.binding, "invocation.binding");
177
+ if (binding && (!Number.isInteger(binding.line) || Number(binding.line) < 1))
178
+ throw new Error("binding.line must be a positive integer");
179
+ return {
180
+ source: string(invocation.source, "invocation.source"),
181
+ targets: array(invocation.targets, "invocation.targets").map((target) => string(target, "invocation.target")),
182
+ unresolved: invocation.unresolved,
183
+ line: Number(invocation.line),
184
+ ...sourcePosition(invocation.position),
185
+ ...invocation.member === undefined ? {} : { member: string(invocation.member, "invocation.member") },
186
+ ...binding === undefined ? {} : { binding: { file: string(binding.file, "binding.file"), line: Number(binding.line), ...sourcePosition(binding.position) } }
187
+ };
188
+ });
189
+ return { operations, invocations };
190
+ }
191
+ function operationEvidence(input, files) {
192
+ if (input.operations === undefined && input.invocations !== undefined)
193
+ throw new Error("invocations require operation declarations");
194
+ if (input.operations === undefined)
195
+ return {};
196
+ const operations = uniqueBy(input.operations, (operation) => operation.id, "operation id");
197
+ const ids = new Set(operations.map((operation) => operation.id));
198
+ for (const operation of operations) {
199
+ if (!files.has(operation.file))
200
+ throw new Error(`operation references unknown file: ${operation.file}`);
201
+ sourcePosition(operation.position);
202
+ validateOperationTokens(operation);
203
+ }
204
+ const invocations = input.invocations ?? [];
205
+ for (const invocation of invocations)
206
+ validateInvocation(invocation, ids, files);
207
+ return { operations, invocations };
208
+ }
209
+ function operationTokens(operation) {
210
+ if (operation.tokens === undefined) {
211
+ if (operation.startLine !== undefined || operation.endLine !== undefined) {
212
+ throw new Error("operation range requires tokens");
213
+ }
214
+ return {};
215
+ }
216
+ if (operation.startLine === undefined || operation.endLine === undefined) {
217
+ throw new Error("operation tokens require a source range");
218
+ }
219
+ if (!Number.isInteger(operation.startLine) || Number(operation.startLine) < 1) {
220
+ throw new Error("operation.startLine must be a positive integer");
221
+ }
222
+ if (!Number.isInteger(operation.endLine) || Number(operation.endLine) < 1) {
223
+ throw new Error("operation.endLine must be a positive integer");
224
+ }
225
+ const startLine = Number(operation.startLine);
226
+ const endLine = Number(operation.endLine);
227
+ if (endLine < startLine)
228
+ throw new Error("operation.endLine must be at or after startLine");
229
+ return {
230
+ startLine,
231
+ endLine,
232
+ tokens: array(operation.tokens, "operation.tokens").map((token) => string(token, "operation.token"))
233
+ };
234
+ }
235
+ function validateOperationTokens(operation) {
236
+ operationTokens({
237
+ tokens: operation.tokens,
238
+ startLine: operation.startLine,
239
+ endLine: operation.endLine
240
+ });
241
+ }
242
+ function validateInvocation(invocation, ids, files) {
243
+ sourcePosition(invocation.position);
244
+ sourcePosition(invocation.binding?.position);
245
+ if (!invocation.targets.length && !invocation.unresolved)
246
+ throw new Error("an empty target set must be unresolved");
247
+ if (!ids.has(invocation.source) || invocation.targets.some((target) => !ids.has(target))) {
248
+ throw new Error("invocation references unknown operation");
249
+ }
250
+ if (invocation.binding && !files.has(invocation.binding.file))
251
+ throw new Error("invocation binding references unknown file");
252
+ }
253
+ function sourcePosition(position) {
254
+ if (position === undefined)
255
+ return {};
256
+ if (!Number.isInteger(position) || Number(position) < 0)
257
+ throw new Error("source position must be a non-negative integer");
258
+ return { position: Number(position) };
259
+ }
260
+
261
+ // plugins/scanners/csharp/src/config.ts
262
+ import { readdir, stat } from "fs/promises";
263
+ import path from "path";
264
+ var defaults = { configuration: "Debug", maxProjects: 128, maxFiles: 20000, timeoutSeconds: 120 };
265
+ var configName = "csharp scanner settings";
266
+ function positiveInteger(value, key, fallback) {
267
+ if (value === undefined)
268
+ return fallback;
269
+ if (!Number.isSafeInteger(value) || Number(value) < 1)
270
+ throw new Error(`${configName}: ${key} must be a positive integer`);
271
+ return Number(value);
272
+ }
273
+ function optionalText(value, key) {
274
+ if (value === undefined)
275
+ return;
276
+ if (typeof value !== "string" || !value.trim())
277
+ throw new Error(`${configName}: ${key} must be nonempty text`);
278
+ return value;
279
+ }
280
+ function parseCSharpSettings(config = {}) {
281
+ for (const key of Object.keys(config)) {
282
+ if (!["input", ...Object.keys(defaults)].includes(key))
283
+ throw new Error(`${configName}: unknown setting ${key}`);
284
+ }
285
+ const input = optionalText(config.input, "input");
286
+ const configuration = optionalText(config.configuration, "configuration");
287
+ return {
288
+ ...input === undefined ? {} : { input },
289
+ configuration: configuration ?? defaults.configuration,
290
+ maxProjects: positiveInteger(config.maxProjects, "maxProjects", defaults.maxProjects),
291
+ maxFiles: positiveInteger(config.maxFiles, "maxFiles", defaults.maxFiles),
292
+ timeoutSeconds: positiveInteger(config.timeoutSeconds, "timeoutSeconds", defaults.timeoutSeconds)
293
+ };
294
+ }
295
+ async function findCSharpInput(root, selected) {
296
+ if (selected !== undefined)
297
+ return validateInput(root, selected);
298
+ const files = (await readdir(root, { withFileTypes: true })).filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
299
+ const solutions = files.filter((file) => /\.slnx?$/i.test(file));
300
+ const candidates = solutions.length ? solutions : files.filter((file) => /\.csproj$/i.test(file));
301
+ if (candidates.length > 1)
302
+ throw new Error(`Several C# scan inputs exist; set settings.input on the csharp entry in scanners.json: ${candidates.join(", ")}`);
303
+ return candidates.length ? path.resolve(root, candidates[0]) : undefined;
304
+ }
305
+ async function validateInput(root, selected) {
306
+ const full = path.resolve(root, selected);
307
+ const relative = path.relative(root, full);
308
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
309
+ throw new Error("C# scan input must stay inside the repository");
310
+ if (!/\.(?:csproj|slnx?)$/i.test(full) || !(await stat(full)).isFile())
311
+ throw new Error("C# input must be an existing .csproj, .sln, or .slnx file");
312
+ return full;
313
+ }
314
+
315
+ // plugins/scanners/csharp/src/process.ts
316
+ import { spawn } from "child_process";
317
+ function stopTree(child) {
318
+ if (child.pid === undefined)
319
+ return;
320
+ if (process.platform === "win32") {
321
+ const killer = spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
322
+ killer.on("error", () => child.kill());
323
+ return;
324
+ }
325
+ try {
326
+ process.kill(-child.pid, "SIGKILL");
327
+ } catch {
328
+ child.kill("SIGKILL");
329
+ }
330
+ }
331
+ function run(command, args, options) {
332
+ return new Promise((resolve, reject) => {
333
+ const script = /\.[cm]?js$/.test(command);
334
+ const child = spawn(script ? process.execPath : command, script ? [command, ...args] : [...args], {
335
+ cwd: options.cwd,
336
+ env: { ...process.env, DOTNET_CLI_TELEMETRY_OPTOUT: "1", DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_DISABLE: "1", ...options.env },
337
+ stdio: ["ignore", "pipe", "pipe"],
338
+ detached: process.platform !== "win32",
339
+ windowsHide: true
340
+ });
341
+ let stdout = "";
342
+ let stderr = "";
343
+ let size = 0;
344
+ let failure;
345
+ const fail = (error) => {
346
+ if (failure)
347
+ return;
348
+ failure = error;
349
+ stopTree(child);
350
+ };
351
+ const timer = setTimeout(() => fail(new Error(`C# command exceeded ${options.timeoutSeconds ?? 120} seconds`)), (options.timeoutSeconds ?? 120) * 1000);
352
+ child.stdout.setEncoding("utf8");
353
+ child.stderr.setEncoding("utf8");
354
+ child.stdout.on("data", (chunk) => {
355
+ size += Buffer.byteLength(chunk);
356
+ if (size > (options.maxOutputBytes ?? 128 * 1024 * 1024))
357
+ fail(new Error("C# command exceeded the output limit"));
358
+ else
359
+ stdout += chunk;
360
+ });
361
+ child.stderr.on("data", (chunk) => {
362
+ if (Buffer.byteLength(stderr) + Buffer.byteLength(chunk) > 1024 * 1024)
363
+ fail(new Error("C# command exceeded the diagnostic output limit"));
364
+ else
365
+ stderr += chunk;
366
+ });
367
+ child.on("error", (error) => {
368
+ clearTimeout(timer);
369
+ reject(error);
370
+ });
371
+ child.on("close", (code) => {
372
+ clearTimeout(timer);
373
+ if (failure)
374
+ reject(failure);
375
+ else if (code !== 0)
376
+ reject(new Error(stderr.trim() || stdout.trim() || `${command} exited ${code}`));
377
+ else
378
+ resolve({ stdout, stderr });
379
+ });
380
+ });
381
+ }
382
+
383
+ // plugins/scanners/csharp/src/adapter.ts
384
+ var workerDll = fileURLToPath(new URL("../dist/worker/Groma.CSharpScanner.dll", import.meta.url));
385
+ async function requireWorker(file = workerDll) {
386
+ try {
387
+ await access(file);
388
+ } catch {
389
+ throw new Error("C# worker is not prepared. Install a prebuilt scanner package; contributors run bun scripts/package-csharp-scanner.ts.");
390
+ }
391
+ }
392
+ async function prepareCSharpScan(repositoryRoot, settings, dotnet, worker) {
393
+ const root = path2.resolve(repositoryRoot);
394
+ const config = parseCSharpSettings(settings);
395
+ const input = await findCSharpInput(root, config.input);
396
+ if (!input)
397
+ throw new Error("No C# input was selected. Set settings.input on the csharp entry in scanners.json or disable the csharp scanner.");
398
+ await requireWorker(worker);
399
+ const options = { cwd: path2.dirname(input), timeoutSeconds: config.timeoutSeconds };
400
+ let sdkVersion;
401
+ try {
402
+ sdkVersion = (await run(dotnet, ["--version"], options)).stdout.trim();
403
+ } catch (error) {
404
+ 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)}`);
405
+ }
406
+ const runtimes = (await run(dotnet, ["--list-runtimes"], options)).stdout;
407
+ if (!/^Microsoft\.NETCore\.App 10\./m.test(runtimes))
408
+ throw new Error("The C# scanner worker requires the .NET 10 runtime alongside the project SDK.");
409
+ return { root, config, input, sdkVersion };
410
+ }
411
+ async function checkCSharpReadiness(repositoryRoot, settings = {}, dotnet = process.env.DOTNET_HOST_PATH ?? "dotnet", worker = workerDll) {
412
+ const { input, sdkVersion } = await prepareCSharpScan(repositoryRoot, settings, dotnet, worker);
413
+ return { input, sdkVersion };
414
+ }
415
+ async function scanCSharpSource(repositoryRoot, settings = {}, dotnet = process.env.DOTNET_HOST_PATH ?? "dotnet", worker = workerDll) {
416
+ const { root, config, input } = await prepareCSharpScan(repositoryRoot, settings, dotnet, worker);
417
+ const { stdout } = await run(dotnet, [
418
+ worker,
419
+ input,
420
+ "--root",
421
+ root,
422
+ "--configuration",
423
+ config.configuration,
424
+ "--max-projects",
425
+ String(config.maxProjects),
426
+ "--max-files",
427
+ String(config.maxFiles)
428
+ ], { cwd: path2.dirname(input), timeoutSeconds: config.timeoutSeconds });
429
+ return parseScanObservation(stdout);
430
+ }
431
+
432
+ // plugins/scanners/csharp/src/index.ts
433
+ var scanner = {
434
+ id: "csharp",
435
+ watch: {
436
+ include: [
437
+ "**/*.cs",
438
+ "**/*.csproj",
439
+ "**/*.sln",
440
+ "**/*.slnx",
441
+ "**/*.props",
442
+ "**/*.targets",
443
+ "**/global.json",
444
+ "**/nuget.config",
445
+ "**/packages.lock.json"
446
+ ].map((pattern) => pattern.replace(/[a-z]/g, (letter) => `[${letter}${letter.toUpperCase()}]`)),
447
+ exclude: ["**/[bB][iI][nN]/**", "**/[oO][bB][jJ]/**"]
448
+ },
449
+ checkReadiness: async (root, settings) => {
450
+ await checkCSharpReadiness(root, settings);
451
+ },
452
+ scan: scanCSharpSource
453
+ };
454
+ var src_default = scanner;
455
+ export {
456
+ checkCSharpReadiness,
457
+ src_default as default
458
+ };
@@ -0,0 +1,68 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <configuration>
3
+ <startup>
4
+ <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
5
+ </startup>
6
+ <runtime>
7
+ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
8
+ <dependentAssembly>
9
+ <assemblyIdentity name="Microsoft.Build" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
10
+ <bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0" />
11
+ </dependentAssembly>
12
+ </assemblyBinding>
13
+ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
14
+ <dependentAssembly>
15
+ <assemblyIdentity name="Microsoft.Build.Framework" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
16
+ <bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0" />
17
+ </dependentAssembly>
18
+ </assemblyBinding>
19
+ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
20
+ <dependentAssembly>
21
+ <assemblyIdentity name="Microsoft.Build.Utilities.Core" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
22
+ <bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0" />
23
+ </dependentAssembly>
24
+ </assemblyBinding>
25
+ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
26
+ <dependentAssembly>
27
+ <assemblyIdentity name="Microsoft.Build.Tasks.Core" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
28
+ <bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0" />
29
+ </dependentAssembly>
30
+ </assemblyBinding>
31
+ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
32
+ <dependentAssembly>
33
+ <assemblyIdentity name="Microsoft.Bcl.AsyncInterfaces" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
34
+ <bindingRedirect oldVersion="0.0.0.0-10.0.0.1" newVersion="10.0.0.1" />
35
+ </dependentAssembly>
36
+ </assemblyBinding>
37
+ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
38
+ <dependentAssembly>
39
+ <assemblyIdentity name="Microsoft.IO.Redist" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
40
+ <bindingRedirect oldVersion="0.0.0.0-6.1.0.0" newVersion="6.1.0.0" />
41
+ </dependentAssembly>
42
+ </assemblyBinding>
43
+ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
44
+ <dependentAssembly>
45
+ <assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
46
+ <bindingRedirect oldVersion="0.0.0.0-4.0.5.0" newVersion="4.0.5.0" />
47
+ </dependentAssembly>
48
+ </assemblyBinding>
49
+ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
50
+ <dependentAssembly>
51
+ <assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
52
+ <bindingRedirect oldVersion="0.0.0.0-4.0.5.0" newVersion="4.0.5.0" />
53
+ </dependentAssembly>
54
+ </assemblyBinding>
55
+ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
56
+ <dependentAssembly>
57
+ <assemblyIdentity name="System.Text.Json" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
58
+ <bindingRedirect oldVersion="0.0.0.0-10.0.0.1" newVersion="10.0.0.1" />
59
+ </dependentAssembly>
60
+ </assemblyBinding>
61
+ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
62
+ <dependentAssembly>
63
+ <assemblyIdentity name="System.Threading.Tasks.Extensions" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
64
+ <bindingRedirect oldVersion="0.0.0.0-4.2.4.0" newVersion="4.2.4.0" />
65
+ </dependentAssembly>
66
+ </assemblyBinding>
67
+ </runtime>
68
+ </configuration>