nuget reorganization

This commit is contained in:
master
2025-11-18 23:45:25 +02:00
parent 77cee6a209
commit d3ecd7f8e6
7712 changed files with 13963 additions and 10007504 deletions

View File

@@ -3,6 +3,7 @@ using System.Globalization;
using System.Text;
using StellaOps.Scanner.Analyzers.Lang.Deno.Internal;
using StellaOps.Scanner.Analyzers.Lang.Deno.Internal.Observations;
using StellaOps.Scanner.Analyzers.Lang.Deno.Internal.Runtime;
using StellaOps.Scanner.Core.Contracts;
namespace StellaOps.Scanner.Analyzers.Lang.Deno;
@@ -18,6 +19,8 @@ public sealed class DenoLanguageAnalyzer : ILanguageAnalyzer
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(writer);
await TryWriteRuntimeShimAsync(context, cancellationToken).ConfigureAwait(false);
var workspace = await DenoWorkspaceNormalizer.NormalizeAsync(context, cancellationToken).ConfigureAwait(false);
var moduleGraph = DenoModuleGraphResolver.Resolve(workspace, cancellationToken);
var compatibility = DenoNpmCompatibilityAdapter.Analyze(workspace, moduleGraph, cancellationToken);
@@ -62,6 +65,8 @@ public sealed class DenoLanguageAnalyzer : ILanguageAnalyzer
metadata: observationMetadata,
evidence: observationEvidence);
TryIngestRuntimeTrace(context);
// Task 5+ will convert moduleGraph + compatibility and bundle insights into SBOM components and evidence records.
GC.KeepAlive(moduleGraph);
GC.KeepAlive(compatibility);
@@ -70,6 +75,18 @@ public sealed class DenoLanguageAnalyzer : ILanguageAnalyzer
GC.KeepAlive(observationDocument);
}
private static async ValueTask TryWriteRuntimeShimAsync(LanguageAnalyzerContext context, CancellationToken cancellationToken)
{
try
{
await DenoRuntimeShim.WriteAsync(context.RootPath, cancellationToken).ConfigureAwait(false);
}
catch
{
// Shim is best-effort; failure should not block static analysis.
}
}
private void TryPersistObservation(
LanguageAnalyzerContext context,
byte[] observationBytes,
@@ -111,4 +128,59 @@ public sealed class DenoLanguageAnalyzer : ILanguageAnalyzer
return dictionary;
}
private void TryIngestRuntimeTrace(LanguageAnalyzerContext context)
{
if (context.AnalysisStore is not { } analysisStore)
{
return;
}
var tracePath = Path.Combine(context.RootPath, "deno-runtime.ndjson");
if (!File.Exists(tracePath))
{
return;
}
byte[] content;
try
{
content = File.ReadAllBytes(tracePath);
}
catch (IOException)
{
return;
}
var (metadata, hash) = DenoRuntimeTraceProbe.Analyze(content);
var runtimeMeta = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
{
["deno.runtime.hash"] = hash,
["deno.runtime.event_count"] = metadata.EventCount.ToString(CultureInfo.InvariantCulture),
["deno.runtime.permission_uses"] = metadata.PermissionUses.ToString(CultureInfo.InvariantCulture),
["deno.runtime.module_loads"] = metadata.ModuleLoads.ToString(CultureInfo.InvariantCulture),
["deno.runtime.remote_origins"] = string.Join(',', metadata.RemoteOrigins),
["deno.runtime.permissions"] = string.Join(',', metadata.UniquePermissions),
["deno.runtime.npm_resolutions"] = metadata.NpmResolutions.ToString(CultureInfo.InvariantCulture),
["deno.runtime.wasm_loads"] = metadata.WasmLoads.ToString(CultureInfo.InvariantCulture),
["deno.runtime.dynamic_imports"] = metadata.DynamicImports.ToString(CultureInfo.InvariantCulture)
};
var payload = new AnalyzerObservationPayload(
analyzerId: Id,
kind: "deno.runtime.v1",
mediaType: "application/x-ndjson",
content: content,
metadata: runtimeMeta,
view: "runtime");
analysisStore.Set("deno.runtime", payload);
// Also emit policy signals into AnalysisStore for downstream consumption.
var signals = DenoPolicySignalEmitter.FromTrace(hash, metadata);
foreach (var signal in signals)
{
analysisStore.Set(signal.Key, signal.Value);
}
}
}

View File

