Files
git.stella-ops.org/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/Internal/DenoModuleGraphResolver.cs
master 69c59defdc
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
feat: Implement Runtime Facts ingestion service and NDJSON reader
- 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.
2025-11-10 07:56:15 +02:00

710 lines
27 KiB
C#

namespace StellaOps.Scanner.Analyzers.Lang.Deno.Internal;
internal static class DenoModuleGraphResolver
{
public static DenoModuleGraph Resolve(DenoWorkspace workspace, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(workspace);
var builder = new GraphBuilder(cancellationToken);
builder.AddWorkspace(workspace);
return builder.Build();
}
private sealed class GraphBuilder
{
private readonly CancellationToken _cancellationToken;
private readonly Dictionary<string, DenoModuleNode> _nodes = new(StringComparer.Ordinal);
private readonly List<DenoModuleEdge> _edges = new();
public GraphBuilder(CancellationToken cancellationToken)
{
_cancellationToken = cancellationToken;
}
public DenoModuleGraph Build()
=> new(_nodes.Values, _edges);
public void AddWorkspace(DenoWorkspace workspace)
{
AddConfigurations(workspace);
AddImportMaps(workspace);
AddLockFiles(workspace);
AddVendors(workspace);
AddCacheLocations(workspace);
}
private void AddConfigurations(DenoWorkspace workspace)
{
foreach (var config in workspace.Configurations)
{
_cancellationToken.ThrowIfCancellationRequested();
var configNodeId = GetOrAddNode(
$"config::{config.RelativePath}",
config.RelativePath,
DenoModuleKind.WorkspaceConfig,
config.AbsolutePath,
layerDigest: null,
integrity: null,
metadata: new Dictionary<string, string?>()
{
["vendor.enabled"] = config.VendorEnabled.ToString(CultureInfo.InvariantCulture),
["lock.enabled"] = config.LockEnabled.ToString(CultureInfo.InvariantCulture),
["nodeModules.enabled"] = config.NodeModulesDirEnabled.ToString(CultureInfo.InvariantCulture),
});
if (config.ImportMapPath is not null)
{
var importMapNodeId = GetOrAddNode(
$"import-map::{NormalizePath(config.ImportMapPath)}",
config.ImportMapPath,
DenoModuleKind.ImportMap,
config.ImportMapPath,
layerDigest: null,
integrity: null,
metadata: ImmutableDictionary<string, string?>.Empty);
AddEdge(
configNodeId,
importMapNodeId,
DenoImportKind.Static,
specifier: "importMap",
provenance: $"config:{config.RelativePath}",
resolution: config.ImportMapPath);
}
if (config.InlineImportMap is not null)
{
var inlineNodeId = GetOrAddNode(
$"import-map::{config.RelativePath}#inline",
$"{config.RelativePath} (inline import map)",
DenoModuleKind.ImportMap,
config.RelativePath,
layerDigest: null,
integrity: null,
metadata: ImmutableDictionary<string, string?>.Empty);
AddEdge(
configNodeId,
inlineNodeId,
DenoImportKind.Static,
specifier: "importMap:inline",
provenance: $"config:{config.RelativePath}",
resolution: config.RelativePath);
}
if (config.LockFilePath is not null)
{
var lockNodeId = GetOrAddNode(
$"lock::{NormalizePath(config.LockFilePath)}",
config.LockFilePath,
DenoModuleKind.LockFile,
config.LockFilePath,
layerDigest: null,
integrity: null,
metadata: ImmutableDictionary<string, string?>.Empty);
AddEdge(
configNodeId,
lockNodeId,
DenoImportKind.Static,
specifier: "lock",
provenance: $"config:{config.RelativePath}",
resolution: config.LockFilePath);
}
}
}
private void AddImportMaps(DenoWorkspace workspace)
{
foreach (var importMap in workspace.ImportMaps)
{
_cancellationToken.ThrowIfCancellationRequested();
var importMapNodeId = GetOrAddNode(
$"import-map::{importMap.SortKey}",
importMap.Origin,
DenoModuleKind.ImportMap,
importMap.AbsolutePath,
layerDigest: null,
integrity: null,
metadata: ImmutableDictionary<string, string?>.Empty);
foreach (var entry in importMap.Imports)
{
var aliasNodeId = GetOrAddAliasNode(entry.Key, importMapNodeId);
var targetNodeId = GetOrAddTargetNode(entry.Value);
AddEdge(
aliasNodeId,
targetNodeId,
DetermineImportKind(entry.Value),
entry.Key,
provenance: $"import-map:{importMap.SortKey}",
resolution: entry.Value);
}
foreach (var scope in importMap.Scopes)
{
foreach (var scopedEntry in scope.Value)
{
var aliasNodeId = GetOrAddAliasNode($"{scope.Key}:{scopedEntry.Key}", importMapNodeId);
var targetNodeId = GetOrAddTargetNode(scopedEntry.Value);
AddEdge(
aliasNodeId,
targetNodeId,
DetermineImportKind(scopedEntry.Value),
scopedEntry.Key,
provenance: $"import-map-scope:{scope.Key}",
resolution: scopedEntry.Value);
}
}
}
}
private void AddLockFiles(DenoWorkspace workspace)
{
foreach (var lockFile in workspace.LockFiles)
{
_cancellationToken.ThrowIfCancellationRequested();
var lockNodeId = GetOrAddNode(
$"lock::{lockFile.RelativePath}",
lockFile.RelativePath,
DenoModuleKind.LockFile,
lockFile.AbsolutePath,
layerDigest: null,
integrity: null,
metadata: new Dictionary<string, string?>()
{
["lock.version"] = lockFile.Version,
});
foreach (var remote in lockFile.RemoteEntries)
{
var aliasNodeId = GetOrAddAliasNode(remote.Key, lockNodeId);
var remoteNodeId = GetOrAddNode(
$"remote::{remote.Key}",
remote.Key,
DenoModuleKind.RemoteModule,
remote.Key,
layerDigest: null,
integrity: remote.Value,
metadata: ImmutableDictionary<string, string?>.Empty);
AddEdge(
aliasNodeId,
remoteNodeId,
DetermineImportKind(remote.Key),
remote.Key,
provenance: $"lock:remote:{lockFile.RelativePath}",
resolution: remote.Key);
}
foreach (var redirect in lockFile.Redirects)
{
var fromNodeId = GetOrAddAliasNode(redirect.Key, lockNodeId);
var toNodeId = GetOrAddAliasNode(redirect.Value, lockNodeId);
AddEdge(
fromNodeId,
toNodeId,
DenoImportKind.Redirect,
redirect.Key,
provenance: $"lock:redirect:{lockFile.RelativePath}",
resolution: redirect.Value);
}
foreach (var npmSpecifier in lockFile.NpmSpecifiers)
{
var aliasNodeId = GetOrAddNode(
$"npm-spec::{npmSpecifier.Key}",
npmSpecifier.Key,
DenoModuleKind.NpmSpecifier,
npmSpecifier.Key,
layerDigest: null,
integrity: null,
metadata: ImmutableDictionary<string, string?>.Empty);
var packageNodeId = GetOrAddNode(
$"npm::{npmSpecifier.Value}",
npmSpecifier.Value,
DenoModuleKind.NpmPackage,
npmSpecifier.Value,
layerDigest: null,
integrity: lockFile.NpmPackages.TryGetValue(npmSpecifier.Value, out var pkg) ? pkg.Integrity : null,
metadata: ImmutableDictionary<string, string?>.Empty);
AddEdge(
aliasNodeId,
packageNodeId,
DenoImportKind.NpmBridge,
npmSpecifier.Key,
provenance: $"lock:npm-spec:{lockFile.RelativePath}",
resolution: npmSpecifier.Value);
}
foreach (var package in lockFile.NpmPackages)
{
var packageNodeId = GetOrAddNode(
$"npm::{package.Key}",
package.Key,
DenoModuleKind.NpmPackage,
package.Key,
layerDigest: null,
integrity: package.Value.Integrity,
metadata: ImmutableDictionary<string, string?>.Empty);
foreach (var dependency in package.Value.Dependencies)
{
var dependencyNodeId = GetOrAddNode(
$"npm::{dependency.Value}",
dependency.Value,
DenoModuleKind.NpmPackage,
dependency.Value,
layerDigest: null,
integrity: lockFile.NpmPackages.TryGetValue(dependency.Value, out var depPkg) ? depPkg.Integrity : null,
metadata: ImmutableDictionary<string, string?>.Empty);
AddEdge(
packageNodeId,
dependencyNodeId,
DenoImportKind.Dependency,
dependency.Key,
provenance: $"lock:npm-package:{lockFile.RelativePath}",
resolution: dependency.Value);
}
}
}
}
private void AddVendors(DenoWorkspace workspace)
{
foreach (var vendor in workspace.Vendors)
{
_cancellationToken.ThrowIfCancellationRequested();
var metadata = new Dictionary<string, string?>(StringComparer.Ordinal)
{
["vendor.alias"] = vendor.Alias,
};
if (vendor.ImportMap is not null)
{
var vendorMapId = GetOrAddNode(
$"vendor-map::{vendor.Alias}",
$"{vendor.Alias} import map",
DenoModuleKind.ImportMap,
vendor.ImportMap.AbsolutePath,
vendor.LayerDigest,
integrity: null,
metadata);
var vendorRootId = GetOrAddNode(
$"vendor-root::{vendor.Alias}",
vendor.RelativePath,
DenoModuleKind.VendorModule,
vendor.AbsolutePath,
vendor.LayerDigest,
integrity: null,
metadata);
AddEdge(
vendorRootId,
vendorMapId,
DenoImportKind.Cache,
vendor.ImportMap.Origin,
provenance: $"vendor:{vendor.Alias}",
resolution: vendor.ImportMap.AbsolutePath);
}
foreach (var file in SafeEnumerateFiles(vendor.AbsolutePath))
{
_cancellationToken.ThrowIfCancellationRequested();
var relative = Path.GetRelativePath(vendor.AbsolutePath, file);
var nodeId = GetOrAddNode(
$"vendor::{vendor.Alias}/{NormalizePath(relative)}",
$"{vendor.Alias}/{NormalizePath(relative)}",
DenoModuleKind.VendorModule,
file,
vendor.LayerDigest,
integrity: null,
metadata);
if (TryResolveUrlFromVendorPath(vendor.RelativePath, relative, out var url) ||
TryResolveUrlFromVendorPath(vendor.RelativePath, $"https/{relative}", out url))
{
var remoteNodeId = GetOrAddNode(
$"remote::{url}",
url,
DenoModuleKind.RemoteModule,
url,
vendor.LayerDigest,
integrity: null,
metadata: ImmutableDictionary<string, string?>.Empty);
AddEdge(
remoteNodeId,
nodeId,
DenoImportKind.Cache,
url,
provenance: $"vendor-cache:{vendor.Alias}",
resolution: file);
}
}
}
}
private void AddCacheLocations(DenoWorkspace workspace)
{
foreach (var cache in workspace.CacheLocations)
{
_cancellationToken.ThrowIfCancellationRequested();
foreach (var file in SafeEnumerateFiles(cache.AbsolutePath))
{
_cancellationToken.ThrowIfCancellationRequested();
var relative = Path.GetRelativePath(cache.AbsolutePath, file);
var nodeId = GetOrAddNode(
$"cache::{cache.Alias}/{NormalizePath(relative)}",
$"{cache.Alias}/{NormalizePath(relative)}",
DenoModuleKind.CacheEntry,
file,
cache.LayerDigest,
integrity: null,
metadata: new Dictionary<string, string?>(StringComparer.Ordinal)
{
["cache.kind"] = cache.Kind.ToString(),
});
if (TryResolveUrlFromCachePath(relative, out var url))
{
var remoteNodeId = GetOrAddNode(
$"remote::{url}",
url,
DenoModuleKind.RemoteModule,
url,
cache.LayerDigest,
integrity: null,
metadata: ImmutableDictionary<string, string?>.Empty);
AddEdge(
remoteNodeId,
nodeId,
DenoImportKind.Cache,
url,
provenance: $"cache:{cache.Kind}",
resolution: file);
}
}
}
}
private string GetOrAddAliasNode(string specifier, string ownerNodeId)
{
var normalized = NormalizeSpecifier(specifier);
if (_nodes.ContainsKey(normalized))
{
return normalized;
}
var metadata = new Dictionary<string, string?>(StringComparer.Ordinal)
{
["owner"] = ownerNodeId,
};
var kind = specifier.StartsWith("node:", StringComparison.OrdinalIgnoreCase) ||
specifier.StartsWith("deno:", StringComparison.OrdinalIgnoreCase)
? DenoModuleKind.BuiltInModule
: DenoModuleKind.SpecifierAlias;
return GetOrAddNode(
normalized,
specifier,
kind,
specifier,
layerDigest: null,
integrity: null,
metadata);
}
private string GetOrAddTargetNode(string target)
{
if (string.IsNullOrWhiteSpace(target))
{
return GetOrAddNode(
"unknown::(empty)",
"(empty)",
DenoModuleKind.Unknown,
reference: null,
layerDigest: null,
integrity: null,
metadata: ImmutableDictionary<string, string?>.Empty);
}
if (target.StartsWith("npm:", StringComparison.OrdinalIgnoreCase))
{
return GetOrAddNode(
$"npm::{target[4..]}",
target,
DenoModuleKind.NpmPackage,
target,
layerDigest: null,
integrity: null,
metadata: ImmutableDictionary<string, string?>.Empty);
}
if (target.StartsWith("node:", StringComparison.OrdinalIgnoreCase) ||
target.StartsWith("deno:", StringComparison.OrdinalIgnoreCase))
{
return GetOrAddNode(
$"builtin::{target}",
target,
DenoModuleKind.BuiltInModule,
target,
layerDigest: null,
integrity: null,
metadata: ImmutableDictionary<string, string?>.Empty);
}
if (target.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
target.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
return GetOrAddNode(
$"remote::{target}",
target,
DenoModuleKind.RemoteModule,
target,
layerDigest: null,
integrity: null,
metadata: ImmutableDictionary<string, string?>.Empty);
}
if (target.StartsWith("./", StringComparison.Ordinal) || target.StartsWith("../", StringComparison.Ordinal) || target.StartsWith("/", StringComparison.Ordinal))
{
return GetOrAddNode(
$"workspace::{NormalizePath(target)}",
target,
DenoModuleKind.WorkspaceModule,
target,
layerDigest: null,
integrity: null,
metadata: ImmutableDictionary<string, string?>.Empty);
}
return GetOrAddNode(
$"alias::{NormalizeSpecifier(target)}",
target,
DenoModuleKind.SpecifierAlias,
target,
layerDigest: null,
integrity: null,
metadata: ImmutableDictionary<string, string?>.Empty);
}
private string GetOrAddNode(
string id,
string? displayName,
DenoModuleKind kind,
string? reference,
string? layerDigest,
string? integrity,
IReadOnlyDictionary<string, string?> metadata)
{
displayName ??= "(unknown)";
metadata ??= ImmutableDictionary<string, string?>.Empty;
if (_nodes.TryGetValue(id, out var existing))
{
var updated = existing;
if (string.IsNullOrEmpty(existing.Reference) && !string.IsNullOrEmpty(reference))
{
updated = updated with { Reference = reference };
}
if (string.IsNullOrEmpty(existing.LayerDigest) && !string.IsNullOrEmpty(layerDigest))
{
updated = updated with { LayerDigest = layerDigest };
}
if (string.IsNullOrEmpty(existing.Integrity) && !string.IsNullOrEmpty(integrity))
{
updated = updated with { Integrity = integrity };
}
if (metadata.Count > 0)
{
var combined = new Dictionary<string, string?>(existing.Metadata, StringComparer.Ordinal);
foreach (var pair in metadata)
{
combined[pair.Key] = pair.Value;
}
updated = updated with { Metadata = combined };
}
if (!ReferenceEquals(updated, existing))
{
_nodes[id] = updated;
}
return updated.Id;
}
_nodes[id] = new DenoModuleNode(
id,
displayName,
kind,
reference,
layerDigest,
integrity,
metadata);
return id;
}
private void AddEdge(
string from,
string to,
DenoImportKind kind,
string? specifier,
string provenance,
string? resolution)
{
specifier ??= "(unknown)";
if (string.Equals(from, to, StringComparison.Ordinal))
{
return;
}
_edges.Add(new DenoModuleEdge(
from,
to,
kind,
specifier,
provenance,
resolution));
}
private static DenoImportKind DetermineImportKind(string target)
{
if (string.IsNullOrWhiteSpace(target))
{
return DenoImportKind.Unknown;
}
var lower = target.ToLowerInvariant();
if (lower.EndsWith(".json", StringComparison.Ordinal))
{
return DenoImportKind.JsonAssertion;
}
if (lower.EndsWith(".wasm", StringComparison.Ordinal))
{
return DenoImportKind.WasmAssertion;
}
if (lower.StartsWith("node:", StringComparison.Ordinal) ||
lower.StartsWith("deno:", StringComparison.Ordinal))
{
return DenoImportKind.BuiltIn;
}
return DenoImportKind.Static;
}
private static IEnumerable<string> SafeEnumerateFiles(string root)
{
if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root))
{
yield break;
}
IEnumerable<string> iterator;
try
{
iterator = Directory.EnumerateFiles(root, "*", new EnumerationOptions
{
RecurseSubdirectories = true,
IgnoreInaccessible = true,
AttributesToSkip = FileAttributes.ReparsePoint,
ReturnSpecialDirectories = false,
});
}
catch (IOException)
{
yield break;
}
catch (UnauthorizedAccessException)
{
yield break;
}
foreach (var file in iterator)
{
yield return file;
}
}
private static string NormalizeSpecifier(string value)
=> $"alias::{(string.IsNullOrWhiteSpace(value) ? "(empty)" : value.Trim())}";
private static string NormalizePath(string value)
=> string.IsNullOrWhiteSpace(value)
? string.Empty
: value.Replace('\\', '/').TrimStart('/');
private static bool TryResolveUrlFromVendorPath(string vendorRelativePath, string fileRelativePath, out string? url)
{
var combined = Path.Combine(vendorRelativePath ?? string.Empty, fileRelativePath ?? string.Empty);
var normalized = NormalizePath(combined);
var segments = normalized.Split('/', StringSplitOptions.RemoveEmptyEntries);
var schemeIndex = Array.FindIndex(segments, static segment => segment is "http" or "https");
if (schemeIndex < 0 || schemeIndex + 1 >= segments.Length)
{
url = null;
return false;
}
var scheme = segments[schemeIndex];
var host = segments[schemeIndex + 1];
var path = string.Join('/', segments[(schemeIndex + 2)..]);
url = path.Length > 0
? $"{scheme}://{host}/{path}"
: $"{scheme}://{host}";
return true;
}
private static bool TryResolveUrlFromCachePath(string relativePath, out string? url)
{
var normalized = NormalizePath(relativePath);
var segments = normalized.Split('/', StringSplitOptions.RemoveEmptyEntries);
var depsIndex = Array.FindIndex(segments, static segment => segment.Equals("deps", StringComparison.OrdinalIgnoreCase));
if (depsIndex < 0 || depsIndex + 2 >= segments.Length)
{
url = null;
return false;
}
var scheme = segments[depsIndex + 1];
var host = segments[depsIndex + 2];
var remainingStart = depsIndex + 3;
if (remainingStart > segments.Length)
{
url = null;
return false;
}
var path = remainingStart < segments.Length
? string.Join('/', segments[remainingStart..])
: string.Empty;
url = path.Length > 0
? $"{scheme}://{host}/{path}"
: $"{scheme}://{host}";
return true;
}
}
}