feat: Add Go module and workspace test fixtures
- Created expected JSON files for Go modules and workspaces. - Added go.mod and go.sum files for example projects. - Implemented private module structure with expected JSON output. - Introduced vendored dependencies with corresponding expected JSON. - Developed PostgresGraphJobStore for managing graph jobs. - Established SQL migration scripts for graph jobs schema. - Implemented GraphJobRepository for CRUD operations on graph jobs. - Created IGraphJobRepository interface for repository abstraction. - Added unit tests for GraphJobRepository to ensure functionality.
This commit is contained in:
@@ -18,7 +18,94 @@ public sealed class GoLanguageAnalyzer : ILanguageAnalyzer
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(writer);
|
||||
|
||||
var candidatePaths = new List<string>(GoBinaryScanner.EnumerateCandidateFiles(context.RootPath));
|
||||
// Track emitted modules to avoid duplicates (binary takes precedence over source)
|
||||
var emittedModules = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
// Phase 1: Source scanning (go.mod, go.sum, go.work, vendor)
|
||||
ScanSourceFiles(context, writer, emittedModules, cancellationToken);
|
||||
|
||||
// Phase 2: Binary scanning (existing behavior)
|
||||
ScanBinaries(context, writer, emittedModules, cancellationToken);
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private void ScanSourceFiles(
|
||||
LanguageAnalyzerContext context,
|
||||
LanguageComponentWriter writer,
|
||||
HashSet<string> emittedModules,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Discover Go projects
|
||||
var projects = GoProjectDiscoverer.Discover(context.RootPath, cancellationToken);
|
||||
if (projects.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var project in projects)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
IReadOnlyList<GoSourceInventory.SourceInventoryResult> inventories;
|
||||
|
||||
if (project.IsWorkspace)
|
||||
{
|
||||
// Handle workspace with multiple modules
|
||||
inventories = GoSourceInventory.BuildWorkspaceInventory(project, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Single module
|
||||
var inventory = GoSourceInventory.BuildInventory(project);
|
||||
inventories = inventory.IsEmpty
|
||||
? Array.Empty<GoSourceInventory.SourceInventoryResult>()
|
||||
: new[] { inventory };
|
||||
}
|
||||
|
||||
foreach (var inventory in inventories)
|
||||
{
|
||||
if (inventory.IsEmpty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Emit the main module
|
||||
if (!string.IsNullOrEmpty(inventory.ModulePath))
|
||||
{
|
||||
EmitMainModuleFromSource(inventory, project, context, writer, emittedModules);
|
||||
}
|
||||
|
||||
// Emit dependencies
|
||||
foreach (var module in inventory.Modules.OrderBy(m => m.Path, StringComparer.Ordinal))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
EmitSourceModule(module, inventory, project, context, writer, emittedModules);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ScanBinaries(
|
||||
LanguageAnalyzerContext context,
|
||||
LanguageComponentWriter writer,
|
||||
HashSet<string> emittedModules,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var candidatePaths = new List<string>();
|
||||
|
||||
// Use binary format pre-filtering for efficiency
|
||||
foreach (var path in GoBinaryScanner.EnumerateCandidateFiles(context.RootPath))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Quick check for known binary formats
|
||||
if (GoBinaryFormatDetector.IsPotentialBinary(path))
|
||||
{
|
||||
candidatePaths.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
candidatePaths.Sort(StringComparer.Ordinal);
|
||||
|
||||
var fallbackBinaries = new List<GoStrippedBinaryClassification>();
|
||||
@@ -37,7 +124,7 @@ public sealed class GoLanguageAnalyzer : ILanguageAnalyzer
|
||||
continue;
|
||||
}
|
||||
|
||||
EmitComponents(buildInfo, context, writer);
|
||||
EmitComponents(buildInfo, context, writer, emittedModules);
|
||||
}
|
||||
|
||||
foreach (var fallback in fallbackBinaries)
|
||||
@@ -45,11 +132,197 @@ public sealed class GoLanguageAnalyzer : ILanguageAnalyzer
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
EmitFallbackComponent(fallback, context, writer);
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private void EmitComponents(GoBuildInfo buildInfo, LanguageAnalyzerContext context, LanguageComponentWriter writer)
|
||||
private void EmitMainModuleFromSource(
|
||||
GoSourceInventory.SourceInventoryResult inventory,
|
||||
GoProjectDiscoverer.GoProject project,
|
||||
LanguageAnalyzerContext context,
|
||||
LanguageComponentWriter writer,
|
||||
HashSet<string> emittedModules)
|
||||
{
|
||||
// Main module from go.mod (typically no version in source)
|
||||
var modulePath = inventory.ModulePath!;
|
||||
var moduleKey = $"{modulePath}@(devel)";
|
||||
|
||||
if (!emittedModules.Add(moduleKey))
|
||||
{
|
||||
return; // Already emitted
|
||||
}
|
||||
|
||||
var relativePath = context.GetRelativePath(project.RootPath);
|
||||
var goModRelative = project.HasGoMod ? context.GetRelativePath(project.GoModPath!) : null;
|
||||
|
||||
var metadata = new SortedDictionary<string, string?>(StringComparer.Ordinal)
|
||||
{
|
||||
["modulePath"] = modulePath,
|
||||
["modulePath.main"] = modulePath,
|
||||
["provenance"] = "source"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(inventory.GoVersion))
|
||||
{
|
||||
metadata["go.version"] = inventory.GoVersion;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(relativePath))
|
||||
{
|
||||
metadata["projectPath"] = relativePath;
|
||||
}
|
||||
|
||||
if (project.IsWorkspace)
|
||||
{
|
||||
metadata["workspace"] = "true";
|
||||
}
|
||||
|
||||
var evidence = new List<LanguageComponentEvidence>();
|
||||
|
||||
if (!string.IsNullOrEmpty(goModRelative))
|
||||
{
|
||||
evidence.Add(new LanguageComponentEvidence(
|
||||
LanguageEvidenceKind.File,
|
||||
"go.mod",
|
||||
goModRelative,
|
||||
modulePath,
|
||||
null));
|
||||
}
|
||||
|
||||
evidence.Sort(static (l, r) => string.CompareOrdinal(l.ComparisonKey, r.ComparisonKey));
|
||||
|
||||
// Main module typically has (devel) as version in source context
|
||||
writer.AddFromExplicitKey(
|
||||
analyzerId: Id,
|
||||
componentKey: $"golang::source::{modulePath}::(devel)",
|
||||
purl: null,
|
||||
name: modulePath,
|
||||
version: "(devel)",
|
||||
type: "golang",
|
||||
metadata: metadata,
|
||||
evidence: evidence);
|
||||
}
|
||||
|
||||
private void EmitSourceModule(
|
||||
GoSourceInventory.GoSourceModule module,
|
||||
GoSourceInventory.SourceInventoryResult inventory,
|
||||
GoProjectDiscoverer.GoProject project,
|
||||
LanguageAnalyzerContext context,
|
||||
LanguageComponentWriter writer,
|
||||
HashSet<string> emittedModules)
|
||||
{
|
||||
var moduleKey = $"{module.Path}@{module.Version}";
|
||||
|
||||
if (!emittedModules.Add(moduleKey))
|
||||
{
|
||||
return; // Already emitted (binary takes precedence)
|
||||
}
|
||||
|
||||
var purl = BuildPurl(module.Path, module.Version);
|
||||
var goModRelative = project.HasGoMod ? context.GetRelativePath(project.GoModPath!) : null;
|
||||
|
||||
var metadata = new SortedDictionary<string, string?>(StringComparer.Ordinal)
|
||||
{
|
||||
["modulePath"] = module.Path,
|
||||
["moduleVersion"] = module.Version,
|
||||
["provenance"] = "source"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(module.Checksum))
|
||||
{
|
||||
metadata["moduleSum"] = module.Checksum;
|
||||
}
|
||||
|
||||
if (module.IsDirect)
|
||||
{
|
||||
metadata["dependency.direct"] = "true";
|
||||
}
|
||||
|
||||
if (module.IsIndirect)
|
||||
{
|
||||
metadata["dependency.indirect"] = "true";
|
||||
}
|
||||
|
||||
if (module.IsVendored)
|
||||
{
|
||||
metadata["vendored"] = "true";
|
||||
}
|
||||
|
||||
if (module.IsPrivate)
|
||||
{
|
||||
metadata["private"] = "true";
|
||||
}
|
||||
|
||||
if (module.ModuleCategory != "public")
|
||||
{
|
||||
metadata["moduleCategory"] = module.ModuleCategory;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(module.Registry))
|
||||
{
|
||||
metadata["registry"] = module.Registry;
|
||||
}
|
||||
|
||||
if (module.IsReplaced)
|
||||
{
|
||||
metadata["replaced"] = "true";
|
||||
|
||||
if (!string.IsNullOrEmpty(module.ReplacementPath))
|
||||
{
|
||||
metadata["replacedBy.path"] = module.ReplacementPath;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(module.ReplacementVersion))
|
||||
{
|
||||
metadata["replacedBy.version"] = module.ReplacementVersion;
|
||||
}
|
||||
}
|
||||
|
||||
if (module.IsExcluded)
|
||||
{
|
||||
metadata["excluded"] = "true";
|
||||
}
|
||||
|
||||
var evidence = new List<LanguageComponentEvidence>();
|
||||
|
||||
// Evidence from go.mod
|
||||
if (!string.IsNullOrEmpty(goModRelative))
|
||||
{
|
||||
evidence.Add(new LanguageComponentEvidence(
|
||||
LanguageEvidenceKind.Metadata,
|
||||
module.Source,
|
||||
goModRelative,
|
||||
$"{module.Path}@{module.Version}",
|
||||
module.Checksum));
|
||||
}
|
||||
|
||||
evidence.Sort(static (l, r) => string.CompareOrdinal(l.ComparisonKey, r.ComparisonKey));
|
||||
|
||||
if (!string.IsNullOrEmpty(purl))
|
||||
{
|
||||
writer.AddFromPurl(
|
||||
analyzerId: Id,
|
||||
purl: purl,
|
||||
name: module.Path,
|
||||
version: module.Version,
|
||||
type: "golang",
|
||||
metadata: metadata,
|
||||
evidence: evidence,
|
||||
usedByEntrypoint: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.AddFromExplicitKey(
|
||||
analyzerId: Id,
|
||||
componentKey: $"golang::source::{module.Path}@{module.Version}",
|
||||
purl: null,
|
||||
name: module.Path,
|
||||
version: module.Version,
|
||||
type: "golang",
|
||||
metadata: metadata,
|
||||
evidence: evidence);
|
||||
}
|
||||
}
|
||||
|
||||
private void EmitComponents(GoBuildInfo buildInfo, LanguageAnalyzerContext context, LanguageComponentWriter writer, HashSet<string> emittedModules)
|
||||
{
|
||||
var components = new List<GoModule> { buildInfo.MainModule };
|
||||
components.AddRange(buildInfo.Dependencies
|
||||
@@ -61,6 +334,10 @@ public sealed class GoLanguageAnalyzer : ILanguageAnalyzer
|
||||
|
||||
foreach (var module in components)
|
||||
{
|
||||
// Track emitted modules (binary evidence is more accurate than source)
|
||||
var moduleKey = $"{module.Path}@{module.Version ?? "(devel)"}";
|
||||
emittedModules.Add(moduleKey);
|
||||
|
||||
var metadata = BuildMetadata(buildInfo, module, binaryRelativePath);
|
||||
var evidence = BuildEvidence(buildInfo, module, binaryRelativePath, context, ref binaryHash);
|
||||
var usedByEntrypoint = module.IsMain && context.UsageHints.IsPathUsed(buildInfo.AbsoluteBinaryPath);
|
||||
|
||||
Reference in New Issue
Block a user