@@ -0,0 +1,146 @@
using System.Text;
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Internal.Runtime;
/// <summary>
/// Provides the TypeScript runtime shim that captures Deno runtime events into NDJSON.
/// This shim is written to disk alongside the analyzer to be invoked by the worker/CLI.
/// </summary>
internal static class DenoRuntimeShim
{
private const string ShimFileName = "trace-shim.ts";
public static string FileName => ShimFileName;
public static async Task<string> WriteAsync(string directory, CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(directory);
Directory.CreateDirectory(directory);
var path = Path.Combine(directory, ShimFileName);
await File.WriteAllTextAsync(path, ShimSource, Encoding.UTF8, cancellationToken).ConfigureAwait(false);
return path;
}
// NOTE: This shim is intentionally self contained and avoids network calls.
private const string ShimSource = """
// deno-runtime trace shim (offline, deterministic)
// Emits module load, permission use, npm resolution, and wasm load events.
const events: Array<Record<string, unknown>> = [];
function nowIso(): string {
return new Date().toISOString();
}
function addEvent(evt: Record<string, unknown>) {
// Deterministic key order via stringify on object literal insertion order.
events.push(evt);
}
function hashPath(input: string): string {
const data = new TextEncoder().encode(input);
const hash = crypto.subtle.digestSync("SHA-256", data);
return Array.from(new Uint8Array(hash))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
function relPath(abs: string): { normalized: string; path_sha256: string } {
const cwd = Deno.cwd();
const rel = abs.startsWith(cwd) ? abs.slice(cwd.length + 1) : abs;
const normalized = rel.replaceAll("\\", "/");
return { normalized, path_sha256: hashPath(normalized) };
}
// Wrap permission requests
const originalPermissions = Deno.permissions;
Deno.permissions = {
...originalPermissions,
request: async (...args: Parameters<typeof originalPermissions.request>) => {
const res = await originalPermissions.request(...args);
const name = args[0]?.name ?? "unknown";
const module = relPath(import.meta.url);
addEvent({
type: "deno.permission.use",
ts: nowIso(),
permission: name,
module,
details: "permissions.request",
});
return res;
},
query: (...args: Parameters<typeof originalPermissions.query>) =>
originalPermissions.query(...args),
revoke: (...args: Parameters<typeof originalPermissions.revoke>) =>
originalPermissions.revoke(...args),
};
// Hook dynamic import calls by wrapping import()
const originalImport = globalThis.import ?? ((specifier: string) => import(specifier));
globalThis.import = async (specifier: string) => {
const mod = typeof specifier === "string" ? specifier : String(specifier);
addEvent({
type: "deno.module.load",
ts: nowIso(),
module: relPath(mod),
reason: "dynamic-import",
permissions: [],
origin: mod.startsWith("http") ? mod : undefined,
});
return originalImport(specifier);
};
// Hook WebAssembly loads
const originalInstantiate = WebAssembly.instantiate;
WebAssembly.instantiate = async (
bufferSource: BufferSource | WebAssembly.Module,
importObject?: WebAssembly.Imports,
) => {
addEvent({
type: "deno.wasm.load",
ts: nowIso(),
module: relPath("wasm://buffer"),
importer: relPath(import.meta.url).normalized,
reason: "instantiate",
});
return originalInstantiate(bufferSource, importObject);
};
// Capture npm resolution hints from env when present
const npmMeta = Deno.env.get("STELLA_NPM_SPECIFIER");
if (npmMeta) {
addEvent({
type: "deno.npm.resolution",
ts: nowIso(),
specifier: npmMeta,
package: npmMeta,
version: "",
resolved: "file://$DENO_DIR/npm",
exists: true,
});
}
// Write NDJSON on exit
function flush() {
const sorted = events.sort((a, b) => {
const at = String(a.ts);
const bt = String(b.ts);
if (at === bt) return String(a.type).localeCompare(String(b.type));
return at.localeCompare(bt);
});
const data = sorted.map((e) => JSON.stringify(e)).join("\\n") + "\\n";
Deno.writeTextFileSync("deno-runtime.ndjson", data);
}
addEvent({
type: "deno.runtime.start",
ts: nowIso(),
module: relPath(import.meta.url),
reason: "shim-start",
});
globalThis.addEventListener("unload", () => {
flush();
});
""";
}

View File

