feat: Implement Runtime Facts ingestion service and NDJSON reader
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Added RuntimeFactsNdjsonReader for reading NDJSON formatted runtime facts.
- Introduced IRuntimeFactsIngestionService interface and its implementation.
- Enhanced Program.cs to register new services and endpoints for runtime facts.
- Updated CallgraphIngestionService to include CAS URI in stored artifacts.
- Created RuntimeFactsValidationException for validation errors during ingestion.
- Added tests for RuntimeFactsIngestionService and RuntimeFactsNdjsonReader.
- Implemented SignalsSealedModeMonitor for compliance checks in sealed mode.
- Updated project dependencies for testing utilities.
This commit is contained in:
master
2025-11-10 07:56:15 +02:00
parent 9df52d84aa
commit 69c59defdc
132 changed files with 19718 additions and 9334 deletions

View File

@@ -2,35 +2,57 @@ using System.Text.Json;
namespace StellaOps.Scanner.Analyzers.Lang.Node.Internal;
internal sealed class NodeLockData
{
private static readonly NodeLockData Empty = new(new Dictionary<string, NodeLockEntry>(StringComparer.Ordinal), new Dictionary<string, NodeLockEntry>(StringComparer.OrdinalIgnoreCase));
internal sealed class NodeLockData
{
private const string PackageLockSource = "package-lock.json";
private const string YarnLockSource = "yarn.lock";
private const string PnpmLockSource = "pnpm-lock.yaml";
private static readonly NodeLockData Empty = new(
new Dictionary<string, NodeLockEntry>(StringComparer.Ordinal),
new Dictionary<string, NodeLockEntry>(StringComparer.OrdinalIgnoreCase),
Array.Empty<NodeLockEntry>());
private readonly Dictionary<string, NodeLockEntry> _byPath;
private readonly Dictionary<string, NodeLockEntry> _byName;
private readonly IReadOnlyCollection<NodeLockEntry> _declared;
private NodeLockData(
Dictionary<string, NodeLockEntry> byPath,
Dictionary<string, NodeLockEntry> byName,
IReadOnlyCollection<NodeLockEntry> declared)
{
_byPath = byPath;
_byName = byName;
_declared = declared;
}
private readonly Dictionary<string, NodeLockEntry> _byPath;
private readonly Dictionary<string, NodeLockEntry> _byName;
private NodeLockData(Dictionary<string, NodeLockEntry> byPath, Dictionary<string, NodeLockEntry> byName)
{
_byPath = byPath;
_byName = byName;
}
public static ValueTask<NodeLockData> LoadAsync(string rootPath, CancellationToken cancellationToken)
{
var byPath = new Dictionary<string, NodeLockEntry>(StringComparer.Ordinal);
var byName = new Dictionary<string, NodeLockEntry>(StringComparer.OrdinalIgnoreCase);
LoadPackageLockJson(rootPath, byPath, byName, cancellationToken);
LoadYarnLock(rootPath, byName);
LoadPnpmLock(rootPath, byName);
if (byPath.Count == 0 && byName.Count == 0)
{
return ValueTask.FromResult(Empty);
}
return ValueTask.FromResult(new NodeLockData(byPath, byName));
}
public IReadOnlyCollection<NodeLockEntry> DeclaredPackages => _declared;
public static ValueTask<NodeLockData> LoadAsync(string rootPath, CancellationToken cancellationToken)
{
var byPath = new Dictionary<string, NodeLockEntry>(StringComparer.Ordinal);
var byName = new Dictionary<string, NodeLockEntry>(StringComparer.OrdinalIgnoreCase);
var declared = new Dictionary<string, NodeLockEntry>(StringComparer.OrdinalIgnoreCase);
LoadPackageLockJson(rootPath, byPath, byName, declared, cancellationToken);
LoadYarnLock(rootPath, byName, declared);
LoadPnpmLock(rootPath, byName, declared);
if (byPath.Count == 0 && byName.Count == 0 && declared.Count == 0)
{
return ValueTask.FromResult(Empty);
}
var declaredList = declared.Values
.OrderBy(static entry => entry.Name, StringComparer.OrdinalIgnoreCase)
.ThenBy(static entry => entry.Version ?? string.Empty, StringComparer.OrdinalIgnoreCase)
.ThenBy(static entry => entry.Source, StringComparer.OrdinalIgnoreCase)
.ThenBy(static entry => entry.Locator ?? string.Empty, StringComparer.OrdinalIgnoreCase)
.ToArray();
return ValueTask.FromResult(new NodeLockData(byPath, byName, declaredList));
}
public bool TryGet(string relativePath, string packageName, out NodeLockEntry? entry)
{
@@ -55,16 +77,30 @@ internal sealed class NodeLockData
return false;
}
private static NodeLockEntry? CreateEntry(JsonElement element)
{
string? version = null;
string? resolved = null;
string? integrity = null;
if (element.TryGetProperty("version", out var versionElement) && versionElement.ValueKind == JsonValueKind.String)
{
version = versionElement.GetString();
}
private static NodeLockEntry? CreateEntry(
string source,
string? locator,
string? inferredName,
JsonElement element)
{
string? name = inferredName;
string? version = null;
string? resolved = null;
string? integrity = null;
if (element.TryGetProperty("name", out var nameElement) && nameElement.ValueKind == JsonValueKind.String)
{
var explicitName = nameElement.GetString();
if (!string.IsNullOrWhiteSpace(explicitName))
{
name = explicitName;
}
}
if (element.TryGetProperty("version", out var versionElement) && versionElement.ValueKind == JsonValueKind.String)
{
version = versionElement.GetString();
}
if (element.TryGetProperty("resolved", out var resolvedElement) && resolvedElement.ValueKind == JsonValueKind.String)
{
@@ -76,43 +112,56 @@ internal sealed class NodeLockData
integrity = integrityElement.GetString();
}
if (version is null && resolved is null && integrity is null)
{
return null;
}
if (version is null && resolved is null && integrity is null)
{
return null;
}
if (string.IsNullOrWhiteSpace(name))
{
return null;
}
var locatorValue = string.IsNullOrWhiteSpace(locator) ? null : locator;
return new NodeLockEntry(source, locatorValue, name!, version, resolved, integrity);
}
return new NodeLockEntry(version, resolved, integrity);
}
private static void TraverseLegacyDependencies(
string currentPath,
JsonElement dependenciesElement,
IDictionary<string, NodeLockEntry> byPath,
IDictionary<string, NodeLockEntry> byName,
IDictionary<string, NodeLockEntry> declared)
{
foreach (var dependency in dependenciesElement.EnumerateObject())
{
var depValue = dependency.Value;
var path = $"{currentPath}/{dependency.Name}";
var normalizedPath = NormalizeLockPath(path);
var entry = CreateEntry(PackageLockSource, normalizedPath, dependency.Name, depValue);
if (entry is not null)
{
byPath[normalizedPath] = entry;
byName[dependency.Name] = entry;
AddDeclaration(declared, entry);
}
if (depValue.TryGetProperty("dependencies", out var childDependencies) && childDependencies.ValueKind == JsonValueKind.Object)
{
TraverseLegacyDependencies(path + "/node_modules", childDependencies, byPath, byName, declared);
}
}
}
private static void TraverseLegacyDependencies(
string currentPath,
JsonElement dependenciesElement,
IDictionary<string, NodeLockEntry> byPath,
IDictionary<string, NodeLockEntry> byName)
{
foreach (var dependency in dependenciesElement.EnumerateObject())
{
var depValue = dependency.Value;
var path = $"{currentPath}/{dependency.Name}";
var entry = CreateEntry(depValue);
if (entry is not null)
{
var normalizedPath = NormalizeLockPath(path);
byPath[normalizedPath] = entry;
byName[dependency.Name] = entry;
}
if (depValue.TryGetProperty("dependencies", out var childDependencies) && childDependencies.ValueKind == JsonValueKind.Object)
{
TraverseLegacyDependencies(path + "/node_modules", childDependencies, byPath, byName);
}
}
}
private static void LoadPackageLockJson(string rootPath, IDictionary<string, NodeLockEntry> byPath, IDictionary<string, NodeLockEntry> byName, CancellationToken cancellationToken)
{
var packageLockPath = Path.Combine(rootPath, "package-lock.json");
if (!File.Exists(packageLockPath))
private static void LoadPackageLockJson(
string rootPath,
IDictionary<string, NodeLockEntry> byPath,
IDictionary<string, NodeLockEntry> byName,
IDictionary<string, NodeLockEntry> declared,
CancellationToken cancellationToken)
{
var packageLockPath = Path.Combine(rootPath, "package-lock.json");
if (!File.Exists(packageLockPath))
{
return;
}
@@ -127,38 +176,32 @@ internal sealed class NodeLockData
if (root.TryGetProperty("packages", out var packagesElement) && packagesElement.ValueKind == JsonValueKind.Object)
{
foreach (var packageProperty in packagesElement.EnumerateObject())
{
var entry = CreateEntry(packageProperty.Value);
if (entry is null)
{
continue;
}
var key = NormalizeLockPath(packageProperty.Name);
byPath[key] = entry;
var name = ExtractNameFromPath(key);
if (!string.IsNullOrEmpty(name))
{
byName[name] = entry;
}
if (packageProperty.Value.TryGetProperty("name", out var explicitNameElement) && explicitNameElement.ValueKind == JsonValueKind.String)
{
var explicitName = explicitNameElement.GetString();
if (!string.IsNullOrWhiteSpace(explicitName))
{
byName[explicitName] = entry;
}
}
}
}
else if (root.TryGetProperty("dependencies", out var dependenciesElement) && dependenciesElement.ValueKind == JsonValueKind.Object)
{
TraverseLegacyDependencies("node_modules", dependenciesElement, byPath, byName);
}
}
foreach (var packageProperty in packagesElement.EnumerateObject())
{
var key = NormalizeLockPath(packageProperty.Name);
var inferredName = ExtractNameFromPath(key);
var entry = CreateEntry(PackageLockSource, key, inferredName, packageProperty.Value);
if (entry is null)
{
continue;
}
byPath[key] = entry;
if (!string.IsNullOrEmpty(entry.Name))
{
byName[entry.Name] = entry;
}
AddDeclaration(declared, entry);
}
}
else if (root.TryGetProperty("dependencies", out var dependenciesElement) && dependenciesElement.ValueKind == JsonValueKind.Object)
{
TraverseLegacyDependencies("node_modules", dependenciesElement, byPath, byName, declared);
}
}
catch (IOException)
{
// Ignore unreadable package-lock.
@@ -169,7 +212,10 @@ internal sealed class NodeLockData
}
}
private static void LoadYarnLock(string rootPath, IDictionary<string, NodeLockEntry> byName)
private static void LoadYarnLock(
string rootPath,
IDictionary<string, NodeLockEntry> byName,
IDictionary<string, NodeLockEntry> declared)
{
var yarnLockPath = Path.Combine(rootPath, "yarn.lock");
if (!File.Exists(yarnLockPath))
@@ -185,31 +231,32 @@ internal sealed class NodeLockData
string? resolved = null;
string? integrity = null;
void Flush()
{
if (string.IsNullOrWhiteSpace(currentName))
{
version = null;
resolved = null;
integrity = null;
return;
}
var simpleName = ExtractPackageNameFromYarnKey(currentName!);
if (string.IsNullOrEmpty(simpleName))
{
version = null;
resolved = null;
integrity = null;
return;
}
var entry = new NodeLockEntry(version, resolved, integrity);
byName[simpleName] = entry;
version = null;
resolved = null;
integrity = null;
}
void Flush()
{
if (string.IsNullOrWhiteSpace(currentName))
{
version = null;
resolved = null;
integrity = null;
return;
}
var simpleName = ExtractPackageNameFromYarnKey(currentName!);
if (string.IsNullOrEmpty(simpleName))
{
version = null;
resolved = null;
integrity = null;
return;
}
var entry = new NodeLockEntry(YarnLockSource, currentName, simpleName, version, resolved, integrity);
byName[simpleName] = entry;
AddDeclaration(declared, entry);
version = null;
resolved = null;
integrity = null;
}
foreach (var line in lines)
{
@@ -250,7 +297,10 @@ internal sealed class NodeLockData
}
}
private static void LoadPnpmLock(string rootPath, IDictionary<string, NodeLockEntry> byName)
private static void LoadPnpmLock(
string rootPath,
IDictionary<string, NodeLockEntry> byName,
IDictionary<string, NodeLockEntry> declared)
{
var pnpmLockPath = Path.Combine(rootPath, "pnpm-lock.yaml");
if (!File.Exists(pnpmLockPath))
@@ -258,94 +308,107 @@ internal sealed class NodeLockData
return;
}
try
{
using var reader = new StreamReader(pnpmLockPath);
string? currentPackage = null;
string? version = null;
string? resolved = null;
string? integrity = null;
var inPackages = false;
while (reader.ReadLine() is { } line)
{
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
if (!inPackages)
{
if (line.StartsWith("packages:", StringComparison.Ordinal))
{
inPackages = true;
}
continue;
}
if (line.StartsWith(" /", StringComparison.Ordinal))
{
if (!string.IsNullOrEmpty(currentPackage) && !string.IsNullOrEmpty(integrity))
{
var name = ExtractNameFromPnpmKey(currentPackage);
if (!string.IsNullOrEmpty(name))
{
byName[name] = new NodeLockEntry(version, resolved, integrity);
}
}
currentPackage = line.Trim().TrimEnd(':').TrimStart('/');
version = null;
resolved = null;
integrity = null;
continue;
}
if (string.IsNullOrEmpty(currentPackage))
{
continue;
}
var trimmed = line.Trim();
if (trimmed.StartsWith("resolution:", StringComparison.Ordinal))
{
var integrityIndex = trimmed.IndexOf("integrity", StringComparison.OrdinalIgnoreCase);
if (integrityIndex >= 0)
{
var integrityValue = trimmed[(integrityIndex + 9)..].Trim(' ', ':', '{', '}', '"');
integrity = integrityValue;
}
var tarballIndex = trimmed.IndexOf("tarball", StringComparison.OrdinalIgnoreCase);
if (tarballIndex >= 0)
{
var tarballValue = trimmed[(tarballIndex + 7)..].Trim(' ', ':', '{', '}', '"');
resolved = tarballValue;
}
}
else if (trimmed.StartsWith("integrity:", StringComparison.Ordinal))
{
integrity = trimmed[("integrity:".Length)..].Trim();
}
else if (trimmed.StartsWith("tarball:", StringComparison.Ordinal))
{
resolved = trimmed[("tarball:".Length)..].Trim();
}
else if (trimmed.StartsWith("version:", StringComparison.Ordinal))
{
version = trimmed[("version:".Length)..].Trim();
}
}
if (!string.IsNullOrEmpty(currentPackage) && !string.IsNullOrEmpty(integrity))
{
var name = ExtractNameFromPnpmKey(currentPackage);
if (!string.IsNullOrEmpty(name))
{
byName[name] = new NodeLockEntry(version, resolved, integrity);
}
}
}
try
{
using var reader = new StreamReader(pnpmLockPath);
string? currentPackage = null;
string? version = null;
string? resolved = null;
string? integrity = null;
var inPackages = false;
void Flush()
{
if (string.IsNullOrEmpty(currentPackage) || string.IsNullOrEmpty(integrity))
{
version = null;
resolved = null;
integrity = null;
return;
}
var name = ExtractNameFromPnpmKey(currentPackage!);
if (string.IsNullOrEmpty(name))
{
version = null;
resolved = null;
integrity = null;
return;
}
var entry = new NodeLockEntry(PnpmLockSource, currentPackage, name, version, resolved, integrity);
byName[name] = entry;
AddDeclaration(declared, entry);
version = null;
resolved = null;
integrity = null;
}
while (reader.ReadLine() is { } line)
{
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
if (!inPackages)
{
if (line.StartsWith("packages:", StringComparison.Ordinal))
{
inPackages = true;
}
continue;
}
if (line.StartsWith(" /", StringComparison.Ordinal))
{
Flush();
currentPackage = line.Trim().TrimEnd(':').TrimStart('/');
version = null;
resolved = null;
integrity = null;
continue;
}
if (string.IsNullOrEmpty(currentPackage))
{
continue;
}
var trimmed = line.Trim();
if (trimmed.StartsWith("resolution:", StringComparison.Ordinal))
{
var integrityIndex = trimmed.IndexOf("integrity", StringComparison.OrdinalIgnoreCase);
if (integrityIndex >= 0)
{
var integrityValue = trimmed[(integrityIndex + 9)..].Trim(' ', ':', '{', '}', '"');
integrity = integrityValue;
}
var tarballIndex = trimmed.IndexOf("tarball", StringComparison.OrdinalIgnoreCase);
if (tarballIndex >= 0)
{
var tarballValue = trimmed[(tarballIndex + 7)..].Trim(' ', ':', '{', '}', '"');
resolved = tarballValue;
}
}
else if (trimmed.StartsWith("integrity:", StringComparison.Ordinal))
{
integrity = trimmed[("integrity:".Length)..].Trim();
}
else if (trimmed.StartsWith("tarball:", StringComparison.Ordinal))
{
resolved = trimmed[("tarball:".Length)..].Trim();
}
else if (trimmed.StartsWith("version:", StringComparison.Ordinal))
{
version = trimmed[("version:".Length)..].Trim();
}
}
Flush();
}
catch (IOException)
{
// Ignore unreadable pnpm lock file.
@@ -384,9 +447,9 @@ internal sealed class NodeLockData
return trimmed;
}
private static string ExtractNameFromPnpmKey(string key)
{
var parts = key.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
private static string ExtractNameFromPnpmKey(string key)
{
var parts = key.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length == 0)
{
return string.Empty;
@@ -397,8 +460,27 @@ internal sealed class NodeLockData
return parts.Length >= 2 ? $"{parts[0]}/{parts[1]}" : parts[0];
}
return parts[0];
}
return parts[0];
}
private static void AddDeclaration(IDictionary<string, NodeLockEntry> declared, NodeLockEntry entry)
{
if (declared is null || entry is null)
{
return;
}
if (string.IsNullOrWhiteSpace(entry.Name) || string.IsNullOrWhiteSpace(entry.Version))
{
return;
}
var key = entry.DeclarationKey();
if (!declared.ContainsKey(key))
{
declared[key] = entry;
}
}
private static string NormalizeLockPath(string path)
{

View File

@@ -1,3 +1,20 @@
namespace StellaOps.Scanner.Analyzers.Lang.Node.Internal;
internal sealed record NodeLockEntry(string? Version, string? Resolved, string? Integrity);
internal sealed record NodeLockEntry(
string Source,
string? Locator,
string Name,
string? Version,
string? Resolved,
string? Integrity);
internal static class NodeLockEntryExtensions
{
public static string DeclarationKey(this NodeLockEntry entry)
{
ArgumentNullException.ThrowIfNull(entry);
var version = entry.Version ?? string.Empty;
return $"{entry.Name}@{version}".ToLowerInvariant();
}
}

View File

@@ -13,23 +13,29 @@ internal sealed class NodePackage
string? workspaceRoot,
IReadOnlyList<string> workspaceTargets,
string? workspaceLink,
IReadOnlyList<NodeLifecycleScript> lifecycleScripts,
bool usedByEntrypoint)
{
Name = name;
Version = version;
RelativePath = relativePath;
PackageJsonLocator = packageJsonLocator;
IReadOnlyList<NodeLifecycleScript> lifecycleScripts,
bool usedByEntrypoint,
bool declaredOnly = false,
string? lockSource = null,
string? lockLocator = null)
{
Name = name;
Version = version;
RelativePath = relativePath;
PackageJsonLocator = packageJsonLocator;
IsPrivate = isPrivate;
LockEntry = lockEntry;
IsWorkspaceMember = isWorkspaceMember;
WorkspaceRoot = workspaceRoot;
WorkspaceTargets = workspaceTargets;
WorkspaceLink = workspaceLink;
LifecycleScripts = lifecycleScripts ?? Array.Empty<NodeLifecycleScript>();
IsUsedByEntrypoint = usedByEntrypoint;
}
WorkspaceLink = workspaceLink;
LifecycleScripts = lifecycleScripts ?? Array.Empty<NodeLifecycleScript>();
IsUsedByEntrypoint = usedByEntrypoint;
DeclaredOnly = declaredOnly;
LockSource = lockSource;
LockLocator = lockLocator;
}
public string Name { get; }
public string Version { get; }
@@ -50,11 +56,17 @@ internal sealed class NodePackage
public string? WorkspaceLink { get; }
public IReadOnlyList<NodeLifecycleScript> LifecycleScripts { get; }
public bool HasInstallScripts => LifecycleScripts.Count > 0;
public bool IsUsedByEntrypoint { get; }
public IReadOnlyList<NodeLifecycleScript> LifecycleScripts { get; }
public bool HasInstallScripts => LifecycleScripts.Count > 0;
public bool IsUsedByEntrypoint { get; }
public bool DeclaredOnly { get; }
public string? LockSource { get; }
public string? LockLocator { get; }
public string RelativePathNormalized => string.IsNullOrEmpty(RelativePath) ? string.Empty : RelativePath.Replace(Path.DirectorySeparatorChar, '/');
@@ -64,10 +76,10 @@ internal sealed class NodePackage
public IReadOnlyCollection<LanguageComponentEvidence> CreateEvidence()
{
var evidence = new List<LanguageComponentEvidence>
{
new LanguageComponentEvidence(LanguageEvidenceKind.File, "package.json", PackageJsonLocator, Value: null, Sha256: null)
};
var evidence = new List<LanguageComponentEvidence>
{
CreateRootEvidence()
};
foreach (var script in LifecycleScripts)
{
@@ -83,8 +95,8 @@ internal sealed class NodePackage
script.Sha256));
}
return evidence;
}
return evidence;
}
public IReadOnlyCollection<KeyValuePair<string, string?>> CreateMetadata()
{
@@ -111,11 +123,11 @@ internal sealed class NodePackage
}
}
if (IsWorkspaceMember)
{
entries.Add(new KeyValuePair<string, string?>("workspaceMember", "true"));
if (!string.IsNullOrWhiteSpace(WorkspaceRoot))
{
if (IsWorkspaceMember)
{
entries.Add(new KeyValuePair<string, string?>("workspaceMember", "true"));
if (!string.IsNullOrWhiteSpace(WorkspaceRoot))
{
entries.Add(new KeyValuePair<string, string?>("workspaceRoot", WorkspaceRoot));
}
}
@@ -148,12 +160,27 @@ internal sealed class NodePackage
{
entries.Add(new KeyValuePair<string, string?>($"script.{script.Name}", script.Command));
}
}
return entries
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
.ToArray();
}
}
if (DeclaredOnly)
{
entries.Add(new KeyValuePair<string, string?>("declaredOnly", "true"));
}
if (!string.IsNullOrWhiteSpace(LockSource))
{
entries.Add(new KeyValuePair<string, string?>("lockSource", LockSource));
}
if (!string.IsNullOrWhiteSpace(LockLocator))
{
entries.Add(new KeyValuePair<string, string?>("lockLocator", LockLocator));
}
return entries
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
.ToArray();
}
private static string BuildPurl(string name, string version)
{
@@ -174,6 +201,21 @@ internal sealed class NodePackage
return $"%40{scopeAndName}";
}
return name;
}
}
return name;
}
private LanguageComponentEvidence CreateRootEvidence()
{
var evidenceSource = DeclaredOnly
? string.IsNullOrWhiteSpace(LockSource) ? "lockfile" : LockSource!
: "package.json";
var locator = DeclaredOnly
? (string.IsNullOrWhiteSpace(LockLocator) ? evidenceSource : LockLocator!)
: (string.IsNullOrWhiteSpace(PackageJsonLocator) ? "package.json" : PackageJsonLocator);
var kind = DeclaredOnly ? LanguageEvidenceKind.Metadata : LanguageEvidenceKind.File;
return new LanguageComponentEvidence(kind, evidenceSource, locator, Value: null, Sha256: null);
}
}

