Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2d097a1
Separated paket.lock handling from NuGetComponentDetector to PaketCom…
Thorium Oct 29, 2025
40a3e72
Merge branch 'main' into paket-component-detector
Thorium Mar 24, 2026
1059a11
Addressed to the code-review feedback of FernandoRojo
Thorium Mar 24, 2026
7e06225
Copilot suggestion commit
Thorium Mar 24, 2026
2b73aa3
New CoPilot feedback addressed as well
Thorium Mar 24, 2026
7595d73
Merge branch 'paket-component-detector' of https://github.com/Thorium…
Thorium Mar 24, 2026
7b72d3f
Add isDevelopmentDependency detection for Paket.
Thorium Mar 25, 2026
db38ced
Merge branch 'main' into paket-component-detector
Thorium Mar 25, 2026
1a14bee
Merge branch 'microsoft:main' into paket-component-detector
Thorium Apr 28, 2026
f7434cd
Merge branch 'microsoft:main' into paket-component-detector
Thorium Jun 19, 2026
260879f
Implemented latest Copilot feedback
Thorium Jun 19, 2026
bff2468
Merge branch 'microsoft:main' into paket-component-detector
Thorium Jul 8, 2026
5791dab
Potential fix for pull request finding
Thorium Jul 8, 2026
02fd413
Copilot comments addressed
Thorium Jul 8, 2026
4e537e5
Potential fix for pull request finding
Thorium Jul 8, 2026
5e3e843
Potential fix for pull request finding
Thorium Jul 8, 2026
30fe03a
Copilot reporting new comments one-by-one. Here is the change for the…
Thorium Jul 8, 2026
fd2a532
Potential fix for pull request finding
Thorium Jul 8, 2026
23cb5bf
Copilot wants these
Thorium Jul 8, 2026
0d4a84d
Potential fix for pull request finding
Thorium Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/detectors/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@
| NuGetProjectModelProjectCentricComponentDetector | Stable |
| MSBuildBinaryLogComponentDetector | Experimental |

- [Paket](paket.md)

| Detector | Status |
| --------------------- | ---------- |
| PaketComponentDetector | DefaultOff |

- [Pip](pip.md)

| Detector | Status |
Expand Down
92 changes: 92 additions & 0 deletions docs/detectors/paket.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Paket Detection

## Requirements

Paket Detection depends on the following to successfully run:

- One or more `paket.lock` files.
- The Paket detector looks for [`paket.lock`][1] files.

[1]: ../../src/Microsoft.ComponentDetection.Detectors/paket/PaketComponentDetector.cs

## Detection Strategy

Paket Detection is performed by parsing any `paket.lock` files found under the scan directory.

The `paket.lock` file is a lock file that records the concrete dependency resolution of all direct and transitive dependencies of your project. It is generated by [Paket][2], an alternative dependency manager for .NET that is popular in both large-scale C# projects and small-scale F# projects.

[2]: https://fsprojects.github.io/Paket/

## What is Paket?

Paket is a dependency manager for .NET and Mono projects that provides:
- Precise control over package dependencies
- Reproducible builds through lock files
- Support for multiple package sources (NuGet, GitHub, HTTP, Git)
- Better resolution algorithm compared to legacy NuGet

The `paket.lock` file structure is straightforward and human-readable:
```
NUGET
remote: https://api.nuget.org/v3/index.json
PackageName (1.0.0)
DependencyName (>= 2.0.0)

GROUP Test
NUGET
remote: https://api.nuget.org/v3/index.json
NUnit (4.3.2)
```

## Enabling the Detector

This detector is currently **DefaultOff** and must be explicitly enabled by passing `--DetectorArgs Paket=EnableIfDefaultOff` (see [enabling default off detectors](../enable-default-off.md)).

When the Paket detector is enabled, the NuGet detector automatically skips `paket.lock` files so the same file is not processed twice; when it is not enabled, the NuGet detector continues to handle `paket.lock` with its legacy parser.

## Paket Detector

The Paket detector parses `paket.lock` files to extract:
- Resolved package names and versions recorded in the lock file
- Dependency relationships between packages as represented in the lock file
- Development dependency classification based on Paket group names

When a companion `paket.dependencies` file is present next to `paket.lock`, the detector uses it to identify explicitly declared NuGet packages; otherwise it falls back to a lock-graph heuristic (packages that appear as dependencies of other packages are treated as transitive).

Currently, the detector focuses on the `NUGET` section of the lock file, which contains NuGet package dependencies. Other dependency types (GITHUB, HTTP, GIT) are not currently supported.

## How It Works

The detector:
1. Locates `paket.lock` files in the scan directory
2. Parses the file line by line, tracking the current GROUP context
3. Identifies packages (4-space indentation) and their versions, keyed by group
4. Identifies dependencies (6-space indentation) and their version constraints
5. Records all packages as NuGet components
6. Establishes parent-child relationships between packages and their dependencies
7. Classifies packages as development dependencies based on their group name
8. Classifies packages as direct (explicitly referenced) using the declarations in the companion `paket.dependencies` file when present, falling back to the lock-graph heuristic otherwise

## Development Dependency Classification

Paket organizes dependencies into groups within `paket.lock`. The detector uses group names to classify packages as development (`isDevelopmentDependency: true`) or production (`isDevelopmentDependency: false`) dependencies.

**Well-known development groups** (case-insensitive):
- Exact matches: `Test`, `Tests`, `Docs`, `Documentation`, `Build`, `Analyzers`, `Fake`, `Benchmark`, `Benchmarks`, `Samples`, `DesignTime`
- Suffix matches: any group name ending with `Test` or `Tests` (e.g., `UnitTest`, `IntegrationTests`, `AcceptanceTests`, `E2ETest`)

