@groma/scanner-csharp 0.1.4 → 0.2.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.
- package/README.md +86 -41
- package/dist/index.js +86 -207
- package/package.json +28 -8
package/README.md
CHANGED
|
@@ -31,44 +31,84 @@ only: scanner evidence, OKF records and C4 boundaries are unchanged.
|
|
|
31
31
|
|
|
32
32
|
## Source inputs
|
|
33
33
|
|
|
34
|
-
The scanner
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
entry to select one
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
34
|
+
The scanner reads the `.csproj`, `.sln` and `.slnx` files among its files, and
|
|
35
|
+
every project among them that they reference, and loads each project once,
|
|
36
|
+
however many solutions list it. Set `settings.input` on the existing C# scanner
|
|
37
|
+
entry to select one of those projects or solutions, with the projects it
|
|
38
|
+
references, relative to the repository root. To explain a file without an owner,
|
|
39
|
+
the scanner lists every C# source its include list names, before exclusions: a
|
|
40
|
+
project may compile any repository file, so this never leaves out a file a scan
|
|
41
|
+
reads.
|
|
42
|
+
|
|
43
|
+
An in-memory Roslyn workspace reads project XML and authored C# files. The
|
|
44
|
+
nearest `Directory.Build.props`, the project file, and the files either imports
|
|
45
|
+
by a path the scanner can expand supply their unconditional properties and items
|
|
46
|
+
in document order: the language version, `DefineConstants`, `ImplicitUsings`
|
|
47
|
+
and `Using` items, `AllowUnsafeBlocks`, `OutputType`, and `Compile` includes and
|
|
48
|
+
removes. A value may use `$(Name)` for a property read before it. In a property
|
|
49
|
+
value an unset property is empty, as in MSBuild, while an item or import path
|
|
50
|
+
that names one stays unknown. Conditions, targets, property functions and SDK
|
|
51
|
+
files are not evaluated. The scanner's runtime supplies base .NET declarations.
|
|
52
|
+
It does not run MSBuild, restore packages, or execute source generators.
|
|
53
|
+
|
|
54
|
+
Every SDK-style project is read, whatever its SDK: .NET, Web, Worker, Razor,
|
|
55
|
+
Blazor WebAssembly, Aspire AppHost or an explicit `Sdk` import. Projects in
|
|
56
|
+
other languages are skipped. A C# project that is not SDK-style is skipped with
|
|
57
|
+
a `CSHARP_UNSUPPORTED_PROJECT` warning. Project references follow the SDK: they
|
|
58
|
+
are transitive.
|
|
59
|
+
|
|
60
|
+
The package declares default [include and exclude lists](../index.md#selecting-source-files).
|
|
61
|
+
The include list names C# sources (`**/*.cs`), projects (`**/*.csproj`),
|
|
62
|
+
solutions (`**/*.sln` and `**/*.slnx`) and MSBuild files (`**/*.props` and
|
|
63
|
+
`**/*.targets`), each extension in any letter case. MSBuild files are only a
|
|
64
|
+
project's build context; the list names them so that a change to one triggers a
|
|
65
|
+
scan. The exclude list names build output (`bin` and `obj`, in any letter case),
|
|
66
|
+
test code (folders named `test` or `tests` in either initial case, folders whose
|
|
67
|
+
names end in `Tests`, and `*.Test` folders), and generated files
|
|
68
|
+
(`*.Designer.cs`, `*.g.cs`, `*.g.i.cs` and `*.generated.cs`). A source, project
|
|
69
|
+
or solution outside the scanner's files is never read, whether a solution, a
|
|
70
|
+
reference or `settings.input` names it, so a declaration that only such a file
|
|
71
|
+
provides is reported as missing where code uses it. An included project's
|
|
72
|
+
nearest `Directory.Build.props` and the files it imports are read as MSBuild
|
|
73
|
+
reads them, even when excluded. Which files a project compiles stays built in.
|
|
74
|
+
|
|
75
|
+
For executable projects, Roslyn's resolved entry point supplies an execution fact
|
|
76
|
+
with its source file, project declaration, assembly name and the project's own
|
|
77
|
+
compiled source files. Referenced projects remain separate source units. An
|
|
78
|
+
explicit `OutputType`, in any case, takes precedence; the Web and Worker SDKs
|
|
79
|
+
default to `Exe`; otherwise top-level statements, which C# accepts only in an
|
|
80
|
+
executable, make the project one. Core applies the
|
|
81
|
+
[shared container-placement rule](../evidence.md#execution-entries-and-container-placement).
|
|
82
|
+
Solutions and library projects alone do not imply containers.
|
|
83
|
+
|
|
84
|
+
A source file that several projects compile belongs to each of them. It is
|
|
85
|
+
analyzed once, in the context of the first project by path. Invalid syntax fails
|
|
86
|
+
a scan. Missing external types produce warnings located by file and line, while
|
|
50
87
|
local declarations and supported operations remain available.
|
|
51
88
|
|
|
52
89
|
## Evidence
|
|
53
90
|
|
|
54
91
|
Partial class declarations in separate authored files are proposed as one source
|
|
55
92
|
unit when Roslyn identifies the same class in the same project. Each file must
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
93
|
+
declare only that type, apart from the types nested in it, and every declaration
|
|
94
|
+
must qualify. The primary is the file named after the class, else the file with
|
|
95
|
+
the shortest name. Same-name classes in other namespaces or projects are
|
|
96
|
+
independent. Files containing additional types remain separate source evidence.
|
|
97
|
+
This does not associate Razor, XAML, or generated source.
|
|
60
98
|
|
|
61
99
|
Core creates one component with all member Code references, attaches newly
|
|
62
100
|
discovered unowned partial files, and retains curated ownership on repeat scans.
|
|
63
|
-
Conflicting owners produce review diagnostics.
|
|
64
|
-
|
|
65
|
-
build, or source-generator execution is needed.
|
|
101
|
+
Conflicting owners produce review diagnostics. No project restore, build, or
|
|
102
|
+
source-generator execution is needed.
|
|
66
103
|
|
|
67
104
|
Roslyn resolves local overloads, generic methods, extensions, partial
|
|
68
|
-
implementations and direct calls.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
105
|
+
implementations and direct calls. Virtual dispatch and interface calls retain
|
|
106
|
+
uncertainty. Calls with type errors, and calls that reach no repository
|
|
107
|
+
operation, such as calls into packages, the framework or delegates, are not
|
|
108
|
+
reported. A lambda is an operation unless a call that binds converts it to an
|
|
109
|
+
expression tree. Implicit calls, initializers, generated operations,
|
|
110
|
+
receiver/delegate value flow, dependency injection and network protocols are
|
|
111
|
+
not resolved.
|
|
72
112
|
|
|
73
113
|
Solutions and projects supply source roots and file membership. They are not
|
|
74
114
|
C4 components or proof of business collaborations. Core owns curated membership
|
|
@@ -76,10 +116,10 @@ and relationship selection; ordinary Markdown readers see the existing OKF
|
|
|
76
116
|
records and Code links. The scanner adds no map level or architecture metadata.
|
|
77
117
|
|
|
78
118
|
Settings also accept `configuration` (default `Debug`), `maxProjects` (128),
|
|
79
|
-
`maxFiles` (20000) and `timeoutSeconds` (120)
|
|
80
|
-
DEBUG/TRACE symbols; it does not execute a
|
|
81
|
-
rather than publish truncated evidence. Failed
|
|
82
|
-
evidence while other scanners can update the map.
|
|
119
|
+
`maxFiles` (20000) and `timeoutSeconds` (120), which apply to the whole scan.
|
|
120
|
+
Configuration selects the source DEBUG/TRACE symbols; it does not execute a
|
|
121
|
+
build configuration. Limits fail rather than publish truncated evidence. Failed
|
|
122
|
+
scanners retain their saved evidence while other scanners can update the map.
|
|
83
123
|
|
|
84
124
|
Run `dotnet test plugins/scanners/csharp/dotnet/test/Groma.CSharpScanner.Tests.csproj`
|
|
85
125
|
for Roslyn tests. Fixtures are scanned without restore. The packaged test
|
|
@@ -97,7 +137,8 @@ project, restoring packages or building.
|
|
|
97
137
|
in the file or inside its namespaces are types. Enums and delegates have no
|
|
98
138
|
members.
|
|
99
139
|
- A type's members are the methods, interface method signatures, constructors,
|
|
100
|
-
finalizers and operators declared in its body,
|
|
140
|
+
finalizers and operators declared in its body, including the methods of its
|
|
141
|
+
C# 14 extension blocks, each overload separately.
|
|
101
142
|
Constructors are named after the type and finalizers `~Type`; operators are
|
|
102
143
|
named like `operator +` or `implicit operator int`.
|
|
103
144
|
|
|
@@ -174,11 +215,13 @@ Endpoints:
|
|
|
174
215
|
- a controller is a public, top-level, non-generic, non-abstract class declared
|
|
175
216
|
in one place, named `*Controller`, marked `[Controller]` or deriving from
|
|
176
217
|
`ControllerBase` or `Controller`, and not `[NonController]`, counting the
|
|
177
|
-
attributes of its bases.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
218
|
+
attributes of its bases. Its class prefixes are the `[Route]` attributes of
|
|
219
|
+
the most derived class that declares any, as ASP.NET Core reads them, even
|
|
220
|
+
when that base is declared in a referenced project. A controller also serves
|
|
221
|
+
the public methods of its bases, so one with a base between it and
|
|
222
|
+
`ControllerBase` that declares a public method, or that the source does not
|
|
223
|
+
declare, reports nothing. An action is a public, non-static, non-generic
|
|
224
|
+
method with a body and without `[NonAction]`.
|
|
182
225
|
- minimal APIs: `MapGet`, `MapPost`, `MapPut`, `MapDelete`, `MapPatch`, and
|
|
183
226
|
`MapMethods` with literal methods, on a `MapGroup` chain or on the
|
|
184
227
|
application: the result of `Build()` or `WebApplication.Create()`, or any
|
|
@@ -187,10 +230,12 @@ Endpoints:
|
|
|
187
230
|
|
|
188
231
|
Requests:
|
|
189
232
|
|
|
190
|
-
- `HttpClient` calls, recognized by the receiver's own type
|
|
191
|
-
`
|
|
192
|
-
`
|
|
193
|
-
`
|
|
233
|
+
- `HttpClient` calls, recognized by the receiver's own type or as the client
|
|
234
|
+
`IHttpClientFactory.CreateClient` returns, directly or held by a local: the
|
|
235
|
+
`GetAsync`, `GetStringAsync`, `GetFromJsonAsync`, `PostAsync`,
|
|
236
|
+
`PostAsJsonAsync`, `PutAsync`, `PatchAsync` and `DeleteAsync` families, and
|
|
237
|
+
`SendAsync` with a `new HttpRequestMessage(HttpMethod.Get, url)` written in
|
|
238
|
+
the call or held by a local the file never assigns again.
|
|
194
239
|
- declarative client interfaces such as Refit: `[Get]`, `[Post]`, `[Put]`,
|
|
195
240
|
`[Delete]`, `[Patch]`, `[Head]` and `[Options]` on an interface method. Such
|
|
196
241
|
a method has no body, so its request declares the operation itself.
|
package/dist/index.js
CHANGED
|
@@ -1,90 +1,8 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// plugins/scanners/csharp/src/
|
|
3
|
-
import {
|
|
4
|
-
import path4 from "path";
|
|
5
|
-
|
|
6
|
-
// plugins/scanners/csharp/src/config.ts
|
|
7
|
-
import { stat } from "fs/promises";
|
|
8
|
-
import path2 from "path";
|
|
9
|
-
|
|
10
|
-
// plugins/scanners/projects.ts
|
|
11
|
-
import { execFile } from "child_process";
|
|
12
|
-
import { existsSync } from "fs";
|
|
2
|
+
// plugins/scanners/csharp/src/adapter.ts
|
|
3
|
+
import { access } from "fs/promises";
|
|
13
4
|
import path from "path";
|
|
14
|
-
import {
|
|
15
|
-
var execute = promisify(execFile);
|
|
16
|
-
var excluded = new Set([
|
|
17
|
-
".git",
|
|
18
|
-
"node_modules",
|
|
19
|
-
"vendor",
|
|
20
|
-
"target",
|
|
21
|
-
"dist",
|
|
22
|
-
"build",
|
|
23
|
-
"bin",
|
|
24
|
-
"obj",
|
|
25
|
-
".gradle",
|
|
26
|
-
".angular",
|
|
27
|
-
"coverage",
|
|
28
|
-
"generated",
|
|
29
|
-
"groma",
|
|
30
|
-
".groma"
|
|
31
|
-
]);
|
|
32
|
-
async function repositoryFiles(root, matches) {
|
|
33
|
-
const { stdout } = await execute("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { maxBuffer: 64 * 1024 * 1024 });
|
|
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)));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// plugins/scanners/csharp/src/config.ts
|
|
41
|
-
var defaults = { configuration: "Debug", maxProjects: 128, maxFiles: 20000, timeoutSeconds: 120 };
|
|
42
|
-
var configName = "csharp scanner settings";
|
|
43
|
-
function positiveInteger(value, key, fallback) {
|
|
44
|
-
if (value === undefined)
|
|
45
|
-
return fallback;
|
|
46
|
-
if (!Number.isSafeInteger(value) || Number(value) < 1)
|
|
47
|
-
throw new Error(`${configName}: ${key} must be a positive integer`);
|
|
48
|
-
return Number(value);
|
|
49
|
-
}
|
|
50
|
-
function optionalText(value, key) {
|
|
51
|
-
if (value === undefined)
|
|
52
|
-
return;
|
|
53
|
-
if (typeof value !== "string" || !value.trim())
|
|
54
|
-
throw new Error(`${configName}: ${key} must be nonempty text`);
|
|
55
|
-
return value;
|
|
56
|
-
}
|
|
57
|
-
function parseCSharpSettings(config = {}) {
|
|
58
|
-
for (const key of Object.keys(config)) {
|
|
59
|
-
if (!["input", ...Object.keys(defaults)].includes(key))
|
|
60
|
-
throw new Error(`${configName}: unknown setting ${key}`);
|
|
61
|
-
}
|
|
62
|
-
const input = optionalText(config.input, "input");
|
|
63
|
-
const configuration = optionalText(config.configuration, "configuration");
|
|
64
|
-
return {
|
|
65
|
-
...input === undefined ? {} : { input },
|
|
66
|
-
configuration: configuration ?? defaults.configuration,
|
|
67
|
-
maxProjects: positiveInteger(config.maxProjects, "maxProjects", defaults.maxProjects),
|
|
68
|
-
maxFiles: positiveInteger(config.maxFiles, "maxFiles", defaults.maxFiles),
|
|
69
|
-
timeoutSeconds: positiveInteger(config.timeoutSeconds, "timeoutSeconds", defaults.timeoutSeconds)
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
async function validateInput(root, selected) {
|
|
73
|
-
const full = path2.resolve(root, selected);
|
|
74
|
-
const relative = path2.relative(root, full);
|
|
75
|
-
if (relative === ".." || relative.startsWith(`..${path2.sep}`) || path2.isAbsolute(relative))
|
|
76
|
-
throw new Error("C# scan input must stay inside the repository");
|
|
77
|
-
if (!/\.(?:csproj|slnx?)$/i.test(full) || !(await stat(full)).isFile())
|
|
78
|
-
throw new Error("C# input must be an existing .csproj, .sln, or .slnx file");
|
|
79
|
-
return full;
|
|
80
|
-
}
|
|
81
|
-
async function csharpInputs(root, settings) {
|
|
82
|
-
const config = parseCSharpSettings(settings);
|
|
83
|
-
if (config.input)
|
|
84
|
-
return [await validateInput(root, config.input)];
|
|
85
|
-
const files = await projectFiles(root, (file) => /\.(?:csproj|slnx?)$/i.test(file));
|
|
86
|
-
return [...files.filter((file) => /\.slnx?$/i.test(file)), ...files.filter((file) => /\.csproj$/i.test(file))].map((file) => path2.resolve(root, file));
|
|
87
|
-
}
|
|
5
|
+
import { createRequire } from "module";
|
|
88
6
|
|
|
89
7
|
// packages/scanner/src/values.ts
|
|
90
8
|
function object(value, label) {
|
|
@@ -240,6 +158,7 @@ function createScanObservation(input) {
|
|
|
240
158
|
roots,
|
|
241
159
|
files,
|
|
242
160
|
...sourceUnits(input.sourceUnits, filePaths),
|
|
161
|
+
...entryPoints(input.entryPoints, filePaths),
|
|
243
162
|
...operationEvidence(input, filePaths),
|
|
244
163
|
...httpEvidence(input.httpEndpoints, input.httpRequests, input.operations && new Set(input.operations.map((operation) => operation.id))),
|
|
245
164
|
diagnostics: [...new Map(input.diagnostics.map((diagnostic) => [
|
|
@@ -250,6 +169,29 @@ function createScanObservation(input) {
|
|
|
250
169
|
})
|
|
251
170
|
};
|
|
252
171
|
}
|
|
172
|
+
function entryPoints(input, paths) {
|
|
173
|
+
if (input === undefined)
|
|
174
|
+
return {};
|
|
175
|
+
const entries = array(input, "entryPoints").map((value) => {
|
|
176
|
+
const entry = object(value, "entry point");
|
|
177
|
+
const files = [...new Set(array(entry.files, "entry point.files").map((file) => string(file, "entry point.file")))].sort();
|
|
178
|
+
for (const file of files) {
|
|
179
|
+
if (!paths.has(file))
|
|
180
|
+
throw new Error(`entry point references unknown file: ${file}`);
|
|
181
|
+
}
|
|
182
|
+
const file = string(entry.file, "entry point.file");
|
|
183
|
+
if (!files.includes(file))
|
|
184
|
+
throw new Error(`entry point must include its own source file: ${file}`);
|
|
185
|
+
return {
|
|
186
|
+
file,
|
|
187
|
+
declaration: string(entry.declaration, "entry point.declaration"),
|
|
188
|
+
name: string(entry.name, "entry point.name"),
|
|
189
|
+
files
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
const key = (entry) => compare(entry.file, entry.declaration, entry.name, ...entry.files);
|
|
193
|
+
return { entryPoints: [...new Map(entries.map((entry) => [key(entry), entry])).values()].sort((a, b) => key(a).localeCompare(key(b))) };
|
|
194
|
+
}
|
|
253
195
|
function sourceUnits(input, paths) {
|
|
254
196
|
if (input === undefined)
|
|
255
197
|
return {};
|
|
@@ -312,6 +254,7 @@ function parseScanObservation(source) {
|
|
|
312
254
|
...parseOperations(value),
|
|
313
255
|
httpEndpoints: value.httpEndpoints,
|
|
314
256
|
httpRequests: value.httpRequests,
|
|
257
|
+
entryPoints: value.entryPoints,
|
|
315
258
|
...sourceUnits(value.sourceUnits, new Set(array(value.files, "files").map((entry) => string(object(entry, "file").file, "file.file")))),
|
|
316
259
|
roots: array(value.roots, "roots").map((entry) => {
|
|
317
260
|
const root = object(entry, "root");
|
|
@@ -456,52 +399,39 @@ function sourcePosition(position) {
|
|
|
456
399
|
return { position: Number(position) };
|
|
457
400
|
}
|
|
458
401
|
|
|
459
|
-
// plugins/scanners/
|
|
460
|
-
|
|
461
|
-
|
|
402
|
+
// plugins/scanners/csharp/src/config.ts
|
|
403
|
+
var defaults = { configuration: "Debug", maxProjects: 128, maxFiles: 20000, timeoutSeconds: 120 };
|
|
404
|
+
var configName = "csharp scanner settings";
|
|
405
|
+
function positiveInteger(value, key, fallback) {
|
|
406
|
+
if (value === undefined)
|
|
407
|
+
return fallback;
|
|
408
|
+
if (!Number.isSafeInteger(value) || Number(value) < 1)
|
|
409
|
+
throw new Error(`${configName}: ${key} must be a positive integer`);
|
|
410
|
+
return Number(value);
|
|
411
|
+
}
|
|
412
|
+
function optionalText(value, key) {
|
|
413
|
+
if (value === undefined)
|
|
462
414
|
return;
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
for (const { key, observation } of parts) {
|
|
472
|
-
const id = (value) => JSON.stringify([key, value]);
|
|
473
|
-
roots.push(...observation.roots.map((root) => ({ ...root, id: id(root.id), ...root.parent ? { parent: id(root.parent) } : {} })));
|
|
474
|
-
for (const source of observation.files) {
|
|
475
|
-
const combined = files.get(source.file) ?? { file: source.file, roots: [], symbols: [] };
|
|
476
|
-
combined.roots.push(...source.roots.map(id));
|
|
477
|
-
combined.symbols.push(...source.symbols.map((symbol) => ({ ...symbol, id: id(symbol.id) })));
|
|
478
|
-
files.set(source.file, combined);
|
|
479
|
-
}
|
|
480
|
-
operations.push(...(observation.operations ?? []).map((operation) => ({ ...operation, id: id(operation.id) })));
|
|
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) })));
|
|
484
|
-
diagnostics.push(...observation.diagnostics);
|
|
485
|
-
sourceUnits.push(...observation.sourceUnits ?? []);
|
|
415
|
+
if (typeof value !== "string" || !value.trim())
|
|
416
|
+
throw new Error(`${configName}: ${key} must be nonempty text`);
|
|
417
|
+
return value;
|
|
418
|
+
}
|
|
419
|
+
function parseCSharpSettings(config = {}) {
|
|
420
|
+
for (const key of Object.keys(config)) {
|
|
421
|
+
if (!["input", ...Object.keys(defaults)].includes(key))
|
|
422
|
+
throw new Error(`${configName}: unknown setting ${key}`);
|
|
486
423
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
...parts.some((part) => part.observation.httpRequests !== undefined) ? { httpRequests } : {}
|
|
497
|
-
});
|
|
424
|
+
const input = optionalText(config.input, "input");
|
|
425
|
+
const configuration = optionalText(config.configuration, "configuration");
|
|
426
|
+
return {
|
|
427
|
+
...input === undefined ? {} : { input },
|
|
428
|
+
configuration: configuration ?? defaults.configuration,
|
|
429
|
+
maxProjects: positiveInteger(config.maxProjects, "maxProjects", defaults.maxProjects),
|
|
430
|
+
maxFiles: positiveInteger(config.maxFiles, "maxFiles", defaults.maxFiles),
|
|
431
|
+
timeoutSeconds: positiveInteger(config.timeoutSeconds, "timeoutSeconds", defaults.timeoutSeconds)
|
|
432
|
+
};
|
|
498
433
|
}
|
|
499
434
|
|
|
500
|
-
// plugins/scanners/csharp/src/adapter.ts
|
|
501
|
-
import { access } from "fs/promises";
|
|
502
|
-
import path3 from "path";
|
|
503
|
-
import { createRequire } from "module";
|
|
504
|
-
|
|
505
435
|
// plugins/scanners/csharp/src/process.ts
|
|
506
436
|
import { spawn } from "child_process";
|
|
507
437
|
function stopTree(child) {
|
|
@@ -573,47 +503,44 @@ function run(command, args, options) {
|
|
|
573
503
|
}
|
|
574
504
|
|
|
575
505
|
// plugins/scanners/csharp/src/adapter.ts
|
|
576
|
-
async function requireWorker(
|
|
506
|
+
async function requireWorker() {
|
|
577
507
|
try {
|
|
578
|
-
const manifest =
|
|
579
|
-
const worker =
|
|
508
|
+
const manifest = createRequire(import.meta.url).resolve(`@groma/scanner-csharp-${process.platform}-${process.arch}/package.json`);
|
|
509
|
+
const worker = path.join(path.dirname(manifest), "worker", `Groma.CSharpScanner${process.platform === "win32" ? ".exe" : ""}`);
|
|
580
510
|
await access(worker);
|
|
581
511
|
return worker;
|
|
582
512
|
} catch {
|
|
583
513
|
throw new Error("C# runtime package is missing. Reinstall the scanner with optional dependencies enabled; contributors run bun scripts/package-csharp-scanner.ts.");
|
|
584
514
|
}
|
|
585
515
|
}
|
|
586
|
-
|
|
587
|
-
const
|
|
516
|
+
function csharpInventory(root, config, files) {
|
|
517
|
+
const projects = files.filter((file) => /\.(?:csproj|slnx?)$/i.test(file));
|
|
518
|
+
const input = config.input && path.relative(root, path.resolve(root, config.input)).split(path.sep).join("/");
|
|
519
|
+
const inputs = input ? projects.filter((file) => file === input) : projects;
|
|
520
|
+
return inputs.length ? { files, inputs } : undefined;
|
|
521
|
+
}
|
|
522
|
+
async function checkCSharpReadiness(repositoryRoot, settings, files) {
|
|
523
|
+
if (csharpInventory(path.resolve(repositoryRoot), parseCSharpSettings(settings), files) === undefined)
|
|
524
|
+
throw new Error("No C# project or solution was found. The C# scanner reads the SDK-style .csproj projects among its files and the .sln or .slnx files that list them, or only the one settings.input names.");
|
|
525
|
+
await requireWorker();
|
|
526
|
+
}
|
|
527
|
+
async function scanCSharpSource(repositoryRoot, settings, files) {
|
|
528
|
+
const root = path.resolve(repositoryRoot);
|
|
588
529
|
const config = parseCSharpSettings(settings);
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const
|
|
593
|
-
|
|
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, [
|
|
602
|
-
input,
|
|
603
|
-
"--root",
|
|
604
|
-
root,
|
|
605
|
-
"--configuration",
|
|
606
|
-
config.configuration,
|
|
607
|
-
"--max-projects",
|
|
608
|
-
String(config.maxProjects),
|
|
609
|
-
"--max-files",
|
|
610
|
-
String(config.maxFiles)
|
|
611
|
-
], { cwd: path3.dirname(input), timeoutSeconds: config.timeoutSeconds });
|
|
530
|
+
const inventory = csharpInventory(root, config, files);
|
|
531
|
+
if (inventory === undefined)
|
|
532
|
+
return;
|
|
533
|
+
const request = JSON.stringify({ root, ...inventory, configuration: config.configuration, maxProjects: config.maxProjects, maxFiles: config.maxFiles });
|
|
534
|
+
const { stdout } = await run(await requireWorker(), ["--scan"], { cwd: root, timeoutSeconds: config.timeoutSeconds, input: request });
|
|
612
535
|
return parseScanObservation(stdout);
|
|
613
536
|
}
|
|
537
|
+
async function listCSharpSources(repositoryRoot, settings, candidates) {
|
|
538
|
+
const inventory = csharpInventory(path.resolve(repositoryRoot), parseCSharpSettings(settings), candidates);
|
|
539
|
+
return inventory?.files.filter((file) => /\.cs$/i.test(file)) ?? [];
|
|
540
|
+
}
|
|
614
541
|
async function readCSharpOutline(repositoryRoot, references, settings = {}) {
|
|
615
542
|
const worker = await requireWorker();
|
|
616
|
-
const root =
|
|
543
|
+
const root = path.resolve(repositoryRoot);
|
|
617
544
|
const { stdout } = await run(worker, ["--outline"], {
|
|
618
545
|
cwd: root,
|
|
619
546
|
timeoutSeconds: parseCSharpSettings(settings).timeoutSeconds,
|
|
@@ -623,62 +550,14 @@ async function readCSharpOutline(repositoryRoot, references, settings = {}) {
|
|
|
623
550
|
}
|
|
624
551
|
|
|
625
552
|
// 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
|
-
}
|
|
632
553
|
var scanner = {
|
|
633
554
|
id: "csharp",
|
|
634
|
-
|
|
635
|
-
include: [
|
|
636
|
-
"**/*.cs",
|
|
637
|
-
"**/*.csproj",
|
|
638
|
-
"**/*.sln",
|
|
639
|
-
"**/*.slnx",
|
|
640
|
-
"**/*.props",
|
|
641
|
-
"**/*.targets",
|
|
642
|
-
"**/global.json",
|
|
643
|
-
"**/nuget.config",
|
|
644
|
-
"**/packages.lock.json"
|
|
645
|
-
].map((pattern) => pattern.replace(/[a-z]/g, (letter) => `[${letter}${letter.toUpperCase()}]`)),
|
|
646
|
-
exclude: ["**/[bB][iI][nN]/**", "**/[oO][bB][jJ]/**"]
|
|
647
|
-
},
|
|
648
|
-
checkReadiness: async (root, settings = {}) => {
|
|
649
|
-
const inputs = await csharpInputs(root, settings);
|
|
650
|
-
if (!inputs.length)
|
|
651
|
-
throw new Error("No C# project or solution was found.");
|
|
652
|
-
for (const input of inputs)
|
|
653
|
-
await checkCSharpReadiness(root, { ...settings, input });
|
|
654
|
-
},
|
|
555
|
+
checkReadiness: checkCSharpReadiness,
|
|
655
556
|
readCodeStructure: readCSharpOutline,
|
|
656
|
-
listSourceFiles:
|
|
657
|
-
|
|
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
|
-
},
|
|
665
|
-
scan: async (root, settings = {}) => {
|
|
666
|
-
const parts = [];
|
|
667
|
-
const covered = new Set;
|
|
668
|
-
for (const input of await csharpInputs(root, settings)) {
|
|
669
|
-
if (covered.has(input))
|
|
670
|
-
continue;
|
|
671
|
-
const observation = await scanCSharpSource(root, { ...settings, input });
|
|
672
|
-
for (const project of observation.roots)
|
|
673
|
-
if (project.file)
|
|
674
|
-
covered.add(path4.resolve(root, project.file));
|
|
675
|
-
parts.push({ key: path4.relative(root, input).split(path4.sep).join("/"), observation });
|
|
676
|
-
}
|
|
677
|
-
return combineObservations(parts);
|
|
678
|
-
}
|
|
557
|
+
listSourceFiles: listCSharpSources,
|
|
558
|
+
scan: scanCSharpSource
|
|
679
559
|
};
|
|
680
560
|
var src_default = scanner;
|
|
681
561
|
export {
|
|
682
|
-
checkCSharpReadiness,
|
|
683
562
|
src_default as default
|
|
684
563
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groma/scanner-csharp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/MrLesk/Groma.md.git"
|
|
10
10
|
},
|
|
11
|
-
"description": "C# project evidence through Roslyn
|
|
11
|
+
"description": "C# project evidence through Roslyn.",
|
|
12
12
|
"publishConfig": {
|
|
13
13
|
"access": "public"
|
|
14
14
|
},
|
|
@@ -28,16 +28,36 @@
|
|
|
28
28
|
"x64"
|
|
29
29
|
],
|
|
30
30
|
"optionalDependencies": {
|
|
31
|
-
"@groma/scanner-csharp-darwin-arm64": "0.
|
|
32
|
-
"@groma/scanner-csharp-linux-arm64": "0.
|
|
33
|
-
"@groma/scanner-csharp-linux-x64": "0.
|
|
34
|
-
"@groma/scanner-csharp-win32-arm64": "0.
|
|
35
|
-
"@groma/scanner-csharp-win32-x64": "0.
|
|
31
|
+
"@groma/scanner-csharp-darwin-arm64": "0.2.0",
|
|
32
|
+
"@groma/scanner-csharp-linux-arm64": "0.2.0",
|
|
33
|
+
"@groma/scanner-csharp-linux-x64": "0.2.0",
|
|
34
|
+
"@groma/scanner-csharp-win32-arm64": "0.2.0",
|
|
35
|
+
"@groma/scanner-csharp-win32-x64": "0.2.0"
|
|
36
36
|
},
|
|
37
37
|
"groma": {
|
|
38
38
|
"scanner": {
|
|
39
39
|
"id": "csharp",
|
|
40
40
|
"entry": "./dist/index.js",
|
|
41
|
+
"include": [
|
|
42
|
+
"**/*.[cC][sS]",
|
|
43
|
+
"**/*.[cC][sS][pP][rR][oO][jJ]",
|
|
44
|
+
"**/*.[sS][lL][nN]",
|
|
45
|
+
"**/*.[sS][lL][nN][xX]",
|
|
46
|
+
"**/*.[pP][rR][oO][pP][sS]",
|
|
47
|
+
"**/*.[tT][aA][rR][gG][eE][tT][sS]"
|
|
48
|
+
],
|
|
49
|
+
"exclude": [
|
|
50
|
+
"[bB][iI][nN]/",
|
|
51
|
+
"[oO][bB][jJ]/",
|
|
52
|
+
"[tT]est/",
|
|
53
|
+
"[tT]ests/",
|
|
54
|
+
"*Tests/",
|
|
55
|
+
"*.Test/",
|
|
56
|
+
"*.[dD]esigner.cs",
|
|
57
|
+
"*.g.cs",
|
|
58
|
+
"*.g.i.cs",
|
|
59
|
+
"*.generated.cs"
|
|
60
|
+
],
|
|
41
61
|
"discovery": {
|
|
42
62
|
"technologies": [
|
|
43
63
|
"csharp"
|
|
@@ -58,7 +78,7 @@
|
|
|
58
78
|
}
|
|
59
79
|
],
|
|
60
80
|
"compatibility": {
|
|
61
|
-
"groma": ">=0.
|
|
81
|
+
"groma": ">=0.5.0"
|
|
62
82
|
}
|
|
63
83
|
}
|
|
64
84
|
}
|