Skip to content

Maven combined detector experiment#1628

Open
zhenghao104 wants to merge 7 commits intomainfrom
users/zhentan/maven-combined-detector-experiment
Open

Maven combined detector experiment#1628
zhenghao104 wants to merge 7 commits intomainfrom
users/zhentan/maven-combined-detector-experiment

Conversation

@zhenghao104
Copy link
Contributor

@zhenghao104 zhenghao104 commented Feb 3, 2026

MavenWithFallbackDetector

Overview

The MavenWithFallbackDetector is an experimental Maven detector that combines Maven CLI detection with static pom.xml parsing fallback. It provides resilient Maven dependency detection even when the Maven CLI fails (e.g., due to authentication errors with private repositories).

Key Features

  • Primary detection: Uses Maven CLI (mvn dependency:tree) for full transitive dependency resolution
  • Automatic fallback: Falls back to static pom.xml parsing when Maven CLI fails
  • Partial failure handling: Can use Maven CLI results for successful projects while falling back to static parsing for failed ones
  • Authentication error detection: Identifies and reports authentication failures with guidance
  • Nested pom.xml filtering: Optimizes multi-module projects by only processing root pom.xml files with Maven CLI
  • Telemetry: Reports detection method, fallback reasons, and component counts

Detection Flow

┌─────────────────────────────────────────────────────────────────────────┐
│                        OnPrepareDetectionAsync                          │
└─────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
                    ┌───────────────────────────────┐
                    │ Check CD_USE_MAVEN_FALLBACK_  │
                    │ DETECTOR environment variable │
                    └───────────────────────────────┘
                                    │
                    ┌───────────────┴───────────────┐
                    │                               │
                    ▼                               ▼
            [Not set/false]                     [true]
                    │                               │
                    ▼                               ▼
        ┌───────────────────┐           ┌───────────────────────┐
        │ Use Static Parser │           │ Check CD_MAVEN_       │
        │ for ALL pom.xml   │           │ DISABLE_CLI env var   │
        │ (avoid race cond.)│           └───────────────────────┘
        └───────────────────┘                       │
                                    ┌───────────────┴───────────────┐
                                    │                               │
                                    ▼                               ▼
                            [Disabled=true]                 [false/unset]
                                    │                               │
                                    ▼                               ▼
                        ┌───────────────────┐       ┌───────────────────────┐
                        │ Use Static Parser │       │ Check Maven CLI exists │
                        │ for ALL pom.xml   │       └───────────────────────┘
                        └───────────────────┘                   │
                                                ┌───────────────┴───────────────┐
                                                │                               │
                                                ▼                               ▼
                                        [CLI not found]                 [CLI available]
                                                │                               │
                                                ▼                               ▼
                                    ┌───────────────────┐       ┌───────────────────────────┐
                                    │ Use Static Parser │       │ RemoveNestedPomXmls       │
                                    │ for ALL pom.xml   │       │ (filter to root poms only)│
                                    └───────────────────┘       └───────────────────────────┘
                                                                            │
                                                                            ▼
                                                            ┌───────────────────────────────┐
                                                            │ Run mvn dependency:tree       │
                                                            │ for each root pom.xml         │
                                                            └───────────────────────────────┘
                                                                            │
                                                                            ▼
                                                            ┌───────────────────────────────┐
                                                            │ Scan for bcde-fallback.mvndeps│
                                                            │ files (generated by Maven CLI)│
                                                            └───────────────────────────────┘
                                                                            │
                                                                            ▼
                                                            ┌───────────────────────────────┐
                                                            │ Compare: which pom.xml        │
                                                            │ directories have              │
                                                            │ bcde-fallback.mvndeps?        │
                                                            └───────────────────────────────┘
                                                                            │
                                    ┌───────────────────────┬───────────────┴───────────────┐
                                    │                       │                               │
                                    ▼                       ▼                               ▼
                            [All succeeded]         [All failed]                    [Partial]
                                    │                       │                               │
                                    ▼                       ▼                               ▼
                        ┌─────────────────┐     ┌─────────────────────┐     ┌─────────────────────┐
                        │ Return          │     │ AnalyzeMvnCliFailure│     │ AnalyzeMvnCliFailure│
                        │ bcde-fallback.  │     │ Return ALL pom.xml  │     │ Return bcde-fallback│
                        │ mvndeps only    │     │ for static parsing  │     │ .mvndeps + failed   │
                        │                 │     │ (nested restored)   │     │ pom.xml for static  │
                        │ Method: MvnCli  │     │ Method: StaticOnly  │     │ Method: Mixed       │
                        └─────────────────┘     └─────────────────────┘     └─────────────────────┘