@@ -0,0 +1,130 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Internal.Runtime;
internal static class DenoRuntimeTraceProbe
{
public static (DenoRuntimeTraceMetadata Metadata, string Sha256) Analyze(byte[] content)
{
var metadata = ComputeMetadata(content);
var sha = ComputeSha256(content);
return (metadata, sha);
}
private static DenoRuntimeTraceMetadata ComputeMetadata(byte[] content)
{
var moduleLoads = 0;
var permissionUses = 0;
var origins = new HashSet<string>(StringComparer.Ordinal);
var permissions = new HashSet<string>(StringComparer.Ordinal);
var npmResolutions = 0;
var wasmLoads = 0;
var dynamicImports = 0;
var events = 0;
if (content is null || content.Length == 0)
{
return new DenoRuntimeTraceMetadata(0, 0, 0, Array.Empty<string>(), Array.Empty<string>(), 0, 0, 0);
}
var span = new ReadOnlySpan<byte>(content);
var start = 0;
while (start < span.Length)
{
var end = span.Slice(start).IndexOf((byte)'\n');
var lineLength = end >= 0 ? end : span.Length - start;
var line = span.Slice(start, lineLength);
start += lineLength + 1;
if (line.IsEmpty)
{
continue;
}
try
{
using var document = JsonDocument.Parse(line);
if (!document.RootElement.TryGetProperty("type", out var typeProp))
{
continue;
}
var type = typeProp.GetString() ?? string.Empty;
events++;
switch (type)
{
case "deno.module.load":
moduleLoads++;
if (document.RootElement.TryGetProperty("origin", out var originProp)
&& originProp.ValueKind == JsonValueKind.String
&& !string.IsNullOrWhiteSpace(originProp.GetString()))
{
origins.Add(originProp.GetString()!);
}
if (document.RootElement.TryGetProperty("reason", out var reasonProp)
&& string.Equals(reasonProp.GetString(), "dynamic-import", StringComparison.Ordinal))
{
dynamicImports++;
}
if (document.RootElement.TryGetProperty("permissions", out var permsProp)
&& permsProp.ValueKind == JsonValueKind.Array)
{
foreach (var perm in permsProp.EnumerateArray())
{
if (perm.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(perm.GetString()))
{
permissions.Add(perm.GetString()!.Trim().ToLowerInvariant());
}
}
}
break;
case "deno.permission.use":
permissionUses++;
if (document.RootElement.TryGetProperty("permission", out var permProp)
&& permProp.ValueKind == JsonValueKind.String
&& !string.IsNullOrWhiteSpace(permProp.GetString()))
{
permissions.Add(permProp.GetString()!.Trim().ToLowerInvariant());
}
break;
case "deno.npm.resolution":
npmResolutions++;
break;
case "deno.wasm.load":
wasmLoads++;
break;
}
}
catch (JsonException)
{
continue;
}
}
return new DenoRuntimeTraceMetadata(
EventCount: events,
ModuleLoads: moduleLoads,
PermissionUses: permissionUses,
RemoteOrigins: origins.OrderBy(o => o, StringComparer.Ordinal).ToArray(),
UniquePermissions: permissions.OrderBy(p => p, StringComparer.Ordinal).ToArray(),
NpmResolutions: npmResolutions,
WasmLoads: wasmLoads,
DynamicImports: dynamicImports);
}
private static string ComputeSha256(byte[] content)
{
using var sha = SHA256.Create();
var hash = sha.ComputeHash(content ?? Array.Empty<byte>());
return Convert.ToHexString(hash).ToLowerInvariant();
}
}

View File

@@ -406,12 +406,29 @@ public sealed class JavaLanguageAnalyzer : ILanguageAnalyzer
var locator = BuildLocator(archive, entry.OriginalPath);
locators.Add(locator);
var sha256 = TryComputeSha256(archive, entry);
evidence.Add(new LanguageComponentEvidence(
LanguageEvidenceKind.File,
"framework-config",
locator,
value: null,
sha256: null));
sha256: sha256));
}
private static string? TryComputeSha256(JavaArchive archive, JavaArchiveEntry entry)
{
try
{
using var stream = archive.OpenEntry(entry);
using var sha = SHA256.Create();
var hash = sha.ComputeHash(stream);
return Convert.ToHexString(hash).ToLowerInvariant();
}
catch
{
return null;
}
}
private static bool IsSpringFactories(string path)

View File