View File

@@ -56,8 +56,10 @@ internal static class NodePackageCollector
TraverseDirectory(context, pendingRoot, lockData, workspaceIndex, packages, visited, cancellationToken);
}
return packages;
}
AppendDeclaredPackages(packages, lockData);
return packages;
}
private static void TraverseDirectory(
LanguageAnalyzerContext context,
@@ -166,18 +168,94 @@ internal static class NodePackageCollector
}
}
private static void TraverseNestedNodeModules(
LanguageAnalyzerContext context,
string directory,
NodeLockData lockData,
NodeWorkspaceIndex workspaceIndex,
List<NodePackage> packages,
HashSet<string> visited,
CancellationToken cancellationToken)
{
var nestedNodeModules = Path.Combine(directory, "node_modules");
TraverseDirectory(context, nestedNodeModules, lockData, workspaceIndex, packages, visited, cancellationToken);
}
private static void TraverseNestedNodeModules(
LanguageAnalyzerContext context,
string directory,
NodeLockData lockData,
NodeWorkspaceIndex workspaceIndex,
List<NodePackage> packages,
HashSet<string> visited,
CancellationToken cancellationToken)
{
var nestedNodeModules = Path.Combine(directory, "node_modules");
TraverseDirectory(context, nestedNodeModules, lockData, workspaceIndex, packages, visited, cancellationToken);
}
private static void AppendDeclaredPackages(List<NodePackage> packages, NodeLockData lockData)
{
if (lockData.DeclaredPackages.Count == 0)
{
return;
}
var observed = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var package in packages)
{
var key = BuildDeclarationKey(package.Name, package.Version);
if (!string.IsNullOrEmpty(key))
{
observed.Add(key);
}
}
foreach (var entry in lockData.DeclaredPackages)
{
if (string.IsNullOrWhiteSpace(entry.Name) || string.IsNullOrWhiteSpace(entry.Version))
{
continue;
}
var key = BuildDeclarationKey(entry.Name, entry.Version);
if (string.IsNullOrEmpty(key) || !observed.Add(key))
{
continue;
}
var declaredPackage = new NodePackage(
entry.Name,
entry.Version,
relativePath: string.Empty,
packageJsonLocator: string.Empty,
isPrivate: null,
lockEntry: entry,
isWorkspaceMember: false,
workspaceRoot: null,
workspaceTargets: Array.Empty<string>(),
workspaceLink: null,
lifecycleScripts: Array.Empty<NodeLifecycleScript>(),
usedByEntrypoint: false,
declaredOnly: true,
lockSource: entry.Source,
lockLocator: BuildLockLocator(entry));
packages.Add(declaredPackage);
}
}
private static string BuildDeclarationKey(string name, string? version)
{
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(version))
{
return string.Empty;
}
return $"{name}@{version}".ToLowerInvariant();
}
private static string? BuildLockLocator(NodeLockEntry? entry)
{
if (entry is null)
{
return null;
}
if (string.IsNullOrWhiteSpace(entry.Locator))
{
return entry.Source;
}
return $"{entry.Source}:{entry.Locator}";
}
private static NodePackage? TryCreatePackage(
LanguageAnalyzerContext context,
@@ -221,9 +299,11 @@ internal static class NodePackageCollector
isPrivate = privateElement.GetBoolean();
}
var lockEntry = lockData.TryGet(relativeDirectory, name, out var entry) ? entry : null;
var locator = BuildLocator(relativeDirectory);
var usedByEntrypoint = context.UsageHints.IsPathUsed(packageJsonPath);
var lockEntry = lockData.TryGet(relativeDirectory, name, out var entry) ? entry : null;
var locator = BuildLocator(relativeDirectory);
var usedByEntrypoint = context.UsageHints.IsPathUsed(packageJsonPath);
var lockLocator = BuildLockLocator(lockEntry);
var lockSource = lockEntry?.Source;
var isWorkspaceMember = workspaceIndex.TryGetMember(relativeDirectory, out var workspaceRoot);
var workspaceTargets = ExtractWorkspaceTargets(relativeDirectory, root, workspaceIndex);
@@ -232,19 +312,22 @@ internal static class NodePackageCollector
: null;
var lifecycleScripts = ExtractLifecycleScripts(root);
return new NodePackage(
name: name.Trim(),
version: version.Trim(),
relativePath: relativeDirectory,
packageJsonLocator: locator,
isPrivate: isPrivate,
lockEntry: lockEntry,
isWorkspaceMember: isWorkspaceMember,
workspaceRoot: workspaceRoot,
workspaceTargets: workspaceTargets,
workspaceLink: workspaceLink,
lifecycleScripts: lifecycleScripts,
usedByEntrypoint: usedByEntrypoint);
return new NodePackage(
name: name.Trim(),
version: version.Trim(),
relativePath: relativeDirectory,
packageJsonLocator: locator,
isPrivate: isPrivate,
lockEntry: lockEntry,
isWorkspaceMember: isWorkspaceMember,
workspaceRoot: workspaceRoot,
workspaceTargets: workspaceTargets,
workspaceLink: workspaceLink,
lifecycleScripts: lifecycleScripts,
usedByEntrypoint: usedByEntrypoint,
declaredOnly: false,
lockSource: lockSource,
lockLocator: lockLocator);
}
catch (IOException)
{