Core Components

1. Environment Variable Control

internal const string DisableMvnCliEnvVar = "CD_MAVEN_DISABLE_CLI";
internal const string UseFallbackDetectorEnvVar = "CD_USE_MAVEN_FALLBACK_DETECTOR";

CD_USE_MAVEN_FALLBACK_DETECTOR (New):

  • Set to true to fully enable this detector and disable MvnCliComponentDetector
  • When not set, this detector runs in static-only mode to avoid race conditions with MvnCliComponentDetector
  • Required for testing the full Maven CLI functionality of this detector

CD_MAVEN_DISABLE_CLI:

  • Set to true to bypass Maven CLI entirely and use only static parsing
  • Only applies when CD_USE_MAVEN_FALLBACK_DETECTOR=true

Useful when:

  • Maven CLI is known to fail in the environment
  • Faster scans are preferred (at the cost of transitive dependencies)
  • Debugging static parser behavior

2. RemoveNestedPomXmls

Filters pom.xml files to keep only root-level ones for Maven CLI processing.

Why? In multi-module Maven projects:

/project/
  pom.xml              ← ROOT (parent pom) - KEEP
  /module-a/
    pom.xml            ← NESTED (child) - SKIP
  /module-b/
    pom.xml            ← NESTED (child) - SKIP

Running mvn dependency:tree on the root pom automatically processes all child modules. Processing each nested pom separately would be redundant and slow.