@@ -19,7 +19,8 @@ internal sealed class NodePackage
bool declaredOnly = false,
string? lockSource = null,
string? lockLocator = null,
string? packageSha256 = null)
string? packageSha256 = null,
bool isYarnPnp = false)
{
Name = name;
Version = version;
@@ -38,6 +39,7 @@ internal sealed class NodePackage
LockSource = lockSource;
LockLocator = lockLocator;
PackageSha256 = packageSha256;
IsYarnPnp = isYarnPnp;
}
public string Name { get; }
@@ -75,8 +77,10 @@ internal sealed class NodePackage
public string? LockLocator { get; }
public string? PackageSha256 { get; }
public string RelativePathNormalized => string.IsNullOrEmpty(RelativePath) ? string.Empty : RelativePath.Replace(Path.DirectorySeparatorChar, '/');
public bool IsYarnPnp { get; }
public string RelativePathNormalized => string.IsNullOrEmpty(RelativePath) ? string.Empty : RelativePath.Replace(Path.DirectorySeparatorChar, '/');
public string ComponentKey => $"purl::{Purl}";
@@ -217,6 +221,11 @@ internal sealed class NodePackage
entries.Add(new KeyValuePair<string, string?>("lockLocator", LockLocator));
}
if (IsYarnPnp)
{
entries.Add(new KeyValuePair<string, string?>("yarnPnp", "true"));
}
return entries
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
.ToArray();

View File

