Files
2026-07-13 13:13:17 +08:00

178 lines
7.0 KiB
C#

#nullable enable
using System.CodeDom.Compiler;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using T3.Core.Model;
using T3.Core.Operator;
namespace T3.Editor.UiModel.Helpers;
internal static partial class GraphUtils
{
public static SyntaxTree? GetSyntaxTree(Symbol symbol)
{
var package = (EditorSymbolPackage)symbol.SymbolPackage;
if (package is not EditableSymbolProject project || !project.TryGetPendingSourceCode(symbol.Id, out var sourceCode))
{
// there's intermediate source, so use this
if (!package.TryGetSourceCodePath(symbol, out var sourceCodePath))
{
Log.Error($"Could not find source file for symbol '{symbol.Name}'");
return null;
}
try
{
sourceCode = File.ReadAllText(sourceCodePath!);
}
catch (Exception e)
{
Log.Error($"Error opening file '{sourceCodePath}");
Log.Error(e.Message);
return null;
}
}
if (string.IsNullOrWhiteSpace(sourceCode))
{
Log.Info("Source was empty, skip compilation.");
return null;
}
return CSharpSyntaxTree.ParseText(sourceCode);
}
public static bool IsNewFieldNameValid(string? newFieldName, Symbol symbol, [NotNullWhen(false)] out string? reason)
{
if (string.IsNullOrWhiteSpace(newFieldName))
{
reason = "Name must not be empty";
return false;
}
if (!IsIdentifierValidNotEmpty(newFieldName))
{
reason = "Name must be a valid C# identifier";
return false;
}
var typeInfo = symbol.SymbolPackage.AssemblyInformation.OperatorTypeInfo[symbol.Id];
if (typeInfo.MemberNames.Contains(newFieldName))
{
reason = "Name must be unique within the operator";
return false;
}
reason = null;
return true;
}
public static bool IsNewSymbolNameValid(string newSymbolName, string destinationNamespace, SymbolPackage destinationPackage, [NotNullWhen(false)] out string? reason)
{
var isNamespaceValid = IsNamespaceValid(destinationNamespace, false, out var namespaceComponents);
if (!isNamespaceValid)
{
reason = "Namespace must be a valid C# namespace";
return false;
}
if (!destinationNamespace.StartsWith(destinationPackage.RootNamespace))
{
reason = "Namespace must be within the project's root namespace";
return false;
}
if(!IsIdentifierValid(newSymbolName))
{
reason = "Name must be a valid C# identifier";
return false;
}
if (ReservedWords.Contains(newSymbolName))
{
reason = "Name must not be a C# reserved word";
return false;
}
if (destinationPackage.ContainsSymbolName(newSymbolName, destinationNamespace))
{
reason = "Name must be unique within the namespace";
return false;
}
// check if the namespace contains a class name that exists
var namespaceSegmentCount = namespaceComponents.Length;
Span<char> nspBuilderBuffer = stackalloc char[destinationNamespace.Length];
for (int i = namespaceSegmentCount - 1; i >= 1; i--)
{
// treat the last segment of the namespace as a potential class name
var className = namespaceComponents[i];
var nspBuilderCount = 0;
// build the namespace up to the current segment
for(int nspIndex = 0; nspIndex < i; nspIndex++)
{
var nspSegment = namespaceComponents[nspIndex];
var insertSlice = nspBuilderBuffer[nspBuilderCount..];
nspSegment.CopyTo(insertSlice);
nspBuilderCount += nspSegment.Length;
nspBuilderBuffer[nspBuilderCount] = '.';
nspBuilderCount++;
}
var builtNamespace = nspBuilderBuffer[..(nspBuilderCount - 1)]; // remove trailing '.'
// check if the package contains namespace.className
if (destinationPackage.ContainsSymbolName(className, builtNamespace))
{
reason = $"Namespace must not contain a class name '{builtNamespace}.{className}'";
return false;
}
}
var totalLength = destinationNamespace.Length + newSymbolName.Length + 1;
const int maxPathLength = 255;
if (totalLength > maxPathLength)
{
reason = $"Total length of namespace and class name must not exceed {maxPathLength} characters";
return false;
}
reason = null;
return true;
}
public static bool IsIdentifierValid(string? className) => !string.IsNullOrWhiteSpace(className)
&& IsIdentifierValidNotEmpty(className);
private static bool IsIdentifierValidNotEmpty(string className) => IdentifierValidator.Value.IsValidIdentifier(className);
public static bool IsValidProjectName(string userName) => IdentifierValidator.Value.IsValidIdentifier(userName);
public static bool IsNamespaceValid(string namespaceName, bool needsToBeUnique, out string[] namespaceComponents)
{
if (namespaceName == "")
{
namespaceComponents = [];
return true;
}
namespaceComponents = namespaceName.Split('.');
return ValidTypeNameSpacePattern.IsMatch(namespaceName)
&& !namespaceComponents.Any(x => x.Length == 0
|| ReservedWords.Contains(x)
/* || char.IsLower(x[0])*/) // enforce PascalCase
&& (!needsToBeUnique
|| !SymbolPackage.AllPackages.Any(x => x.OwnsNamespace(namespaceName) // namespace is already owned by another package
|| x.RootNamespace.StartsWith(namespaceName))); // namespace is the name of a larger namespace the package owns a part of - this is not allowed
}
private static readonly Regex ValidTypeNameSpacePattern = NamespaceRegex();
private static readonly string[] ReservedWords = new string[] { "value", "Var", "instance", "item", "Input", "slot" };
private static readonly Lazy<CodeDomProvider> IdentifierValidator = new(() => CodeDomProvider.CreateProvider("C#"));
[GeneratedRegex(@"^([A-Za-z][A-Za-z\d]*)(\.([A-Za-z_][A-Za-z_\d]*))*$")]
private static partial Regex NamespaceRegex();
}