@groma/scanner-csharp 0.1.2 → 0.1.4
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 +229 -55
- package/dist/index.js +195 -47
- package/package.json +25 -9
- package/dist/worker/BuildHost-net472/Microsoft.Bcl.AsyncInterfaces.dll +0 -0
- package/dist/worker/BuildHost-net472/Microsoft.Build.Locator.dll +0 -0
- package/dist/worker/BuildHost-net472/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.exe +0 -0
- package/dist/worker/BuildHost-net472/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.exe.config +0 -68
- package/dist/worker/BuildHost-net472/Microsoft.CodeAnalysis.Workspaces.MSBuild.Contracts.dll +0 -0
- package/dist/worker/BuildHost-net472/Microsoft.CodeAnalysis.Workspaces.MSBuild.Contracts.pdb +0 -0
- package/dist/worker/BuildHost-net472/Microsoft.IO.Redist.dll +0 -0
- package/dist/worker/BuildHost-net472/System.Buffers.dll +0 -0
- package/dist/worker/BuildHost-net472/System.IO.Pipelines.dll +0 -0
- package/dist/worker/BuildHost-net472/System.Memory.dll +0 -0
- package/dist/worker/BuildHost-net472/System.Numerics.Vectors.dll +0 -0
- package/dist/worker/BuildHost-net472/System.Runtime.CompilerServices.Unsafe.dll +0 -0
- package/dist/worker/BuildHost-net472/System.Text.Encodings.Web.dll +0 -0
- package/dist/worker/BuildHost-net472/System.Text.Json.dll +0 -0
- package/dist/worker/BuildHost-net472/System.Threading.Tasks.Extensions.dll +0 -0
- package/dist/worker/BuildHost-netcore/Microsoft.Build.Locator.dll +0 -0
- package/dist/worker/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.deps.json +0 -96
- package/dist/worker/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll +0 -0
- package/dist/worker/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.runtimeconfig.json +0 -14
- package/dist/worker/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.Contracts.dll +0 -0
- package/dist/worker/BuildHost-netcore/Microsoft.CodeAnalysis.Workspaces.MSBuild.Contracts.pdb +0 -0
- package/dist/worker/Groma.CSharpScanner.deps.json +0 -594
- package/dist/worker/Groma.CSharpScanner.dll +0 -0
- package/dist/worker/Groma.CSharpScanner.pdb +0 -0
- package/dist/worker/Groma.CSharpScanner.runtimeconfig.json +0 -13
- package/dist/worker/Humanizer.dll +0 -0
- package/dist/worker/Microsoft.Build.Locator.dll +0 -0
- package/dist/worker/Microsoft.CodeAnalysis.CSharp.Workspaces.dll +0 -0
- package/dist/worker/Microsoft.CodeAnalysis.CSharp.dll +0 -0
- package/dist/worker/Microsoft.CodeAnalysis.ExternalAccess.RazorCompiler.dll +0 -0
- package/dist/worker/Microsoft.CodeAnalysis.Workspaces.MSBuild.Contracts.dll +0 -0
- package/dist/worker/Microsoft.CodeAnalysis.Workspaces.MSBuild.dll +0 -0
- package/dist/worker/Microsoft.CodeAnalysis.Workspaces.dll +0 -0
- package/dist/worker/Microsoft.CodeAnalysis.dll +0 -0
- package/dist/worker/Microsoft.Extensions.DependencyInjection.Abstractions.dll +0 -0
- package/dist/worker/Microsoft.Extensions.DependencyInjection.dll +0 -0
- package/dist/worker/Microsoft.Extensions.Logging.Abstractions.dll +0 -0
- package/dist/worker/Microsoft.Extensions.Logging.dll +0 -0
- package/dist/worker/Microsoft.Extensions.Options.dll +0 -0
- package/dist/worker/Microsoft.Extensions.Primitives.dll +0 -0
- package/dist/worker/Microsoft.VisualStudio.SolutionPersistence.dll +0 -0
- package/dist/worker/System.Composition.AttributedModel.dll +0 -0
- package/dist/worker/System.Composition.Convention.dll +0 -0
- package/dist/worker/System.Composition.Hosting.dll +0 -0
- package/dist/worker/System.Composition.Runtime.dll +0 -0
- package/dist/worker/System.Composition.TypedParts.dll +0 -0
- package/dist/worker/cs/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
- package/dist/worker/cs/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/worker/cs/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
- package/dist/worker/cs/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
- package/dist/worker/cs/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/worker/de/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
- package/dist/worker/de/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/worker/de/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
- package/dist/worker/de/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
- package/dist/worker/de/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/worker/es/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
- package/dist/worker/es/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/worker/es/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
- package/dist/worker/es/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
- package/dist/worker/es/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/worker/fr/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
- package/dist/worker/fr/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/worker/fr/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
- package/dist/worker/fr/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
- package/dist/worker/fr/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/worker/it/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
- package/dist/worker/it/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/worker/it/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
- package/dist/worker/it/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
- package/dist/worker/it/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/worker/ja/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
- package/dist/worker/ja/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/worker/ja/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
- package/dist/worker/ja/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
- package/dist/worker/ja/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/worker/ko/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
- package/dist/worker/ko/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/worker/ko/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
- package/dist/worker/ko/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
- package/dist/worker/ko/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/worker/pl/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
- package/dist/worker/pl/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/worker/pl/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
- package/dist/worker/pl/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
- package/dist/worker/pl/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/worker/pt-BR/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
- package/dist/worker/pt-BR/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/worker/pt-BR/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
- package/dist/worker/pt-BR/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
- package/dist/worker/pt-BR/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/worker/ru/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
- package/dist/worker/ru/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/worker/ru/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
- package/dist/worker/ru/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
- package/dist/worker/ru/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/worker/tr/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
- package/dist/worker/tr/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/worker/tr/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
- package/dist/worker/tr/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
- package/dist/worker/tr/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/worker/zh-Hans/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
- package/dist/worker/zh-Hans/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/worker/zh-Hans/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
- package/dist/worker/zh-Hans/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
- package/dist/worker/zh-Hans/Microsoft.CodeAnalysis.resources.dll +0 -0
- package/dist/worker/zh-Hant/Microsoft.CodeAnalysis.CSharp.Workspaces.resources.dll +0 -0
- package/dist/worker/zh-Hant/Microsoft.CodeAnalysis.CSharp.resources.dll +0 -0
- package/dist/worker/zh-Hant/Microsoft.CodeAnalysis.Workspaces.MSBuild.resources.dll +0 -0
- package/dist/worker/zh-Hant/Microsoft.CodeAnalysis.Workspaces.resources.dll +0 -0
- 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
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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 inside the repository has one inventory record. Generated `bin` and `obj` sources are excluded. SDK and package source files outside the repository remain compiler context, including validation, but do not produce file or operation evidence. Project references must stay inside the repository. 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. A failed scanner preserves its saved evidence; other scanners can still update the 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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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) &&
|
|
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 {
|
|
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: ["
|
|
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
|
-
|
|
446
|
-
async function requireWorker(file = workerDll) {
|
|
576
|
+
async function requireWorker(file) {
|
|
447
577
|
try {
|
|
448
|
-
|
|
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#
|
|
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,
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const
|
|
468
|
-
|
|
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;
|