@@ -1,175 +1,179 @@
using System.Text.Json;
namespace StellaOps.Scanner.Analyzers.Lang.Node.Internal;
internal static class NodePackageCollector
{
private static readonly string[] IgnoredDirectories =
{
".bin",
".cache",
".store",
"__pycache__"
};
public static IReadOnlyList<NodePackage> CollectPackages(LanguageAnalyzerContext context, NodeLockData lockData, CancellationToken cancellationToken)
{
var packages = new List<NodePackage>();
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var pendingNodeModuleRoots = new List<string>();
var rootPackageJson = Path.Combine(context.RootPath, "package.json");
var workspaceIndex = NodeWorkspaceIndex.Create(context.RootPath);
if (File.Exists(rootPackageJson))
{
var rootPackage = TryCreatePackage(context, rootPackageJson, string.Empty, lockData, workspaceIndex, cancellationToken);
if (rootPackage is not null)
{
packages.Add(rootPackage);
visited.Add(rootPackage.RelativePathNormalized);
}
}
foreach (var workspaceRelative in workspaceIndex.GetMembers())
{
var workspaceAbsolute = Path.Combine(context.RootPath, workspaceRelative.Replace('/', Path.DirectorySeparatorChar));
if (!Directory.Exists(workspaceAbsolute))
{
continue;
}
ProcessPackageDirectory(context, workspaceAbsolute, lockData, workspaceIndex, includeNestedNodeModules: false, packages, visited, cancellationToken);
var workspaceNodeModules = Path.Combine(workspaceAbsolute, "node_modules");
if (Directory.Exists(workspaceNodeModules))
{
pendingNodeModuleRoots.Add(workspaceNodeModules);
}
}
using System.Text.Json;
namespace StellaOps.Scanner.Analyzers.Lang.Node.Internal;
internal static class NodePackageCollector
{
private static readonly string[] IgnoredDirectories =
{
".bin",
".cache",
".store",
"__pycache__"
};
public static IReadOnlyList<NodePackage> CollectPackages(LanguageAnalyzerContext context, NodeLockData lockData, CancellationToken cancellationToken)
{
var packages = new List<NodePackage>();
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var pendingNodeModuleRoots = new List<string>();
var rootPackageJson = Path.Combine(context.RootPath, "package.json");
var workspaceIndex = NodeWorkspaceIndex.Create(context.RootPath);
var yarnPnpPresent = HasYarnPnp(context.RootPath);
if (File.Exists(rootPackageJson))
{
var rootPackage = TryCreatePackage(context, rootPackageJson, string.Empty, lockData, workspaceIndex, yarnPnpPresent, cancellationToken);
if (rootPackage is not null)
{
packages.Add(rootPackage);
visited.Add(rootPackage.RelativePathNormalized);
}
}
foreach (var workspaceRelative in workspaceIndex.GetMembers())
{
var workspaceAbsolute = Path.Combine(context.RootPath, workspaceRelative.Replace('/', Path.DirectorySeparatorChar));
if (!Directory.Exists(workspaceAbsolute))
{
continue;
}
ProcessPackageDirectory(context, workspaceAbsolute, lockData, workspaceIndex, includeNestedNodeModules: false, packages, visited, yarnPnpPresent, cancellationToken);
var workspaceNodeModules = Path.Combine(workspaceAbsolute, "node_modules");
if (Directory.Exists(workspaceNodeModules))
{
pendingNodeModuleRoots.Add(workspaceNodeModules);
}
}
var nodeModules = Path.Combine(context.RootPath, "node_modules");
TraverseDirectory(context, nodeModules, lockData, workspaceIndex, packages, visited, cancellationToken);
TraverseDirectory(context, nodeModules, lockData, workspaceIndex, packages, visited, yarnPnpPresent, cancellationToken);
foreach (var pendingRoot in pendingNodeModuleRoots.OrderBy(static path => path, StringComparer.Ordinal))
{
TraverseDirectory(context, pendingRoot, lockData, workspaceIndex, packages, visited, cancellationToken);
TraverseDirectory(context, pendingRoot, lockData, workspaceIndex, packages, visited, yarnPnpPresent, cancellationToken);
}
TraverseTarballs(context, lockData, workspaceIndex, packages, visited, cancellationToken);
TraverseTarballs(context, lockData, workspaceIndex, packages, visited, yarnPnpPresent, cancellationToken);
AppendDeclaredPackages(packages, lockData);
return packages;
}
private static void TraverseDirectory(
LanguageAnalyzerContext context,
string directory,
NodeLockData lockData,
NodeWorkspaceIndex workspaceIndex,
List<NodePackage> packages,
HashSet<string> visited,
CancellationToken cancellationToken)
{
if (!Directory.Exists(directory))
{
return;
}
foreach (var child in Directory.EnumerateDirectories(directory))
{
cancellationToken.ThrowIfCancellationRequested();
var name = Path.GetFileName(child);
if (string.IsNullOrEmpty(name))
{
continue;
}
if (ShouldSkipDirectory(name))
{
continue;
}
if (string.Equals(name, ".pnpm", StringComparison.OrdinalIgnoreCase))
{
TraversePnpmStore(context, child, lockData, workspaceIndex, packages, visited, cancellationToken);
continue;
}
if (name.StartsWith('@'))
{
foreach (var scoped in Directory.EnumerateDirectories(child))
{
ProcessPackageDirectory(context, scoped, lockData, workspaceIndex, includeNestedNodeModules: true, packages, visited, cancellationToken);
}
continue;
}
ProcessPackageDirectory(context, child, lockData, workspaceIndex, includeNestedNodeModules: true, packages, visited, cancellationToken);
}
}
private static void TraversePnpmStore(
LanguageAnalyzerContext context,
string pnpmDirectory,
NodeLockData lockData,
NodeWorkspaceIndex workspaceIndex,
List<NodePackage> packages,
HashSet<string> visited,
CancellationToken cancellationToken)
{
foreach (var storeEntry in Directory.EnumerateDirectories(pnpmDirectory))
{
cancellationToken.ThrowIfCancellationRequested();
var nestedNodeModules = Path.Combine(storeEntry, "node_modules");
if (Directory.Exists(nestedNodeModules))
{
TraverseDirectory(context, nestedNodeModules, lockData, workspaceIndex, packages, visited, cancellationToken);
}
}
}
private static void ProcessPackageDirectory(
LanguageAnalyzerContext context,
string directory,
NodeLockData lockData,
NodeWorkspaceIndex workspaceIndex,
bool includeNestedNodeModules,
List<NodePackage> packages,
HashSet<string> visited,
CancellationToken cancellationToken)
{
var packageJsonPath = Path.Combine(directory, "package.json");
var relativeDirectory = NormalizeRelativeDirectory(context, directory);
if (!visited.Add(relativeDirectory))
{
// Already processed this path.
if (includeNestedNodeModules)
{
TraverseNestedNodeModules(context, directory, lockData, workspaceIndex, packages, visited, cancellationToken);
}
return;
}
if (File.Exists(packageJsonPath))
{
var package = TryCreatePackage(context, packageJsonPath, relativeDirectory, lockData, workspaceIndex, cancellationToken);
if (package is not null)
{
packages.Add(package);
}
}
if (includeNestedNodeModules)
{
TraverseNestedNodeModules(context, directory, lockData, workspaceIndex, packages, visited, cancellationToken);
}
}
private static void TraverseDirectory(
LanguageAnalyzerContext context,
string directory,
NodeLockData lockData,
NodeWorkspaceIndex workspaceIndex,
List<NodePackage> packages,
HashSet<string> visited,
bool yarnPnpPresent,
CancellationToken cancellationToken)
{
if (!Directory.Exists(directory))
{
return;
}
foreach (var child in Directory.EnumerateDirectories(directory))
{
cancellationToken.ThrowIfCancellationRequested();
var name = Path.GetFileName(child);
if (string.IsNullOrEmpty(name))
{
continue;
}
if (ShouldSkipDirectory(name))
{
continue;
}
if (string.Equals(name, ".pnpm", StringComparison.OrdinalIgnoreCase))
{
TraversePnpmStore(context, child, lockData, workspaceIndex, packages, visited, yarnPnpPresent, cancellationToken);
continue;
}
if (name.StartsWith('@'))
{
foreach (var scoped in Directory.EnumerateDirectories(child))
{
ProcessPackageDirectory(context, scoped, lockData, workspaceIndex, includeNestedNodeModules: true, packages, visited, yarnPnpPresent, cancellationToken);
}
continue;
}
ProcessPackageDirectory(context, child, lockData, workspaceIndex, includeNestedNodeModules: true, packages, visited, yarnPnpPresent, cancellationToken);
}
}
private static void TraversePnpmStore(
LanguageAnalyzerContext context,
string pnpmDirectory,
NodeLockData lockData,
NodeWorkspaceIndex workspaceIndex,
List<NodePackage> packages,
HashSet<string> visited,
bool yarnPnpPresent,
CancellationToken cancellationToken)
{
foreach (var storeEntry in Directory.EnumerateDirectories(pnpmDirectory))
{
cancellationToken.ThrowIfCancellationRequested();
var nestedNodeModules = Path.Combine(storeEntry, "node_modules");
if (Directory.Exists(nestedNodeModules))
{
TraverseDirectory(context, nestedNodeModules, lockData, workspaceIndex, packages, visited, yarnPnpPresent, cancellationToken);
}
}
}
private static void ProcessPackageDirectory(
LanguageAnalyzerContext context,
string directory,
NodeLockData lockData,
NodeWorkspaceIndex workspaceIndex,
bool includeNestedNodeModules,
List<NodePackage> packages,
HashSet<string> visited,
bool yarnPnpPresent,
CancellationToken cancellationToken)
{
var packageJsonPath = Path.Combine(directory, "package.json");
var relativeDirectory = NormalizeRelativeDirectory(context, directory);
if (!visited.Add(relativeDirectory))
{
// Already processed this path.
if (includeNestedNodeModules)
{
TraverseNestedNodeModules(context, directory, lockData, workspaceIndex, packages, visited, yarnPnpPresent, cancellationToken);
}
return;
}
if (File.Exists(packageJsonPath))
{
var package = TryCreatePackage(context, packageJsonPath, relativeDirectory, lockData, workspaceIndex, yarnPnpPresent, cancellationToken);
if (package is not null)
{
packages.Add(package);
}
}
if (includeNestedNodeModules)
{
TraverseNestedNodeModules(context, directory, lockData, workspaceIndex, packages, visited, yarnPnpPresent, cancellationToken);
}
}
private static void TraverseNestedNodeModules(
LanguageAnalyzerContext context,
string directory,
@@ -177,10 +181,11 @@ internal static class NodePackageCollector
NodeWorkspaceIndex workspaceIndex,
List<NodePackage> packages,
HashSet<string> visited,
bool yarnPnpPresent,
CancellationToken cancellationToken)
{
var nestedNodeModules = Path.Combine(directory, "node_modules");
TraverseDirectory(context, nestedNodeModules, lockData, workspaceIndex, packages, visited, cancellationToken);
TraverseDirectory(context, nestedNodeModules, lockData, workspaceIndex, packages, visited, yarnPnpPresent, cancellationToken);
}
private static void TraverseTarballs(
@@ -189,6 +194,7 @@ internal static class NodePackageCollector
NodeWorkspaceIndex workspaceIndex,
List<NodePackage> packages,
HashSet<string> visited,
bool yarnPnpPresent,
CancellationToken cancellationToken)
{
var enumerationOptions = new EnumerationOptions
@@ -201,7 +207,7 @@ internal static class NodePackageCollector
foreach (var tgzPath in Directory.EnumerateFiles(context.RootPath, "*.tgz", enumerationOptions))
{
cancellationToken.ThrowIfCancellationRequested();
TryProcessTarball(context, tgzPath, packages, visited, cancellationToken);
TryProcessTarball(context, tgzPath, packages, visited, yarnPnpPresent, cancellationToken);
}
}
@@ -210,6 +216,7 @@ internal static class NodePackageCollector
string tgzPath,
List<NodePackage> packages,
HashSet<string> visited,
bool yarnPnpPresent,
CancellationToken cancellationToken)
{
try
@@ -256,7 +263,8 @@ internal static class NodePackageCollector
locator,
usedByEntrypoint,
cancellationToken,
packageSha256: sha256Hex);
packageSha256: sha256Hex,
yarnPnpPresent: yarnPnpPresent);
if (package is null)
{
@@ -361,13 +369,20 @@ internal static class NodePackageCollector
return $"{entry.Source}:{entry.Locator}";
}
private static bool HasYarnPnp(string rootPath)
{
return File.Exists(Path.Combine(rootPath, ".pnp.cjs"))
|| File.Exists(Path.Combine(rootPath, ".pnp.data.cjs"));
}
private static NodePackage? TryCreatePackage(
LanguageAnalyzerContext context,
string packageJsonPath,
string relativeDirectory,
NodeLockData lockData,
NodeWorkspaceIndex workspaceIndex,
bool yarnPnpPresent,
CancellationToken cancellationToken)
{
try
@@ -385,16 +400,18 @@ internal static class NodePackageCollector
cancellationToken,
lockData,
workspaceIndex,
packageJsonPath);
packageJsonPath,
packageSha256: null,
yarnPnpPresent: yarnPnpPresent);
}
catch (IOException)
{
return null;
}
catch (JsonException)
{
return null;
}
catch (JsonException)
{
return null;
}
}
private static NodePackage? TryCreatePackageFromJson(
@@ -407,7 +424,8 @@ internal static class NodePackageCollector
NodeLockData? lockData = null,
NodeWorkspaceIndex? workspaceIndex = null,
string? packageJsonPath = null,
string? packageSha256 = null)
string? packageSha256 = null,
bool yarnPnpPresent = false)
{
if (!root.TryGetProperty("name", out var nameElement))
{
@@ -467,20 +485,21 @@ internal static class NodePackageCollector
declaredOnly: false,
lockSource: lockSource,
lockLocator: lockLocator,
packageSha256: packageSha256);
packageSha256: packageSha256,
isYarnPnp: yarnPnpPresent);
}
private static string NormalizeRelativeDirectory(LanguageAnalyzerContext context, string directory)
{
var relative = context.GetRelativePath(directory);
if (string.IsNullOrEmpty(relative) || relative == ".")
{
return string.Empty;
}
return relative.Replace(Path.DirectorySeparatorChar, '/');
}
private static string NormalizeRelativeDirectory(LanguageAnalyzerContext context, string directory)
{
var relative = context.GetRelativePath(directory);
if (string.IsNullOrEmpty(relative) || relative == ".")
{
return string.Empty;
}
return relative.Replace(Path.DirectorySeparatorChar, '/');
}
private static string BuildLocator(string relativeDirectory)
{
if (string.IsNullOrEmpty(relativeDirectory))
@@ -512,103 +531,119 @@ internal static class NodePackageCollector
return relative.Replace(Path.DirectorySeparatorChar, '/');
}
private static bool ShouldSkipDirectory(string name)
{
if (name.Length == 0)
{
return true;
}
if (name[0] == '.')
{
return !string.Equals(name, ".pnpm", StringComparison.OrdinalIgnoreCase);
}
return IgnoredDirectories.Any(ignored => string.Equals(name, ignored, StringComparison.OrdinalIgnoreCase));
}
private static IReadOnlyList<string> ExtractWorkspaceTargets(string relativeDirectory, JsonElement root, NodeWorkspaceIndex workspaceIndex)
{
var dependencies = workspaceIndex.ResolveWorkspaceTargets(relativeDirectory, TryGetProperty(root, "dependencies"));
var devDependencies = workspaceIndex.ResolveWorkspaceTargets(relativeDirectory, TryGetProperty(root, "devDependencies"));
var peerDependencies = workspaceIndex.ResolveWorkspaceTargets(relativeDirectory, TryGetProperty(root, "peerDependencies"));
if (dependencies.Count == 0 && devDependencies.Count == 0 && peerDependencies.Count == 0)
{
return Array.Empty<string>();
}
var combined = new HashSet<string>(StringComparer.Ordinal);
foreach (var item in dependencies)
{
combined.Add(item);
}
foreach (var item in devDependencies)
{
combined.Add(item);
}
foreach (var item in peerDependencies)
{
combined.Add(item);
}
return combined.OrderBy(static x => x, StringComparer.Ordinal).ToArray();
}
private static JsonElement? TryGetProperty(JsonElement element, string propertyName)
=> element.TryGetProperty(propertyName, out var property) ? property : null;
private static IReadOnlyList<NodeLifecycleScript> ExtractLifecycleScripts(JsonElement root)
{
if (!root.TryGetProperty("scripts", out var scriptsElement) || scriptsElement.ValueKind != JsonValueKind.Object)
{
return Array.Empty<NodeLifecycleScript>();
}
var lifecycleScripts = new Dictionary<string, NodeLifecycleScript>(StringComparer.OrdinalIgnoreCase);
foreach (var script in scriptsElement.EnumerateObject())
{
if (!IsLifecycleScriptName(script.Name))
{
continue;
}
if (script.Value.ValueKind != JsonValueKind.String)
{
continue;
}
var command = script.Value.GetString();
if (string.IsNullOrWhiteSpace(command))
{
continue;
}
var canonicalName = script.Name.Trim().ToLowerInvariant();
var lifecycleScript = new NodeLifecycleScript(canonicalName, command);
if (!lifecycleScripts.ContainsKey(canonicalName))
{
NodeAnalyzerMetrics.RecordLifecycleScript(canonicalName);
}
lifecycleScripts[canonicalName] = lifecycleScript;
}
if (lifecycleScripts.Count == 0)
{
return Array.Empty<NodeLifecycleScript>();
}
return lifecycleScripts.Values
.OrderBy(static script => script.Name, StringComparer.Ordinal)
.ToArray();
}
private static bool IsLifecycleScriptName(string name)
=> name.Equals("preinstall", StringComparison.OrdinalIgnoreCase)
|| name.Equals("install", StringComparison.OrdinalIgnoreCase)
|| name.Equals("postinstall", StringComparison.OrdinalIgnoreCase);
}
private static bool ShouldSkipDirectory(string name)
{
if (name.Length == 0)
{
return true;
}
if (name[0] == '.')
{
return !string.Equals(name, ".pnpm", StringComparison.OrdinalIgnoreCase);
}
return IgnoredDirectories.Any(ignored => string.Equals(name, ignored, StringComparison.OrdinalIgnoreCase));
}
private static bool HasYarnPnp(string rootPath)
{
if (string.IsNullOrWhiteSpace(rootPath))
{
return false;
}
var pnpCjs = Path.Combine(rootPath, ".pnp.cjs");
var pnpData = Path.Combine(rootPath, ".pnp.data.json");
var yarnCache = Path.Combine(rootPath, ".yarn", "cache");
return File.Exists(pnpCjs)
|| File.Exists(pnpData)
|| Directory.Exists(yarnCache);
}
private static IReadOnlyList<string> ExtractWorkspaceTargets(string relativeDirectory, JsonElement root, NodeWorkspaceIndex workspaceIndex)
{
var dependencies = workspaceIndex.ResolveWorkspaceTargets(relativeDirectory, TryGetProperty(root, "dependencies"));
var devDependencies = workspaceIndex.ResolveWorkspaceTargets(relativeDirectory, TryGetProperty(root, "devDependencies"));
var peerDependencies = workspaceIndex.ResolveWorkspaceTargets(relativeDirectory, TryGetProperty(root, "peerDependencies"));
if (dependencies.Count == 0 && devDependencies.Count == 0 && peerDependencies.Count == 0)
{
return Array.Empty<string>();
}
var combined = new HashSet<string>(StringComparer.Ordinal);
foreach (var item in dependencies)
{
combined.Add(item);
}
foreach (var item in devDependencies)
{
combined.Add(item);
}
foreach (var item in peerDependencies)
{
combined.Add(item);
}
return combined.OrderBy(static x => x, StringComparer.Ordinal).ToArray();
}
private static JsonElement? TryGetProperty(JsonElement element, string propertyName)
=> element.TryGetProperty(propertyName, out var property) ? property : null;
private static IReadOnlyList<NodeLifecycleScript> ExtractLifecycleScripts(JsonElement root)
{
if (!root.TryGetProperty("scripts", out var scriptsElement) || scriptsElement.ValueKind != JsonValueKind.Object)
{
return Array.Empty<NodeLifecycleScript>();
}
var lifecycleScripts = new Dictionary<string, NodeLifecycleScript>(StringComparer.OrdinalIgnoreCase);
foreach (var script in scriptsElement.EnumerateObject())
{
if (!IsLifecycleScriptName(script.Name))
{
continue;
}
if (script.Value.ValueKind != JsonValueKind.String)
{
continue;
}
var command = script.Value.GetString();
if (string.IsNullOrWhiteSpace(command))
{
continue;
}
var canonicalName = script.Name.Trim().ToLowerInvariant();
var lifecycleScript = new NodeLifecycleScript(canonicalName, command);
if (!lifecycleScripts.ContainsKey(canonicalName))
{
NodeAnalyzerMetrics.RecordLifecycleScript(canonicalName);
}
lifecycleScripts[canonicalName] = lifecycleScript;
}
if (lifecycleScripts.Count == 0)
{
return Array.Empty<NodeLifecycleScript>();
}
return lifecycleScripts.Values
.OrderBy(static script => script.Name, StringComparer.Ordinal)
.ToArray();
}
private static bool IsLifecycleScriptName(string name)
=> name.Equals("preinstall", StringComparison.OrdinalIgnoreCase)
|| name.Equals("install", StringComparison.OrdinalIgnoreCase)
|| name.Equals("postinstall", StringComparison.OrdinalIgnoreCase);
}