Algorithm:

  1. For each pom.xml file, walk up the directory tree
  2. If a parent directory contains a pom.xml → skip this file (it's nested)
  3. If we reach the filesystem root without finding a parent pom.xml → keep this file (it's a root)

3. Maven CLI Execution

Uses IMavenCommandService.GenerateDependenciesFileAsync() to run:

mvn dependency:tree -B -DoutputFile=bcde-fallback.mvndeps -DoutputType=text -f<pom.xml>

This generates a bcde-fallback.mvndeps file containing the full dependency tree.

Note: The detector uses a different output filename (bcde-fallback.mvndeps) than the standard MvnCliComponentDetector (bcde.mvndeps) to avoid race conditions when both detectors run in parallel.

4. Failure Detection

After Maven CLI runs, the detector scans for bcde-fallback.mvndeps files:

  • Directory has bcde-fallback.mvndeps → Maven CLI succeeded for that pom.xml
  • Directory missing bcde-fallback.mvndeps → Maven CLI failed for that pom.xml

5. Static Parsing Fallback

When Maven CLI fails, static parsing extracts dependencies directly from pom.xml:

<dependencies>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.12.0</version>
    </dependency>
</dependencies>

Limitations of static parsing:

  • Only detects direct dependencies (no transitive)
  • Cannot resolve version ranges (e.g., [1.0,2.0))
  • May not resolve all property references (e.g., ${project.version})

6. Nested Pom Restoration

When Maven CLI fails (partial or complete), nested pom.xml files that were filtered out by RemoveNestedPomXmls are restored for static parsing.

Why? Maven CLI on the root pom would have processed nested modules. Since it failed, we need to parse each nested pom.xml individually.

Implementation:

  • GetAllPomFilesForStaticParsing() - Re-scans ALL pom.xml files (used for complete failure)
  • GetStaticParsingRequestsForFailedFiles() - Re-scans pom.xml files only under failed directories (used for partial failure)

7. Authentication Error Analysis

When Maven CLI fails, the detector analyzes error patterns:

private static readonly string[] AuthErrorPatterns =
[
    "401", "403", "Unauthorized", "Not authorized",
    "authentication", "Could not authenticate",
    "Access denied", "Forbidden",
    "status code: 401", "status code: 403",
];

If authentication errors are detected:

  1. Sets fallbackReason = MavenFallbackReason.AuthenticationFailure
  2. Extracts failed endpoint URLs from error messages
  3. Logs guidance for resolving authentication issues

Detection Methods

Method When Used What's Detected
MvnCliOnly Maven CLI succeeds for all pom.xml files Full transitive dependencies
StaticParserOnly Maven CLI disabled, unavailable, or fails completely Direct dependencies only
Mixed Maven CLI succeeds for some, fails for others Transitive (where CLI worked) + Direct (where CLI failed)

Fallback Reasons

Reason Description
None No fallback needed (MvnCli succeeded)
MvnCliDisabledByUser User set CD_MAVEN_DISABLE_CLI=true
MavenCliNotAvailable mvn command not found in PATH
AuthenticationFailure Maven CLI failed with 401/403 errors
OtherMvnCliFailure Maven CLI failed for other reasons

Telemetry

The detector records the following telemetry:

Key Description
DetectionMethod MvnCliOnly, StaticParserOnly, or Mixed
FallbackReason Why fallback occurred (if applicable)
MvnCliComponentCount Components detected via Maven CLI
StaticParserComponentCount Components detected via static parsing
TotalComponentCount Total components detected
MavenCliAvailable Whether Maven CLI was found
OriginalPomFileCount Number of root pom.xml files processed
FailedEndpoints URLs of repositories that had auth failures

Version Property Resolution

Static parsing supports resolving version properties:

<properties>
    <commons.version>3.12.0</commons.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>${commons.version}</version>  <!-- Resolved to 3.12.0 -->
    </dependency>
</dependencies>

The resolver:

  1. Checks <properties> section in current document
  2. Checks other loaded pom.xml documents (for multi-module projects)
  3. If unresolved, skips the dependency with a debug log

Usage

As Part of Component Detection

The detector is registered as an experimental detector and runs automatically when Component Detection scans a directory containing pom.xml files.

Enabling Full Detector Mode

By default, this detector runs in static-only mode when running alongside MvnCliComponentDetector to avoid race conditions. To fully enable the detector (including Maven CLI):

# Bash
export CD_USE_MAVEN_FALLBACK_DETECTOR=true

# PowerShell
$env:CD_USE_MAVEN_FALLBACK_DETECTOR = "true"

# Azure DevOps Pipeline
variables:
  CD_USE_MAVEN_FALLBACK_DETECTOR: 'true'

When CD_USE_MAVEN_FALLBACK_DETECTOR=true:

  • MavenWithFallbackDetector runs with full Maven CLI support
  • MvnCliComponentDetector is automatically disabled
  • No race conditions occur

Disabling Maven CLI (Static Parsing Only)

Set the environment variable to use only static parsing:

# Bash
export CD_USE_MAVEN_FALLBACK_DETECTOR=true
export CD_MAVEN_DISABLE_CLI=true

# PowerShell
$env:CD_USE_MAVEN_FALLBACK_DETECTOR = "true"
$env:CD_MAVEN_DISABLE_CLI = "true"

# Azure DevOps Pipeline
variables:
  CD_USE_MAVEN_FALLBACK_DETECTOR: 'true'
  CD_MAVEN_DISABLE_CLI: 'true'

Experiment Configuration

The detector is paired with MavenWithFallbackExperiment which compares results against the standard MvnCliComponentDetector:

  • Control group: MvnCliComponentDetector
  • Experiment group: MavenWithFallbackDetector

Enable experiments via:

export CD_DETECTOR_EXPERIMENTS=true

Unit Tests

The following unit tests are implemented in MavenWithFallbackDetectorTests.cs:

Maven CLI Scenarios

Test Description
WhenMavenCliNotAvailable_FallsBackToStaticParsing_Async Verifies fallback to static parsing when Maven CLI is not installed
WhenMavenCliNotAvailable_DetectsMultipleDependencies_Async Verifies static parsing detects multiple dependencies correctly
WhenMavenCliSucceeds_UsesMvnCliResults_Async Verifies Maven CLI results are used when available
WhenMavenCliSucceeds_PreservesTransitiveDependencies_Async Verifies transitive dependency relationships are preserved
WhenMavenCliProducesNoOutput_FallsBackToStaticParsing_Async Verifies fallback when Maven CLI runs but produces no output

Static Parser Edge Cases

Test Description
StaticParser_IgnoresDependenciesWithoutVersion_Async Verifies dependencies without version tags are skipped
StaticParser_IgnoresDependenciesWithVersionRanges_Async Verifies version ranges (e.g., [1.0,2.0)) are skipped
StaticParser_ResolvesPropertyVersions_Async Verifies ${property} versions are resolved from <properties>
StaticParser_IgnoresDependenciesWithUnresolvablePropertyVersions_Async Verifies unresolvable properties are skipped

Empty/Missing File Scenarios

Test Description
WhenNoPomXmlFiles_ReturnsSuccessWithNoComponents_Async Verifies graceful handling of no pom.xml files
WhenPomXmlHasNoDependencies_ReturnsSuccessWithNoComponents_Async Verifies handling of pom.xml with no dependencies

Environment Variable Control

Test Description
WhenUseFallbackDetectorNotSet_UsesStaticParsingOnly_Async Verifies detector uses static parsing only when CD_USE_MAVEN_FALLBACK_DETECTOR is not set (default behavior to avoid race conditions)
WhenUseFallbackDetectorTrue_AndDisableMvnCliTrue_UsesStaticParsing_Async Verifies CD_MAVEN_DISABLE_CLI=true bypasses Maven CLI when fallback detector is enabled
WhenDisableMvnCliEnvVarIsFalse_UsesMvnCliNormally_Async Verifies CD_MAVEN_DISABLE_CLI=false uses Maven CLI when fallback detector is enabled

Nested Pom.xml Handling

Test Description
WhenMvnCliSucceeds_NestedPomXmlsAreFilteredOut_Async Verifies nested pom.xml files are filtered when Maven CLI is used
WhenMvnCliFailsCompletely_AllNestedPomXmlsAreRestoredForStaticParsing_Async Verifies all nested pom.xml files are restored when Maven CLI fails completely
WhenMvnCliPartiallyFails_NestedPomXmlsRestoredOnlyForFailedDirectories_Async Verifies nested pom.xml files are restored only for failed directories in partial failure

Error Analysis and Telemetry

Test Description
WhenMvnCliFailsWithAuthError_LogsFailedEndpointAndSetsTelemetry_Async Verifies auth errors (401/403) are detected, failed endpoint URLs are extracted, and telemetry is set to AuthenticationFailure
WhenMvnCliFailsWithNonAuthError_SetsFallbackReasonToOther_Async Verifies non-auth errors set telemetry to OtherMvnCliFailure without extracting endpoints

File Structure

src/Microsoft.ComponentDetection.Detectors/maven/
├── MavenWithFallbackDetector.cs      # Main detector implementation
├── MavenWithFallbackDetector.md      # This documentation
├── MvnCliComponentDetector.cs        # Standard Maven CLI detector
├── MavenCommandService.cs            # Maven CLI execution service
├── IMavenCommandService.cs           # Service interface
└── ...

src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/
└── MavenWithFallbackExperiment.cs    # Experiment configuration

test/Microsoft.ComponentDetection.Detectors.Tests/
└── MavenWithFallbackDetectorTests.cs # Unit tests

@codecov
Copy link

codecov bot commented Feb 3, 2026

Codecov Report

❌ Patch coverage is 96.52837% with 41 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.6%. Comparing base (6748194) to head (7faa9cc).

Files with missing lines Patch % Lines
...ction.Detectors/maven/MavenWithFallbackDetector.cs 93.1% 23 Missing and 9 partials ⚠️
....Detectors.Tests/MavenWithFallbackDetectorTests.cs 99.2% 0 Missing and 5 partials ⚠️
...tection.Detectors/maven/MvnCliComponentDetector.cs 50.0% 3 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##            main   #1628     +/-   ##
=======================================
+ Coverage   90.4%   90.6%   +0.1%     
=======================================
  Files        441     445      +4     
  Lines      38314   39492   +1178     
  Branches    2347    2403     +56     
=======================================
+ Hits       34674   35810   +1136     
- Misses      3159    3185     +26     
- Partials     481     497     +16     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces MavenWithFallbackDetector, an experimental Maven detector that provides resilient dependency detection by combining Maven CLI execution with static pom.xml parsing fallback. The detector automatically falls back to static parsing when Maven CLI fails (e.g., due to authentication errors or missing CLI), ensuring components are still detected even in challenging build environments.

Changes:

  • Added MavenWithFallbackDetector with dual detection strategy (Maven CLI primary, static parsing fallback) and comprehensive telemetry
  • Registered new experiment configuration comparing the new detector against the standard MvnCliComponentDetector
  • Added 13 comprehensive unit tests covering CLI scenarios, static parser edge cases, environment variable control, and nested pom.xml handling

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
src/Microsoft.ComponentDetection.Detectors/maven/MavenWithFallbackDetector.cs Main detector implementation with Maven CLI detection, static XML parsing fallback, nested pom filtering, and telemetry tracking
src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/MavenWithFallbackExperiment.cs Experiment configuration to compare new detector against existing MvnCliComponentDetector
src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs Registered new detector and experiment in DI container
test/Microsoft.ComponentDetection.Detectors.Tests/MavenWithFallbackDetectorTests.cs Comprehensive test suite with 13 tests covering all detection scenarios and edge cases

Comment on lines +346 to +354
foreach (var error in this.mavenCliErrors)
{
var endpoints = this.ExtractFailedEndpoints(error);
foreach (var endpoint in endpoints)
{
this.failedEndpoints.Add(endpoint);
}
}

Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.

Suggested change
foreach (var error in this.mavenCliErrors)
{
var endpoints = this.ExtractFailedEndpoints(error);
foreach (var endpoint in endpoints)
{
this.failedEndpoints.Add(endpoint);
}
}
foreach (var endpoint in this.mavenCliErrors.SelectMany(this.ExtractFailedEndpoints))
{
this.failedEndpoints.Add(endpoint);
}

Copilot uses AI. Check for mistakes.
.Returns("false");

// Act
var (detectorResult, componentRecorder) = await this.DetectorTestUtility
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to componentRecorder is useless, since its value is never read.

Suggested change
var (detectorResult, componentRecorder) = await this.DetectorTestUtility
var (detectorResult, _) = await this.DetectorTestUtility

Copilot uses AI. Check for mistakes.
Comment on lines +497 to +507
string versionString;

if (versionRef.StartsWith("${"))
{
versionString = this.ResolveVersion(versionRef, document, file.Location);
}
else
{
versionString = versionRef;
}

Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.

Suggested change
string versionString;
if (versionRef.StartsWith("${"))
{
versionString = this.ResolveVersion(versionRef, document, file.Location);
}
else
{
versionString = versionRef;
}
var versionString = versionRef.StartsWith("${")
? this.ResolveVersion(versionRef, document, file.Location)
: versionRef;

Copilot uses AI. Check for mistakes.
@github-actions
Copy link

github-actions bot commented Feb 3, 2026

👋 Hi! It looks like you modified some files in the Detectors folder.
You may need to bump the detector versions if any of the following scenarios apply:

  • The detector detects more or fewer components than before
  • The detector generates different parent/child graph relationships than before
  • The detector generates different devDependencies values than before

If none of the above scenarios apply, feel free to ignore this comment 🙂

Copilot AI review requested due to automatic review settings February 4, 2026 00:17
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Comment on lines +137 to +138
private int mvnCliComponentCount;
private int staticParserComponentCount;
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mvnCliComponentCount and staticParserComponentCount fields are incremented without synchronization in ProcessMvnCliResult (line 499) and ProcessPomFileStatically (line 574). Since OnFileFoundAsync can be called concurrently (ActionBlock with MaxDegreeOfParallelism > 1), these increments are not thread-safe and can result in lost updates. Consider using Interlocked.Add for these operations or protecting them with a lock.

Copilot uses AI. Check for mistakes.
private readonly ConcurrentBag<ProcessRequest> originalPomFiles = [];

// Track Maven CLI errors for analysis
private readonly ConcurrentBag<string> mavenCliErrors = [];
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mavenCliErrors collection is declared but never populated with any error messages. The authentication error analysis in AnalyzeMvnCliFailure (line 377-403) will never detect authentication failures because there are no errors to analyze. The MavenCommandService logs errors but doesn't return them to the detector. Consider modifying the IMavenCommandService interface to return error information, or capture errors from the command line invocation service output.

Copilot uses AI. Check for mistakes.
Comment on lines +387 to +393
foreach (var error in this.mavenCliErrors)
{
var endpoints = this.ExtractFailedEndpoints(error);
foreach (var endpoint in endpoints)
{
this.failedEndpoints.Add(endpoint);
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.

Suggested change
foreach (var error in this.mavenCliErrors)
{
var endpoints = this.ExtractFailedEndpoints(error);
foreach (var endpoint in endpoints)
{
this.failedEndpoints.Add(endpoint);
}
foreach (var endpoint in this.mavenCliErrors.SelectMany(this.ExtractFailedEndpoints))
{
this.failedEndpoints.Add(endpoint);

Copilot uses AI. Check for mistakes.
.Returns("false");

// Act
var (detectorResult, componentRecorder) = await this.DetectorTestUtility
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to componentRecorder is useless, since its value is never read.

Suggested change
var (detectorResult, componentRecorder) = await this.DetectorTestUtility
var (detectorResult, _) = await this.DetectorTestUtility

Copilot uses AI. Check for mistakes.
Comment on lines +538 to +548
string versionString;

if (versionRef.StartsWith("${"))
{
versionString = this.ResolveVersion(versionRef, document, file.Location);
}
else
{
versionString = versionRef;
}

Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.

Suggested change
string versionString;
if (versionRef.StartsWith("${"))
{
versionString = this.ResolveVersion(versionRef, document, file.Location);
}
else
{
versionString = versionRef;
}
var versionString = versionRef.StartsWith("${")
? this.ResolveVersion(versionRef, document, file.Location)
: versionRef;

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings February 4, 2026 01:27
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 8 comments.

Comment on lines +422 to +436
{
using var reader = new StreamReader(componentStream.Stream);
var content = reader.ReadToEnd();
return new ProcessRequest
{
ComponentStream = new ComponentStream
{
Stream = new MemoryStream(Encoding.UTF8.GetBytes(content)),
Location = componentStream.Location,
Pattern = MavenManifest,
},
SingleFileComponentRecorder = this.ComponentRecorder.CreateSingleFileComponentRecorder(componentStream.Location),
};
})
.ToObservable();
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stream is disposed prematurely by the 'using' statement. The ComponentStream.Stream is read in the 'using' block, then the content is passed to a new MemoryStream in the returned ProcessRequest. However, in OnFileFoundAsync, the ProcessRequest.ComponentStream.Stream is accessed again to process the file (line 518 loads document from file.Stream). At that point, the original stream has been disposed. This will cause ObjectDisposedException when the detector tries to process these streams.

Copilot uses AI. Check for mistakes.
Comment on lines +471 to +485
{
using var reader = new StreamReader(componentStream.Stream);
var content = reader.ReadToEnd();
return new ProcessRequest
{
ComponentStream = new ComponentStream
{
Stream = new MemoryStream(Encoding.UTF8.GetBytes(content)),
Location = componentStream.Location,
Pattern = MavenManifest,
},
SingleFileComponentRecorder = this.ComponentRecorder.CreateSingleFileComponentRecorder(componentStream.Location),
};
})
.ToList();
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stream is disposed prematurely by the 'using' statement. The ComponentStream.Stream is read in the 'using' block, then the content is passed to a new MemoryStream in the returned ProcessRequest. However, in OnFileFoundAsync, the ProcessRequest.ComponentStream.Stream is accessed again to process the file (line 518 loads document from file.Stream). At that point, the original stream has been disposed. This will cause ObjectDisposedException when the detector tries to process these streams.

Copilot uses AI. Check for mistakes.
Comment on lines +273 to +292
.Select(componentStream =>
{
var depsDir = Path.GetDirectoryName(componentStream.Location);
successfulDirectories.Add(depsDir);

using var reader = new StreamReader(componentStream.Stream);
var content = reader.ReadToEnd();
return new ProcessRequest
{
ComponentStream = new ComponentStream
{
Stream = new MemoryStream(Encoding.UTF8.GetBytes(content)),
Location = componentStream.Location,
Pattern = BcdeMvnDepsFileName,
},
SingleFileComponentRecorder = this.ComponentRecorder.CreateSingleFileComponentRecorder(
Path.Combine(depsDir, MavenManifest)),
};
})
.ToList();
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stream is disposed prematurely by the 'using' statement. The ComponentStream.Stream is read in the 'using' block at line 278, then the content is passed to a new MemoryStream in the returned ProcessRequest. However, in OnFileFoundAsync, the ProcessRequest.ComponentStream.Stream is accessed again to process the file (line 518 loads document from file.Stream). At that point, the original stream has been disposed. This will cause ObjectDisposedException when the detector tries to process these streams.

Copilot uses AI. Check for mistakes.
Comment on lines +393 to +399
foreach (var error in this.mavenCliErrors)
{
var endpoints = this.ExtractFailedEndpoints(error);
foreach (var endpoint in endpoints)
{
this.failedEndpoints.Add(endpoint);
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.

Suggested change
foreach (var error in this.mavenCliErrors)
{
var endpoints = this.ExtractFailedEndpoints(error);
foreach (var endpoint in endpoints)
{
this.failedEndpoints.Add(endpoint);
}
var endpoints = this.mavenCliErrors.SelectMany(this.ExtractFailedEndpoints);
foreach (var endpoint in endpoints)
{
this.failedEndpoints.Add(endpoint);

Copilot uses AI. Check for mistakes.
.Returns("false");

// Act
var (detectorResult, componentRecorder) = await this.DetectorTestUtility
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to componentRecorder is useless, since its value is never read.

Suggested change
var (detectorResult, componentRecorder) = await this.DetectorTestUtility
var (detectorResult, _) = await this.DetectorTestUtility

Copilot uses AI. Check for mistakes.
Comment on lines +544 to +554
string versionString;

if (versionRef.StartsWith("${"))
{
versionString = this.ResolveVersion(versionRef, document, file.Location);
}
else
{
versionString = versionRef;
}

Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.

Suggested change
string versionString;
if (versionRef.StartsWith("${"))
{
versionString = this.ResolveVersion(versionRef, document, file.Location);
}
else
{
versionString = versionRef;
}
var versionString = versionRef.StartsWith("${")
? this.ResolveVersion(versionRef, document, file.Location)
: versionRef;

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings February 4, 2026 21:52
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.

public class MavenWithFallbackExperiment : IExperimentConfiguration
{
/// <inheritdoc />
public string Name => "MavenWithFallbackExperiment";
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The experiment name is set to "MavenWithFallbackExperiment" which is redundant (includes "Experiment" twice). Looking at other experiments in the codebase:

  • SimplePipExperiment has Name => "NewPipDetector" (line 12 in SimplePipExperiment.cs)
  • UvLockDetectorExperiment has Name => "UvLockDetectorExperiment" (includes "Experiment")
  • LinuxApplicationLayerExperiment has Name => "LinuxApplicationLayer" (excludes "Experiment")

For consistency and brevity, consider changing this to "MavenWithFallback" to match the detector naming pattern and avoid the redundant "Experiment" suffix.

Copilot uses AI. Check for mistakes.
Comment on lines +682 to +688
// Arrange - Maven CLI is available but fails for all pom.xml files (e.g., auth error)
this.mavenCommandServiceMock.Setup(x => x.MavenCLIExistsAsync())
.ReturnsAsync(true);

// MvnCli runs but produces no bcde-fallback.mvndeps files (simulating complete failure)
this.mavenCommandServiceMock.Setup(x => x.GenerateDependenciesFileAsync(It.IsAny<ProcessRequest>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new MavenCliResult(true, null));
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test doesn't set up the UseFallbackDetectorEnvVar environment variable, which means the detector will run in experimental mode (static parsing only) and will not execute Maven CLI at all. Looking at the detector logic (lines 182-200 in MavenWithFallbackDetector.cs), when useFallbackDetector is false, the detector returns immediately with static parsing only and never calls MavenCLIExistsAsync().

However, the test sets up MavenCLIExistsAsync() to return true (line 683) and expects Maven CLI to run but fail. This test should enable the fallback detector similar to other Maven CLI tests (e.g., lines 770-774, 893-896).

Copilot uses AI. Check for mistakes.
Comment on lines +210 to +216
// Arrange
this.mavenCommandServiceMock.Setup(x => x.MavenCLIExistsAsync())
.ReturnsAsync(true);

// MvnCli runs but produces no bcde-fallback.mvndeps files (simulating failure)
this.mavenCommandServiceMock.Setup(x => x.GenerateDependenciesFileAsync(It.IsAny<ProcessRequest>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new MavenCliResult(true, null));
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test doesn't set up the UseFallbackDetectorEnvVar environment variable, which means the detector will run in experimental mode (static parsing only) and will not execute Maven CLI at all. The test expects Maven CLI to run but produce no output, then fall back to static parsing. However, without enabling the fallback detector, the Maven CLI path is never executed.

This test should enable the fallback detector by setting up the environment variable mock:

this.envVarServiceMock.Setup(x => x.DoesEnvironmentVariableExist(MavenWithFallbackDetector.UseFallbackDetectorEnvVar))
    .Returns(true);
this.envVarServiceMock.Setup(x => x.GetEnvironmentVariable(MavenWithFallbackDetector.UseFallbackDetectorEnvVar))
    .Returns("true");

Copilot uses AI. Check for mistakes.
Comment on lines +505 to +580
this.mvnCliComponentCount += newCount - initialCount;
}

private void ProcessPomFileStatically(ProcessRequest processRequest)
{
var file = processRequest.ComponentStream;
var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder;
var filePath = file.Location;

try
{
var document = new XmlDocument();
document.Load(file.Stream);

lock (this.documentsLoaded)
{
this.documentsLoaded.TryAdd(file.Location, document);
}

var namespaceManager = new XmlNamespaceManager(document.NameTable);
namespaceManager.AddNamespace(ProjNamespace, MavenXmlNamespace);

var dependencyList = document.SelectNodes(DependencyNode, namespaceManager);
var componentsFoundInFile = 0;

foreach (XmlNode dependency in dependencyList)
{
var groupId = dependency[GroupIdSelector]?.InnerText;
var artifactId = dependency[ArtifactIdSelector]?.InnerText;

if (groupId == null || artifactId == null)
{
continue;
}

var version = dependency[VersionSelector];
if (version != null && !version.InnerText.Contains(','))
{
var versionRef = version.InnerText.Trim('[', ']');
string versionString;

if (versionRef.StartsWith("${"))
{
versionString = this.ResolveVersion(versionRef, document, file.Location);
}
else
{
versionString = versionRef;
}

if (!versionString.StartsWith("${"))
{
var component = new MavenComponent(groupId, artifactId, versionString);
var detectedComponent = new DetectedComponent(component);
singleFileComponentRecorder.RegisterUsage(detectedComponent);
componentsFoundInFile++;
}
else
{
this.Logger.LogDebug(
"Version string {Version} for component {Group}/{Artifact} is invalid or unsupported and a component will not be recorded.",
versionString,
groupId,
artifactId);
}
}
else
{
this.Logger.LogDebug(
"Version string for component {Group}/{Artifact} is invalid or unsupported and a component will not be recorded.",
groupId,
artifactId);
}
}

this.staticParserComponentCount += componentsFoundInFile;
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The counter increments for mvnCliComponentCount (line 505) and staticParserComponentCount (line 580) are not thread-safe. While the detector currently runs single-threaded by default (EnableParallelism defaults to false), if parallelism is enabled in the future, these increments could result in race conditions and inaccurate counts.

Consider using Interlocked.Add for thread-safe increments:

  • Line 505: Interlocked.Add(ref this.mvnCliComponentCount, newCount - initialCount);
  • Line 580: Interlocked.Add(ref this.staticParserComponentCount, componentsFoundInFile);

This would make the code more robust and prevent potential future bugs if parallelism is enabled.

Copilot uses AI. Check for mistakes.
Comment on lines +393 to +400
foreach (var error in this.mavenCliErrors)
{
var endpoints = this.ExtractFailedEndpoints(error);
foreach (var endpoint in endpoints)
{
this.failedEndpoints.Add(endpoint);
}
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +592 to +595
var (detectorResult, componentRecorder) = await this.DetectorTestUtility
.WithFile("pom.xml", componentString)
.WithFile("pom.xml", componentString, searchPatterns: [BcdeMvnFileName])
.ExecuteDetectorAsync();
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to componentRecorder is useless, since its value is never read.

Copilot uses AI. Check for mistakes.
Comment on lines +546 to +553
if (versionRef.StartsWith("${"))
{
versionString = this.ResolveVersion(versionRef, document, file.Location);
}
else
{
versionString = versionRef;
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant