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 _nodes = new(StringComparer.Ordinal); private readonly List _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() { ["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.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.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.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.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() { ["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.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.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.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.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.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(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.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(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.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(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.Empty); } if (target.StartsWith("npm:", StringComparison.OrdinalIgnoreCase)) { return GetOrAddNode( $"npm::{target[4..]}", target, DenoModuleKind.NpmPackage, target, layerDigest: null, integrity: null, metadata: ImmutableDictionary.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.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.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.Empty); } return GetOrAddNode( $"alias::{NormalizeSpecifier(target)}", target, DenoModuleKind.SpecifierAlias, target, layerDigest: null, integrity: null, metadata: ImmutableDictionary.Empty); } private string GetOrAddNode( string id, string? displayName, DenoModuleKind kind, string? reference, string? layerDigest, string? integrity, IReadOnlyDictionary metadata) { displayName ??= "(unknown)"; metadata ??= ImmutableDictionary.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(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 SafeEnumerateFiles(string root) { if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root)) { yield break; } IEnumerable 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; } } }