Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,57 +13,73 @@
@Component
public class PipParser {

// Matches: package==1.0, package>=1.0, package~=1.0, package<=1.0, package!=1.0
// Also handles inline comments like: requests==2.31.0 # HTTP library
private static final Pattern DEP_PATTERN =
Pattern.compile("^([A-Za-z0-9_.-]+)\\s*([><=!~]+)\\s*([A-Za-z0-9_.*+-]+)");
/**
* Matches a pip requirement line:
* group 1 — package name (letters, digits, -, _, .)
* group 2 — optional extras e.g. [asyncio,security] (captured but discarded)
* group 3 — optional version specifier(s) e.g. >=1.0,<2.0 or ~=1.4.4
* (environment markers after ';' are stripped before matching)
*
* Examples handled:
* requests==2.31.0
* aiohttp>=3.11.18
* sqlalchemy[asyncio]>=2.0.41
* markitdown-no-magika[docx,xls,xlsx]>=0.1.2
* audioop-lts ; python_full_version >= '3.13'
* xinference-client (no version)
* chardet~=5.1.0
*/
private static final Pattern DEP_PATTERN = Pattern.compile(
"^([A-Za-z0-9][A-Za-z0-9_.-]*) # package name\n" +
"(\\[[^\\]]*\\])? # optional extras\n" +
"\\s*([><=!~,A-Za-z0-9_.*+\\-]*)\\s*$ # optional version spec(s)",
Pattern.COMMENTS
);

public List<Dependency> parse(String content) {

List<Dependency> deps = new ArrayList<>();

if (content == null || content.isBlank()) {
log.warn("[PipParser] Empty or null content received");
return deps;
}

String[] lines = content.split("\n");
// Normalise escaped newlines that appear when content travels through JSON
// e.g. "requests==2.31.0\naiohttp>=3.11.18" → real newlines
String normalised = content
.replace("\\r\\n", "\n")
.replace("\\n", "\n")
.replace("\\r", "\n");

for (String rawLine : lines) {
for (String rawLine : normalised.split("\n")) {

// Strip inline comments and whitespace
String line = rawLine.split("#")[0].trim();
// Strip inline comments and environment markers (everything after ';')
String line = rawLine.split("#")[0].split(";")[0].trim();

// Skip blank lines, comments, -r includes, -e editable installs
// Skip blank lines, options (-r, --index-url, -e editable installs, etc.)
if (line.isEmpty() || line.startsWith("-")) {
continue;
}

Matcher matcher = DEP_PATTERN.matcher(line);
if (!matcher.matches()) {
log.debug("[PipParser] Line did not match: '{}'", line);
continue;
}

if (matcher.find()) {
String name = matcher.group(1).trim();
String version = matcher.group(2).trim() + matcher.group(3).trim();
String name = matcher.group(1).trim();
String version = matcher.group(3) == null ? "" : matcher.group(3).trim();

deps.add(Dependency.builder()
.name(name)
.version(version)
.ecosystem("pip")
.build());
} else {
// Package with no version pinned (e.g. just "requests")
if (!line.isEmpty()) {
deps.add(Dependency.builder()
.name(line.trim())
.version("")
.ecosystem("pip")
.build());
}
}
if (name.isEmpty()) continue;

deps.add(Dependency.builder()
.name(name)
.version(version)
.ecosystem("pip")
.build());
}

log.info("[PipParser] Parsed {} dependencies", deps.size());
return deps;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.agent.service;

import com.agent.service.IDependencyParserService;

import com.agent.model.Dependency;
import com.agent.parser.*;
import lombok.RequiredArgsConstructor;
Expand All @@ -11,7 +13,7 @@
@Slf4j
@Service
@RequiredArgsConstructor
public class DependencyParserService {
public class DependencyParserService implements IDependencyParserService {

private final PomParser pomParser;
private final NpmParser npmParser;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.agent.service;

import com.agent.model.Dependency;

import java.util.List;

/**
* IDependencyParserService — contract for dependency file parsers.
*
* Extracted from the concrete class to enable mock injection in unit tests.
* Implementation: {@link impl.DependencyParserServiceImpl}

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

The Javadoc references impl.DependencyParserServiceImpl, but there is no such class in this module (the implementation is com.agent.service.DependencyParserService). Update the link to the correct implementation so IDE navigation and generated docs aren’t broken.

Suggested change
* Implementation: {@link impl.DependencyParserServiceImpl}
* Implementation: {@link DependencyParserService}

Copilot uses AI. Check for mistakes.
*/
public interface IDependencyParserService {

/** Parse Maven pom.xml content */
List<Dependency> parsePom(String content);

/** Parse npm package.json content */
List<Dependency> parsePackageJson(String content) throws Exception;

/** Parse pip requirements.txt content */
List<Dependency> parseRequirements(String content);

/** Parse Gradle build.gradle / build.gradle.kts content */
List<Dependency> parseGradle(String content);

/** Parse Flutter/Dart pubspec.yaml content */
List<Dependency> parsePubspec(String content);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.agent.service;

import com.agent.service.IGithubRepoService;

import com.agent.util.FileScannerUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand All @@ -15,7 +17,7 @@

@Slf4j
@Service
public class GithubRepoService {
public class GithubRepoService implements IGithubRepoService {

private static final String REPO_BASE_DIR = "/tmp/repos";
private static final int CLONE_TIMEOUT_SECONDS = 120;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.agent.service;

import java.io.IOException;
import java.util.List;

/**
* GithubRepoService — contract for repository access operations.
*
* Extracted from the concrete class to enable mock injection in unit tests.
* Implementation: {@link impl.GithubRepoServiceImpl}
*/
public interface IGithubRepoService {

/**
* Clone a GitHub repository to a local temp directory.
*
* @param repoUrl full HTTPS GitHub URL
* @param forceReclone if true, delete and re-clone even if already present
* @return absolute path to the cloned repository root
*/
String cloneRepository(String repoUrl, boolean forceReclone) throws Exception;

/**
* List all files recursively under repoPath (up to configured depth).
*/
List<String> listRepositoryFiles(String repoPath) throws IOException;

/**
* Find dependency manifest files (pom.xml, package.json, etc.) under repoPath.
*/
List<String> findDependencyFiles(String repoPath) throws IOException;

/**
* Read the content of a single file (path-traversal protected).
*/
String readFileContent(String filePath) throws IOException;
}
Loading