**Production groups**:
- The default/unnamed group (packages before any `GROUP` line)
- `Main`
- Any group name not matching the well-known patterns above (e.g., `Server`, `Client`, `Shared`)

When the same package appears in multiple groups (e.g., `FSharp.Core` in both `Build` and `Server`), both occurrences are registered. The framework's merge logic ensures that if a package appears in **any** production group, it is ultimately classified as a production dependency.

## Known Limitations

- This detector is currently **DefaultOff** and must be explicitly enabled with `--DetectorArgs Paket=EnableIfDefaultOff` (see [Enabling the Detector](#enabling-the-detector))
- Only NuGet dependencies from the `NUGET` section are detected
- GitHub, HTTP, and Git dependencies are not currently supported
- Direct vs. transitive classification is authoritative only when a readable `paket.dependencies` file is present next to `paket.lock`; when it is missing or unreadable, the detector falls back to approximating this from the dependency graph within the lock file, which cannot reliably distinguish a direct dependency that is also pulled in transitively
- Development dependency classification is based on group names only; it does not cross-reference `paket.references` files to verify which packages are actually used by test vs. production projects (planned for a future iteration)
- The detector assumes the lock file format follows the standard Paket conventions
Comment thread
Thorium marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,50 @@ protected override async Task OnFileFoundAsync(ProcessRequest processRequest, ID
var stream = processRequest.ComponentStream;
var ignoreNugetConfig = detectorArgs.TryGetValue("NuGet.IncludeRepositoryPaths", out var includeRepositoryPathsValue) && includeRepositoryPathsValue.Equals(bool.FalseString, StringComparison.OrdinalIgnoreCase);

var paketDetectorEnabled = IsPaketDetectorEnabled(detectorArgs);

if (NugetConfigFileName.Equals(stream.Pattern, StringComparison.OrdinalIgnoreCase))
{
await this.ProcessAdditionalDirectoryAsync(processRequest, ignoreNugetConfig);
await this.ProcessAdditionalDirectoryAsync(processRequest, ignoreNugetConfig, paketDetectorEnabled);
}
else if ("paket.lock".Equals(stream.Pattern, StringComparison.OrdinalIgnoreCase) && paketDetectorEnabled)
{
// The dedicated Paket detector is enabled and will process this file, so skip it here
// to avoid double-processing the same paket.lock with the legacy parser below.
this.Logger.LogDebug("Skipping paket.lock at {Location} because the Paket detector is enabled and will process it.", stream.Location);
}
else
{
await this.ProcessFileAsync(processRequest);
}
}

private async Task ProcessAdditionalDirectoryAsync(ProcessRequest processRequest, bool ignoreNugetConfig)
/// <summary>
/// Determines whether the dedicated Paket detector has been explicitly enabled via detector args
/// (e.g. <c>--DetectorArgs Paket=EnableIfDefaultOff</c>). When enabled, the NuGet detector defers
/// paket.lock handling to it; otherwise the NuGet detector parses paket.lock with its legacy parser.
/// </summary>
private static bool IsPaketDetectorEnabled(IDictionary<string, string> detectorArgs)
{
if (detectorArgs == null)
{
return false;
}

// detectorArgs is a case-sensitive dictionary, but detector enablement is evaluated
// case-insensitively (ScanExecutionService builds ExplicitlyEnabledDetectorIds with OrdinalIgnoreCase).
// Match the key case-insensitively so a lowercase override such as
// `--DetectorArgs paket=EnableIfDefaultOff` still causes NuGet to skip paket.lock and avoid double-processing.
//
var value = detectorArgs
.FirstOrDefault(kvp => string.Equals(kvp.Key, Microsoft.ComponentDetection.Detectors.Paket.PaketComponentDetector.DetectorId, StringComparison.OrdinalIgnoreCase))
.Value;

return string.Equals(value, "EnableIfDefaultOff", StringComparison.OrdinalIgnoreCase)
|| string.Equals(value, "Enable", StringComparison.OrdinalIgnoreCase);
}

private async Task ProcessAdditionalDirectoryAsync(ProcessRequest processRequest, bool ignoreNugetConfig, bool paketDetectorEnabled)
{
var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder;
var stream = processRequest.ComponentStream;
Expand All @@ -70,6 +103,12 @@ private async Task ProcessAdditionalDirectoryAsync(ProcessRequest processRequest
var additionalPaths = this.GetRepositoryPathsFromNugetConfig(stream);
var rootPath = new Uri(this.CurrentScanRequest.SourceDirectory.FullName + Path.DirectorySeparatorChar);

// Mirror OnFileFoundAsync: when the Paket detector is enabled it owns paket.lock, so exclude it
// from the additional-directory scan to avoid double-processing the same file with the legacy parser.
var searchPatterns = this.SearchPatterns.Where(sp =>
!NugetConfigFileName.Equals(sp)
&& !(paketDetectorEnabled && "paket.lock".Equals(sp, StringComparison.OrdinalIgnoreCase)));

foreach (var additionalPath in additionalPaths)
{
// Only paths outside of our sourceDirectory need to be added
Expand All @@ -80,7 +119,7 @@ private async Task ProcessAdditionalDirectoryAsync(ProcessRequest processRequest

this.Scanner.Initialize(additionalPath, (name, directoryName) => false, 1);

await this.Scanner.GetFilteredComponentStreamObservable(additionalPath, this.SearchPatterns.Where(sp => !NugetConfigFileName.Equals(sp)), singleFileComponentRecorder.GetParentComponentRecorder())
await this.Scanner.GetFilteredComponentStreamObservable(additionalPath, searchPatterns, singleFileComponentRecorder.GetParentComponentRecorder())
.ForEachAsync(async fi => await this.ProcessFileAsync(fi));
}
}
Expand Down
Loading