#nullable enable using System.Diagnostics.CodeAnalysis; using T3.Core.Operator; using T3.Core.Operator.Slots; using T3.Core.Settings; using T3.Editor.Gui.InputUi.SimpleInputUis; namespace T3.Editor.UiModel.Helpers; /// /// Aggregates information about all symbols (warnings, dependencies, usage, examples), /// and also provides single-symbol analysis for dialogs and tools. /// internal static class SymbolAnalysis { /// /// Detailed info per symbol id, filled by UpdateDetails(). /// internal static readonly Dictionary InformationForSymbolIds = new(1000); internal static int TotalUsageCount; /// /// All connections between input slots stored as hashes (sourceSlotId x targetSlotId). /// Used by SymbolBrowser for relevancy weighting of frequent combinations. /// internal static Dictionary ConnectionHashCounts = new(); internal static bool DetailsInitialized { get; private set; } // Cache for usage counts (used by both bulk and single-symbol queries) private static readonly Dictionary _latestUsageCounts = new(); internal static void LogInvalidSymbolDependencies() { if (!DetailsInitialized) UpdateDetails(); foreach (var symbol in SymbolRegistry.SymbolsFromAllPackages() .OrderBy(s => s.Namespace) .ThenBy(s => s.Name)) { if (!InformationForSymbolIds.TryGetValue(symbol.Id, out var infos)) continue; if (!_packageDependencies.TryGetValue(symbol.SymbolPackage.Name, out var deps)) continue; foreach (var requiredSymbolId in infos.RequiredSymbolIds) { if (!SymbolRegistry.TryGetSymbol(requiredSymbolId, out var depSymbol)) continue; if (!deps.Contains(depSymbol.SymbolPackage.Name)) { Log.Warning($"{symbol.Namespace}.{symbol.Name} depends on {depSymbol.Namespace}.{depSymbol.Name}"); } } } } internal static void LogInvalidAssetReference() { foreach (var symbol in SymbolRegistry.SymbolsFromAllPackages() .OrderBy(s => s.Namespace) .ThenBy(s => s.Name)) { if (!_packageDependencies.TryGetValue(symbol.SymbolPackage.Name, out var deps)) continue; // Symbol children foreach (var child in symbol.Children.Values) { foreach (var input in child.Inputs.Values) { if (input.IsDefault) continue; if (input.InputDefinition.ValueType != typeof(string)) continue; if (input.Value is not InputValue stringValue) continue; var pathValue = stringValue.Value; if (string.IsNullOrEmpty(pathValue)) continue; if (!SymbolUiRegistry.TryGetSymbolUi(child.Symbol.Id, out var childSymbolUi)) continue; if (!childSymbolUi.InputUis.TryGetValue(input.Id, out var inputUi)) continue; if (inputUi is not StringInputUi stringUi) continue; switch (stringUi.Usage) { case StringInputUi.UsageType.FilePath: case StringInputUi.UsageType.DirectoryPath: { var assetPackageNameIndex = pathValue.IndexOf(':'); if (assetPackageNameIndex < 1) break; var assetPackageName = pathValue[..(assetPackageNameIndex)]; if (!deps.Contains(assetPackageName)) { Log.Warning($"{symbol.Namespace}.[{symbol.Name}].{childSymbolUi.Symbol.Name} references {pathValue}"); } break; } } } } } } private static readonly string[] _libPackages = [FileLocations.LibPackageName, FileLocations.TypesPackageName, "Mediapipe", "t3.ndi", "t3.spout", "t3.unsplash"]; private static readonly Dictionary _packageDependencies = new() { { FileLocations.LibPackageName, [.._libPackages] }, { FileLocations.ExamplesPackageName, [.._libPackages, FileLocations.ExamplesPackageName] }, { FileLocations.SkillsPackageName, [.._libPackages, FileLocations.SkillsPackageName] } }; /// /// Basic usage info used by symbol browser and relevancy search. /// internal static void UpdateSymbolUsageCounts() { Log.Debug("Updating symbol counts..."); var usages = CollectSymbolUsageCounts(); ConnectionHashCounts = new Dictionary(); foreach (var symbolUi in EditorSymbolPackage.AllSymbolUis) { var symbolId = symbolUi.Symbol.Id; if (!InformationForSymbolIds.TryGetValue(symbolId, out var info)) { info = new SymbolInformation(); InformationForSymbolIds[symbolId] = info; } // Update connection counts foreach (var connection in symbolUi.Symbol.Connections) { var hash = connection.SourceSlotId.GetHashCode() * 31 + connection.TargetSlotId.GetHashCode(); ConnectionHashCounts.TryGetValue(hash, out var connectionCount); ConnectionHashCounts[hash] = connectionCount + 1; } usages.TryGetValue(symbolId, out var count); info.UsageCount = count; } } /// /// Update collection with details useful for /// library structure cleanup, browsing, etc. /// internal static void UpdateDetails() { var usages = CollectSymbolUsageCounts(); InformationForSymbolIds.Clear(); TotalUsageCount = 0; // Precompute reverse dependencies once for all symbols var (_, reverseDeps) = CollectUsageAndReverseDependencies(); foreach (var symbolUi in EditorSymbolPackage.AllSymbolUis) { var symbol = symbolUi.Symbol; usages.TryGetValue(symbol.Id, out var usageCount); TotalUsageCount += usageCount; var requiredSymbols = CollectRequiredSymbols(symbol); var invalidRequirements = CollectInvalidRequirements(symbol, requiredSymbols).ToList(); reverseDeps.TryGetValue(symbol.Id, out var deps); var dependingSymbols = deps ?? new HashSet(); InformationForSymbolIds[symbol.Id] = BuildSymbolInformation( symbol, symbolUi, requiredSymbols, invalidRequirements, dependingSymbols, usageCount); } DetailsInitialized = true; } /// /// Fast single-symbol analysis for dialogs, menus, etc. /// Uses cached bulk data when available, otherwise computes on-demand. /// /// The symbol to analyze. /// Returns the collected information for the symbol. /// /// If true, forces a fresh analysis for this symbol even if cached data is available. /// If false, uses the cached information when possible. /// internal static bool TryGetSymbolInfo(Symbol symbol, out SymbolInformation info, bool forceUpdate = false) { info = new SymbolInformation(); if (!symbol.TryGetSymbolUi(out var symbolUi)) return false; // If UpdateDetails has run and no force update is requested, return the cached info if (!forceUpdate && DetailsInitialized && InformationForSymbolIds.TryGetValue(symbol.Id, out var cached)) { // Keep a copy so callers cannot accidentally mutate the cache. info = cached; // Keep usage count fresh if we have a newer value if (_latestUsageCounts.TryGetValue(symbol.Id, out var usage)) info.UsageCount = usage; return true; } // On-demand analysis for this single symbol var requiredSymbols = CollectRequiredSymbols(symbol); var invalidRequirements = CollectInvalidRequirements(symbol, requiredSymbols).ToList(); var (usageCounts, reverseDeps) = CollectUsageAndReverseDependencies(); usageCounts.TryGetValue(symbol.Id, out var usageCount); reverseDeps.TryGetValue(symbol.Id, out var deps); var dependingSymbols = deps ?? new HashSet(); info = BuildSymbolInformation( symbol, symbolUi, requiredSymbols, invalidRequirements, dependingSymbols, usageCount); // Optionally refresh the cached entry if details are already initialized // so that subsequent calls without forceUpdate can benefit. if (DetailsInitialized) { InformationForSymbolIds[symbol.Id] = info; } return true; } /// /// Unified structure with all known symbol metadata. /// public sealed class SymbolInformation { public List Warnings = []; internal HashSet RequiredSymbolIds = []; internal HashSet DependingSymbols = []; public List InvalidRequiredIds = []; public IReadOnlyList ExampleSymbolsIds = []; internal int UsageCount; internal bool LacksDescription; internal bool LacksAllParameterDescription; internal bool LacksSomeParameterDescription; internal bool LacksParameterGrouping; internal bool DependsOnObsoleteOps; internal SymbolUi.SymbolTags Tags; // Copy to avoid reference to symbolUi internal OperatorClassification OperatorType; } public enum OperatorClassification { Unknown = 0, Lib, Type, Example, T3, Skill, } // Shared helpers (used by both bulk and single analysis) #region Shared Helpers internal static bool TryGetOperatorType(Symbol symbol, out OperatorClassification opType) { var ns = symbol.Namespace ?? string.Empty; var rootSegment = ns.Split('.')[0]; opType = rootSegment switch { FileLocations.LibPackageName => OperatorClassification.Lib, FileLocations.TypesPackageName => OperatorClassification.Type, FileLocations.ExamplesPackageName => OperatorClassification.Example, "t3" => OperatorClassification.T3, FileLocations.SkillsPackageName => OperatorClassification.Skill, _ => OperatorClassification.Unknown }; return opType != OperatorClassification.Unknown; } private static SymbolInformation BuildSymbolInformation(Symbol symbol, SymbolUi symbolUi, HashSet requiredSymbols, List invalidRequirements, HashSet dependingSymbols, int usageCount) { var inputUis = symbolUi.InputUis.Values; var lacksDescription = string.IsNullOrWhiteSpace(symbolUi.Description); var hasManyInputs = symbolUi.InputUis.Count > 2; var lacksAllParamDesc = hasManyInputs && inputUis.All(i => string.IsNullOrWhiteSpace(i.Description)); var lacksSomeParamDesc = hasManyInputs && inputUis.Any(i => string.IsNullOrWhiteSpace(i.Description)); var lacksParameterGrouping = symbolUi.InputUis.Count > 4 && !inputUis.Any(i => i.AddPadding || !string.IsNullOrEmpty(i.GroupTitle)); var dependsOnObsolete = requiredSymbols .Select(s => s.GetSymbolUi()) .Any(ui => ui != null && ui.Tags.HasFlag(SymbolUi.SymbolTags.Obsolete)); TryGetOperatorType(symbol, out var opType); return new SymbolInformation { Warnings = new List(), RequiredSymbolIds = requiredSymbols.Select(s => s.Id).ToHashSet(), DependingSymbols = dependingSymbols, InvalidRequiredIds = invalidRequirements, ExampleSymbolsIds = ExampleSymbolLinking.GetExampleIds(symbol.Id), UsageCount = usageCount, LacksDescription = lacksDescription, LacksAllParameterDescription = lacksAllParamDesc, LacksSomeParameterDescription = lacksSomeParamDesc, LacksParameterGrouping = lacksParameterGrouping, DependsOnObsoleteOps = dependsOnObsolete, Tags = symbolUi.Tags, OperatorType = opType }; } private static HashSet CollectRequiredSymbols(Symbol root) { var all = new HashSet(); Collect(root); return all; void Collect(Symbol symbol) { foreach (var symbolChild in symbol.Children.Values) { if (!all.Add(symbolChild.Symbol)) continue; Collect(symbolChild.Symbol); } } } /// /// Collect Ids of required symbols that are not within the list of projects /// private static IEnumerable CollectInvalidRequirements(Symbol root, HashSet requiredSymbols) { var result = new List(); // Todo: implement this correctly? HashSet validPackagesNames = new() { FileLocations.TypesPackageName, FileLocations.LibPackageName, FileLocations.ExamplesPackageName, root.SymbolPackage.RootNamespace, }; foreach (var r in requiredSymbols) { var projectId = r.SymbolPackage.RootNamespace; if (validPackagesNames.Contains(projectId)) continue; result.Add(r); } return result .OrderBy(s => s.Namespace) .ThenBy(s => s.Name) .Select(s => s.Id); } private static Dictionary CollectSymbolUsageCounts() { var results = new Dictionary(); TotalUsageCount = 0; foreach (var s in EditorSymbolPackage.AllSymbols) { foreach (var child in s.Children.Values) { results.TryGetValue(child.Symbol.Id, out var currentCount); results[child.Symbol.Id] = currentCount + 1; TotalUsageCount++; } } _latestUsageCounts.Clear(); foreach (var kvp in results) _latestUsageCounts[kvp.Key] = kvp.Value; return results; } /// /// Builds per-symbol usage counts and a reverse dependency map in a single pass /// over . /// This is used by single-symbol analysis without running the full detail update. /// private static (Dictionary UsageCounts, Dictionary> ReverseDependencies) CollectUsageAndReverseDependencies() { var usageCounts = new Dictionary(); var reverseDeps = new Dictionary>(); foreach (var container in EditorSymbolPackage.AllSymbols) { var containerId = container.Id; foreach (var child in container.Children.Values) { var childId = child.Symbol.Id; usageCounts.TryGetValue(childId, out var current); usageCounts[childId] = current + 1; if (!reverseDeps.TryGetValue(childId, out var set)) { set = new HashSet(); reverseDeps[childId] = set; } set.Add(containerId); } } return (usageCounts, reverseDeps); } #endregion public static bool TryGetFileInputFromInstance(Instance instance, [NotNullWhen(true)] out InputSlot? stringInput, [NotNullWhen(true)] out StringInputUi? stringInputUi) { stringInput = null; stringInputUi = null; var symbolUi = instance.GetSymbolUi(); foreach (var input in instance.Inputs) { if (input is not InputSlot tmpStringInput) continue; stringInput = tmpStringInput; var inputUi = symbolUi.InputUis[input.Id]; if (inputUi is not StringInputUi { Usage: StringInputUi.UsageType.FilePath } tmpStringInputUi) continue; stringInputUi = tmpStringInputUi; return true; } return false; } }