163 lines
5.9 KiB
C#
163 lines
5.9 KiB
C#
using ScadaLink.Commons.Entities.Templates;
|
|
|
|
namespace ScadaLink.TemplateEngine;
|
|
|
|
/// <summary>
|
|
/// Detects naming collisions across composed module members using canonical (path-qualified) names.
|
|
/// Two members from different composed modules collide if they produce the same canonical name.
|
|
/// Members from different module instance names cannot collide because the prefix differentiates them.
|
|
/// </summary>
|
|
public static class CollisionDetector
|
|
{
|
|
/// <summary>
|
|
/// Represents a resolved member with its canonical name and origin.
|
|
/// </summary>
|
|
public sealed record ResolvedMember(
|
|
string CanonicalName,
|
|
string MemberType, // "Attribute", "Alarm", "Script"
|
|
string OriginDescription);
|
|
|
|
/// <summary>
|
|
/// Detects naming collisions among all members (direct + composed) of a template.
|
|
/// </summary>
|
|
/// <param name="template">The template to check.</param>
|
|
/// <param name="allTemplates">All templates in the system (for resolving composed templates).</param>
|
|
/// <returns>List of collision descriptions. Empty if no collisions.</returns>
|
|
public static IReadOnlyList<string> DetectCollisions(
|
|
Template template,
|
|
IReadOnlyList<Template> allTemplates)
|
|
{
|
|
// Duplicate-tolerant lookup (see CycleDetector.BuildLookup): a plain
|
|
// ToDictionary(t => t.Id) throws if two templates share an Id (e.g.
|
|
// not-yet-saved templates carrying Id 0).
|
|
var lookup = CycleDetector.BuildLookup(allTemplates);
|
|
var allMembers = new List<ResolvedMember>();
|
|
|
|
// Collect direct (top-level) members
|
|
CollectDirectMembers(template, prefix: null, originPrefix: template.Name, allMembers);
|
|
|
|
// Collect members from composed modules recursively
|
|
foreach (var composition in template.Compositions)
|
|
{
|
|
if (lookup.TryGetValue(composition.ComposedTemplateId, out var composedTemplate))
|
|
{
|
|
CollectComposedMembers(
|
|
composedTemplate,
|
|
prefix: composition.InstanceName,
|
|
lookup,
|
|
allMembers,
|
|
visited: new HashSet<int>());
|
|
}
|
|
}
|
|
|
|
// Collect inherited members (walk parent chain)
|
|
CollectInheritedMembers(template, lookup, allMembers, new HashSet<int> { template.Id });
|
|
|
|
// Detect duplicates by canonical name
|
|
var collisions = new List<string>();
|
|
var grouped = allMembers.GroupBy(m => m.CanonicalName, StringComparer.Ordinal);
|
|
|
|
foreach (var group in grouped)
|
|
{
|
|
var members = group.ToList();
|
|
if (members.Count > 1)
|
|
{
|
|
// Only report collision if members come from different origins
|
|
var distinctOrigins = members.Select(m => m.OriginDescription).Distinct().ToList();
|
|
if (distinctOrigins.Count > 1)
|
|
{
|
|
var origins = string.Join(", ", members.Select(m => $"{m.MemberType} from {m.OriginDescription}"));
|
|
collisions.Add($"Naming collision on '{group.Key}': {origins}.");
|
|
}
|
|
}
|
|
}
|
|
|
|
return collisions;
|
|
}
|
|
|
|
private static void CollectDirectMembers(
|
|
Template template,
|
|
string? prefix,
|
|
string originPrefix,
|
|
List<ResolvedMember> members)
|
|
{
|
|
foreach (var attr in template.Attributes)
|
|
{
|
|
var canonicalName = prefix == null ? attr.Name : $"{prefix}.{attr.Name}";
|
|
members.Add(new ResolvedMember(canonicalName, "Attribute", originPrefix));
|
|
}
|
|
|
|
foreach (var alarm in template.Alarms)
|
|
{
|
|
var canonicalName = prefix == null ? alarm.Name : $"{prefix}.{alarm.Name}";
|
|
members.Add(new ResolvedMember(canonicalName, "Alarm", originPrefix));
|
|
}
|
|
|
|
foreach (var script in template.Scripts)
|
|
{
|
|
var canonicalName = prefix == null ? script.Name : $"{prefix}.{script.Name}";
|
|
members.Add(new ResolvedMember(canonicalName, "Script", originPrefix));
|
|
}
|
|
}
|
|
|
|
private static void CollectComposedMembers(
|
|
Template template,
|
|
string prefix,
|
|
Dictionary<int, Template> lookup,
|
|
List<ResolvedMember> members,
|
|
HashSet<int> visited)
|
|
{
|
|
if (!visited.Add(template.Id))
|
|
return;
|
|
|
|
// Add direct members of this composed template with the prefix
|
|
CollectDirectMembers(template, prefix, $"module '{prefix}'", members);
|
|
|
|
// Recurse into nested compositions
|
|
foreach (var composition in template.Compositions)
|
|
{
|
|
if (lookup.TryGetValue(composition.ComposedTemplateId, out var nested))
|
|
{
|
|
var nestedPrefix = $"{prefix}.{composition.InstanceName}";
|
|
CollectComposedMembers(nested, nestedPrefix, lookup, members, visited);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void CollectInheritedMembers(
|
|
Template template,
|
|
Dictionary<int, Template> lookup,
|
|
List<ResolvedMember> members,
|
|
HashSet<int> visited)
|
|
{
|
|
if (!template.ParentTemplateId.HasValue)
|
|
return;
|
|
|
|
if (!lookup.TryGetValue(template.ParentTemplateId.Value, out var parent))
|
|
return;
|
|
|
|
if (!visited.Add(parent.Id))
|
|
return;
|
|
|
|
// Inherited direct members (no prefix)
|
|
CollectDirectMembers(parent, prefix: null, $"parent '{parent.Name}'", members);
|
|
|
|
// Inherited composed modules
|
|
foreach (var composition in parent.Compositions)
|
|
{
|
|
if (lookup.TryGetValue(composition.ComposedTemplateId, out var composedTemplate))
|
|
{
|
|
CollectComposedMembers(
|
|
composedTemplate,
|
|
composition.InstanceName,
|
|
lookup,
|
|
members,
|
|
new HashSet<int>());
|
|
}
|
|
}
|
|
|
|
// Continue up the inheritance chain
|
|
CollectInheritedMembers(parent, lookup, members, visited);
|
|
}
|
|
}
|