diff --git a/src/Neo.Compiler.CSharp/Diagnostic/DiagnosticId.cs b/src/Neo.Compiler.CSharp/Diagnostic/DiagnosticId.cs index 0d2600b2f..c614f5be1 100644 --- a/src/Neo.Compiler.CSharp/Diagnostic/DiagnosticId.cs +++ b/src/Neo.Compiler.CSharp/Diagnostic/DiagnosticId.cs @@ -36,5 +36,7 @@ static class DiagnosticId public const string InvalidInitialValue = "NC3005"; public const string IncorrectNEPStandard = "NC3006"; public const string CapturedStaticFieldNotFound = "NC3007"; + public const string InvalidType = "NC3008"; + public const string InvalidArgument = "NC3009"; } } diff --git a/src/Neo.Compiler.CSharp/MethodConvert/Expression/Expression.cs b/src/Neo.Compiler.CSharp/MethodConvert/Expression/Expression.cs index 979139696..d06badb61 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/Expression/Expression.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/Expression/Expression.cs @@ -190,11 +190,27 @@ private void ConvertNonConstantExpression(SemanticModel model, ExpressionSyntax // Example: 42 or "Hello" ConvertLiteralExpression(model, expression); break; + case TypeOfExpressionSyntax expression: + // Example: typeof(int) + // Note: Neo currently does not support the Type type of C#. The typeof operator here + // will only return the string name of the class/type. This support is added + // to ensure we can process enum parse methods. + ConvertTypeOfExpression(model, expression); + break; default: throw new CompilationException(syntax, DiagnosticId.SyntaxNotSupported, $"Unsupported syntax: {syntax}"); } } + private void ConvertTypeOfExpression(SemanticModel model, TypeOfExpressionSyntax expression) + { + var typeInfo = model.GetTypeInfo(expression.Type); + if (typeInfo.Type == null) + throw new CompilationException(expression, DiagnosticId.InvalidType, $"Invalid type in typeof expression: {expression.Type}"); + + Push(typeInfo.Type.Name); + } + private static ITypeSymbol? GetTypeSymbol(SyntaxNode? syntaxNode, SemanticModel model) { return syntaxNode switch diff --git a/src/Neo.Compiler.CSharp/MethodConvert/Helpers/StackHelpers.cs b/src/Neo.Compiler.CSharp/MethodConvert/Helpers/StackHelpers.cs index 283704b5a..0c5cdf5c2 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/Helpers/StackHelpers.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/Helpers/StackHelpers.cs @@ -68,6 +68,8 @@ private void Push(bool value) AddInstruction(value ? OpCode.PUSHT : OpCode.PUSHF); } + private Instruction Ret() => AddInstruction(OpCode.RET); + private Instruction Push(BigInteger number) { if (number >= -1 && number <= 16) return AddInstruction(number == -1 ? OpCode.PUSHM1 : OpCode.PUSH0 + (byte)(int)number); diff --git a/src/Neo.Compiler.CSharp/MethodConvert/StackHelpers.OpCodes.cs b/src/Neo.Compiler.CSharp/MethodConvert/StackHelpers.OpCodes.cs index b13666f18..c975b41e6 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/StackHelpers.OpCodes.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/StackHelpers.OpCodes.cs @@ -541,5 +541,16 @@ private Instruction Assertmsg() { return AddInstruction(OpCode.ASSERTMSG); } + + private Instruction JumpIfNot(JumpTarget target) + { + return Jump(OpCode.JMPIFNOT, target); + } + + private Instruction Jump(JumpTarget target) + { + return Jump(OpCode.JMP, target); + } + #endregion } diff --git a/src/Neo.Compiler.CSharp/MethodConvert/System/SystemCall.Enum.cs b/src/Neo.Compiler.CSharp/MethodConvert/System/SystemCall.Enum.cs new file mode 100644 index 000000000..589c72bbd --- /dev/null +++ b/src/Neo.Compiler.CSharp/MethodConvert/System/SystemCall.Enum.cs @@ -0,0 +1,706 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// The Neo.Compiler.CSharp is free software distributed under the MIT +// software license, see the accompanying file LICENSE in the main directory +// of the project or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Neo.SmartContract.Native; +using Neo.VM; + +namespace Neo.Compiler; + +internal partial class MethodConvert +{ + private static void HandleEnumParse(MethodConvert methodConvert, SemanticModel model, IMethodSymbol symbol, + ExpressionSyntax? instanceExpression, IReadOnlyList? arguments) + { + if (instanceExpression is not null) + methodConvert.ConvertExpression(model, instanceExpression); + if (arguments is not null) + methodConvert.PrepareArgumentsForMethod(model, symbol, arguments, CallingConvention.StdCall); + + // Get the enum type from the first argument (typeof(enum)) + ITypeSymbol? enumTypeSymbol = null; + if (arguments is { Count: > 0 }) + { + if ((arguments[0] as ArgumentSyntax).Expression is TypeOfExpressionSyntax typeOfExpression) + { + var typeInfo = model.GetTypeInfo(typeOfExpression.Type); + enumTypeSymbol = typeInfo.Type; + } + else + { + throw new CompilationException(arguments[0], DiagnosticId.InvalidArgument, "First argument must be a typeof(enum)"); + } + } + else + { + throw new CompilationException(symbol.Locations.FirstOrDefault().MetadataModule, DiagnosticId.InvalidArgument, "Enum.Parse requires at least two arguments"); + } + + if (enumTypeSymbol is not { TypeKind: TypeKind.Enum }) + { + throw new CompilationException(arguments![0], DiagnosticId.InvalidType, "Unable to determine enum type from first argument"); + } + + var enumMembers = enumTypeSymbol.GetMembers().OfType() + .Where(field => field is { HasConstantValue: true, IsImplicitlyDeclared: false }).ToArray(); + + foreach (var t in enumMembers) + { + // Duplicate inputString + methodConvert.Dup(); // Stack: [type, inputString,inputString] + + // Push enum name + methodConvert.Push(t.Name); // Stack: [type, inputString,inputString, enumName] + + // Equal comparison + methodConvert.Equal(); // Stack: [type,inputString, isEqual] + + var nextCheck = new JumpTarget(); + // If not equal, discard duplicated inputString and proceed to next + methodConvert.Jump(OpCode.JMPIFNOT, nextCheck); + + // If equal: + // Push enum value + methodConvert.Push(t.ConstantValue); // Stack: [type, inputString, enumValue] + methodConvert.Reverse3(); + methodConvert.Drop(2); + methodConvert.AddInstruction(OpCode.RET); + + nextCheck.Instruction = methodConvert.AddInstruction(OpCode.NOP); + } + + // No match found + // Remove the inputString from the stack + methodConvert.Drop(); + methodConvert.Push("No such enum value"); + methodConvert.Throw(); + } + + private static void HandleEnumParseIgnoreCase(MethodConvert methodConvert, SemanticModel model, IMethodSymbol symbol, + ExpressionSyntax? instanceExpression, IReadOnlyList? arguments) + { + if (instanceExpression is not null) + methodConvert.ConvertExpression(model, instanceExpression); + if (arguments is not null) + methodConvert.PrepareArgumentsForMethod(model, symbol, arguments, CallingConvention.StdCall); + + // Get the enum type from the first argument (typeof(enum)) + ITypeSymbol? enumTypeSymbol = null; + if (arguments is { Count: > 0 }) + { + if ((arguments[0] as ArgumentSyntax).Expression is TypeOfExpressionSyntax typeOfExpression) + { + var typeInfo = model.GetTypeInfo(typeOfExpression.Type); + enumTypeSymbol = typeInfo.Type; + } + else + { + throw new CompilationException(arguments[0], DiagnosticId.InvalidArgument, "First argument must be a typeof(enum)"); + } + } + else + { + throw new CompilationException(symbol.Locations.FirstOrDefault().MetadataModule, DiagnosticId.InvalidArgument, "Enum.Parse requires at least two arguments"); + } + + if (enumTypeSymbol is not { TypeKind: TypeKind.Enum }) + { + throw new CompilationException(arguments![0], DiagnosticId.InvalidType, "Unable to determine enum type from first argument"); + } + + var enumMembers = enumTypeSymbol.GetMembers().OfType() + .Where(field => field is { HasConstantValue: true, IsImplicitlyDeclared: false }).ToArray(); + + var ignoreCase = new JumpTarget(); + var ignoreCase2 = new JumpTarget(); + methodConvert.JumpIfNot(ignoreCase); + ConvertToUpper(methodConvert); // Convert inputString to upper case + ignoreCase.Instruction = methodConvert.Nop(); + foreach (var t in enumMembers) + { + // Duplicate inputString + methodConvert.Dup(); // Stack: [..., inputString, inputString] + methodConvert.AddInstruction(OpCode.LDARG1); + methodConvert.JumpIfNot(ignoreCase2); + JumpTarget endCase = new JumpTarget(); + // Push enum name + methodConvert.Push(t.Name.ToUpper()); // Stack: [..., inputString, inputString, enumName] + methodConvert.Jump(endCase); + ignoreCase2.Instruction = methodConvert.Nop(); + methodConvert.Push(t.Name); + endCase.Instruction = methodConvert.Nop(); + + // Equal comparison + methodConvert.Equal(); // Stack: [..., inputString, isEqual] + + var nextCheck = new JumpTarget(); + // If not equal, discard duplicated inputString and proceed to next + methodConvert.Jump(OpCode.JMPIFNOT, nextCheck); + + // If equal: + // Remove the duplicated inputString from the stack + methodConvert.Drop(3); + // Push enum value + methodConvert.Push(t.ConstantValue); + methodConvert.AddInstruction(OpCode.RET); + + nextCheck.Instruction = methodConvert.AddInstruction(OpCode.NOP); + } + + // No match found + // Remove the inputString from the stack + methodConvert.Drop(2); + methodConvert.Push("No such enum value"); + methodConvert.Throw(); + } + + private static void HandleEnumTryParseIgnoreCase(MethodConvert methodConvert, SemanticModel model, IMethodSymbol symbol, + ExpressionSyntax? instanceExpression, IReadOnlyList? arguments) + { + if (instanceExpression is not null) + methodConvert.ConvertExpression(model, instanceExpression); + if (arguments is not null) + methodConvert.PrepareArgumentsForMethod(model, symbol, arguments, CallingConvention.StdCall); + + // Get the enum type from the first argument (typeof(enum)) + ITypeSymbol? enumTypeSymbol = null; + if (arguments is { Count: > 2 }) + { + if ((arguments[0] as ArgumentSyntax).Expression is TypeOfExpressionSyntax typeOfExpression) + { + var typeInfo = model.GetTypeInfo(typeOfExpression.Type); + enumTypeSymbol = typeInfo.Type; + } + else + { + throw new CompilationException(arguments[0], DiagnosticId.InvalidArgument, "First argument must be a typeof(enum)"); + } + } + else + { + throw new CompilationException(symbol.Locations.FirstOrDefault().MetadataModule, DiagnosticId.InvalidArgument, "Enum.TryParse requires at least three arguments"); + } + + if (enumTypeSymbol is not { TypeKind: TypeKind.Enum }) + { + throw new CompilationException(arguments![0], DiagnosticId.InvalidType, "Unable to determine enum type from first argument"); + } + + if (!methodConvert._context.TryGetCapturedStaticField(symbol.Parameters[3], out var index)) + throw new CompilationException(symbol, DiagnosticId.SyntaxNotSupported, "Out parameter must be captured in a static field."); + + var enumMembers = enumTypeSymbol.GetMembers().OfType() + .Where(field => field is { HasConstantValue: true, IsImplicitlyDeclared: false }).ToArray(); + var ignoreCase = new JumpTarget(); + var ignoreCase2 = new JumpTarget(); + methodConvert.Drop(); + methodConvert.JumpIfNot(ignoreCase); + methodConvert.Swap(); + methodConvert.Drop(); + ConvertToUpper(methodConvert); // Convert inputString to upper case + ignoreCase.Instruction = methodConvert.Nop(); + foreach (var t in enumMembers) + { + // Duplicate inputString + methodConvert.Dup(); // Stack: [..., inputString, inputString] + methodConvert.AddInstruction(OpCode.LDARG1); + methodConvert.JumpIfNot(ignoreCase2); + JumpTarget endCase = new JumpTarget(); + // Push enum name + methodConvert.Push(t.Name.ToUpper()); // Stack: [..., inputString, inputString, enumName] + methodConvert.Jump(endCase); + ignoreCase2.Instruction = methodConvert.Nop(); + methodConvert.Push(t.Name); + endCase.Instruction = methodConvert.Nop(); + + // Equal comparison + methodConvert.Equal(); // Stack: [..., inputString, isEqual] + + var nextCheck = new JumpTarget(); + // If not equal, discard duplicated inputString and proceed to next + methodConvert.Jump(OpCode.JMPIFNOT, nextCheck); + + // If equal: + // Remove the duplicated inputString from the stack + methodConvert.Drop(2); + // Push enum value + methodConvert.Push(t.ConstantValue); + // Store the result in the out parameter + methodConvert.AccessSlot(OpCode.STSFLD, index); + // Push true to indicate success + methodConvert.Push(true); + methodConvert.AddInstruction(OpCode.RET); + + nextCheck.Instruction = methodConvert.AddInstruction(OpCode.NOP); + } + + // No match found + // Remove the inputString from the stack + methodConvert.Drop(2); + // Push default value (0) for the out parameter + methodConvert.Push(0); + methodConvert.AccessSlot(OpCode.STSFLD, index); + // Push false to indicate failure + methodConvert.Push(false); + methodConvert.AddInstruction(OpCode.RET); + } + + private static void ConvertToUpper(MethodConvert methodConvert) + { + var loopStart = new JumpTarget(); + var loopEnd = new JumpTarget(); + var charIsLower = new JumpTarget(); + methodConvert.Push(""); // Create an empty ByteString + + // methodConvert.AddInstruction(OpCode.LDARG0); // Load the string | arr str + methodConvert.AddInstruction(OpCode.PUSH0); // Push the initial index (0) arr str 0 + loopStart.Instruction = methodConvert.AddInstruction(OpCode.NOP); + + methodConvert.AddInstruction(OpCode.DUP); // Duplicate the index + methodConvert.AddInstruction(OpCode.LDARG0); // Load the string + methodConvert.AddInstruction(OpCode.SIZE); // Get the length of the string + methodConvert.AddInstruction(OpCode.LT); // Check if index < length + methodConvert.Jump(OpCode.JMPIFNOT, loopEnd); // If not, exit the loop + + methodConvert.AddInstruction(OpCode.DUP); // Duplicate the index | arr str 0 0 + methodConvert.AddInstruction(OpCode.LDARG0); // Load the string + methodConvert.Swap(); + methodConvert.AddInstruction(OpCode.PICKITEM); // Get the character at the current index + methodConvert.AddInstruction(OpCode.DUP); // Duplicate the character + methodConvert.Push((ushort)'a'); // Push 'a' + methodConvert.Push((ushort)'z' + 1); // Push 'z' + 1 + methodConvert.AddInstruction(OpCode.WITHIN); // Check if character is within 'a' to 'z' + methodConvert.Jump(OpCode.JMPIF, charIsLower); // If true, jump to charIsLower + methodConvert.Rot(); + methodConvert.Swap(); + methodConvert.AddInstruction(OpCode.CAT); // Append the original character to the array + methodConvert.Swap(); + methodConvert.Inc(); + methodConvert.Jump(OpCode.JMP, loopStart); // Jump to the start of the loop + + charIsLower.Instruction = methodConvert.AddInstruction(OpCode.NOP); + methodConvert.Push((ushort)'a'); // Push 'a' + methodConvert.AddInstruction(OpCode.SUB); // Subtract 'a' from the character + methodConvert.Push((ushort)'A'); // Push 'A' + methodConvert.AddInstruction(OpCode.ADD); // Add 'A' to the result + methodConvert.Rot(); + methodConvert.Swap(); + methodConvert.AddInstruction(OpCode.CAT); // Append the upper case character to the array + methodConvert.Swap(); + methodConvert.Inc(); + methodConvert.Jump(OpCode.JMP, loopStart); // Jump to the start of the loop + + loopEnd.Instruction = methodConvert.AddInstruction(OpCode.NOP); + methodConvert.Drop(); + methodConvert.ChangeType(VM.Types.StackItemType.ByteString); // Convert the array to a byte string + } + + private static void HandleEnumIsDefinedByName(MethodConvert methodConvert, SemanticModel model, IMethodSymbol symbol, + ExpressionSyntax? instanceExpression, IReadOnlyList? arguments) + { + if (instanceExpression is not null) + methodConvert.ConvertExpression(model, instanceExpression); + if (arguments is not null) + methodConvert.PrepareArgumentsForMethod(model, symbol, arguments, CallingConvention.StdCall); + + // Get the enum type from the first argument (typeof(enum)) + ITypeSymbol? enumTypeSymbol = null; + if (arguments is { Count: > 0 }) + { + if ((arguments[0] as ArgumentSyntax).Expression is TypeOfExpressionSyntax typeOfExpression) + { + var typeInfo = model.GetTypeInfo(typeOfExpression.Type); + enumTypeSymbol = typeInfo.Type; + } + else + { + throw new CompilationException(arguments[0], DiagnosticId.InvalidArgument, "First argument must be a typeof(enum)"); + } + } + else + { + throw new CompilationException(symbol.Locations.FirstOrDefault().MetadataModule, DiagnosticId.InvalidArgument, "Enum.IsDefined requires at least two arguments"); + } + + if (enumTypeSymbol is not { TypeKind: TypeKind.Enum }) + { + throw new CompilationException(arguments![0], DiagnosticId.InvalidType, "Unable to determine enum type from first argument"); + } + + var enumMembers = enumTypeSymbol.GetMembers().OfType() + .Where(field => field is { HasConstantValue: true, IsImplicitlyDeclared: false }).ToArray(); + + foreach (var t in enumMembers) + { + methodConvert.Dup(); // Duplicate input name + methodConvert.Push(t.Name); // Push enum name + methodConvert.Equal(); // Compare strings + + var nextCheck = new JumpTarget(); + methodConvert.JumpIfNot(nextCheck); // If not equal, check next + + methodConvert.Drop(); // Remove the duplicated input name + methodConvert.Push(true); // Push true (enum name is defined) + methodConvert.AddInstruction(OpCode.RET); // Return true + + nextCheck.Instruction = methodConvert.AddInstruction(OpCode.NOP); + } + + // No match found + methodConvert.Drop(); // Remove the input name + methodConvert.Push(false); // Push false (enum name is not defined) + methodConvert.AddInstruction(OpCode.RET); // Return false + } + + private static void HandleEnumGetName(MethodConvert methodConvert, SemanticModel model, IMethodSymbol symbol, + ExpressionSyntax? instanceExpression, IReadOnlyList? arguments) + { + if (instanceExpression is not null) + methodConvert.ConvertExpression(model, instanceExpression); + if (arguments is not null) + methodConvert.PrepareArgumentsForMethod(model, symbol, arguments, CallingConvention.StdCall); + + // Get the enum type from the Enum value + var enumType = symbol.Parameters[0].Type; + + if (enumType is not INamedTypeSymbol enumTypeSymbol) + { + throw new CompilationException(symbol.Locations.FirstOrDefault().MetadataModule, DiagnosticId.InvalidType, "Unable to determine enum type"); + } + + var enumMembers = enumTypeSymbol.GetMembers().OfType() + .Where(field => field is { HasConstantValue: true, IsImplicitlyDeclared: false }).ToArray(); + + foreach (var t in enumMembers) + { + methodConvert.Dup(); // Duplicate input value + methodConvert.Push(t.ConstantValue); // Push enum value + methodConvert.Equal(); // Compare values + + var nextCheck = new JumpTarget(); + methodConvert.JumpIfNot(nextCheck); // If not equal, check next + + methodConvert.Drop(); // Remove the duplicated input value + methodConvert.Push(t.Name); // Push enum name + methodConvert.AddInstruction(OpCode.RET); // Return enum name + + nextCheck.Instruction = methodConvert.AddInstruction(OpCode.NOP); + } + + // No match found + methodConvert.Drop(); // Remove the input value + methodConvert.AddInstruction(OpCode.PUSHNULL); // Push null (no matching enum name) + methodConvert.AddInstruction(OpCode.RET); // Return null + } + + private static void HandleEnumGetNameWithType(MethodConvert methodConvert, SemanticModel model, IMethodSymbol symbol, + ExpressionSyntax? instanceExpression, IReadOnlyList? arguments) + { + if (instanceExpression is not null) + methodConvert.ConvertExpression(model, instanceExpression); + if (arguments is not null) + methodConvert.PrepareArgumentsForMethod(model, symbol, arguments, CallingConvention.StdCall); + + // Get the enum type from the first argument (typeof(enum)) + ITypeSymbol? enumTypeSymbol = null; + if (arguments is { Count: > 0 }) + { + if ((arguments[0] as ArgumentSyntax).Expression is TypeOfExpressionSyntax typeOfExpression) + { + var typeInfo = model.GetTypeInfo(typeOfExpression.Type); + enumTypeSymbol = typeInfo.Type; + } + else + { + throw new CompilationException(arguments[0], DiagnosticId.InvalidArgument, "First argument must be a typeof(enum)"); + } + } + else + { + throw new CompilationException(symbol.Locations.FirstOrDefault().MetadataModule, DiagnosticId.InvalidArgument, "Enum.GetName requires at least two arguments"); + } + + if (enumTypeSymbol is not { TypeKind: TypeKind.Enum }) + { + throw new CompilationException(arguments![0], DiagnosticId.InvalidType, "Unable to determine enum type from first argument"); + } + + var enumMembers = enumTypeSymbol.GetMembers().OfType() + .Where(field => field is { HasConstantValue: true, IsImplicitlyDeclared: false }).ToArray(); + + foreach (var t in enumMembers) + { + methodConvert.Dup(); // Duplicate input value + methodConvert.Push(t.ConstantValue); // Push enum value + methodConvert.Equal(); // Compare values + + var nextCheck = new JumpTarget(); + methodConvert.JumpIfNot(nextCheck); // If not equal, check next + + methodConvert.Drop(2); // Remove the duplicated input value + methodConvert.Push(t.Name); // Push enum name + methodConvert.AddInstruction(OpCode.RET); // Return enum name + + nextCheck.Instruction = methodConvert.AddInstruction(OpCode.NOP); + } + + // No match found + methodConvert.Drop(2); // Remove the input value + methodConvert.AddInstruction(OpCode.PUSHNULL); // Push null (no matching enum name) + methodConvert.AddInstruction(OpCode.RET); // Return null + } + + private static void HandleEnumTryParse(MethodConvert methodConvert, SemanticModel model, IMethodSymbol symbol, + ExpressionSyntax? instanceExpression, IReadOnlyList? arguments) + { + if (instanceExpression is not null) + methodConvert.ConvertExpression(model, instanceExpression); + if (arguments is not null) + methodConvert.PrepareArgumentsForMethod(model, symbol, arguments, CallingConvention.StdCall); + + if (!methodConvert._context.TryGetCapturedStaticField(symbol.Parameters[2], out var index)) + throw new CompilationException(symbol, DiagnosticId.SyntaxNotSupported, "Out parameter must be captured in a static field."); + + // Get the enum type from the first argument (typeof(enum)) + ITypeSymbol? enumTypeSymbol = null; + if (arguments is { Count: > 0 }) + { + if ((arguments[0] as ArgumentSyntax).Expression is TypeOfExpressionSyntax typeOfExpression) + { + var typeInfo = model.GetTypeInfo(typeOfExpression.Type); + enumTypeSymbol = typeInfo.Type; + } + else + { + throw new CompilationException(arguments[0], DiagnosticId.InvalidArgument, "First argument must be a typeof(enum)"); + } + } + else + { + throw new CompilationException(symbol.Locations.FirstOrDefault().MetadataModule, DiagnosticId.InvalidArgument, "Enum.TryParse requires at least three arguments"); + } + + if (enumTypeSymbol is not { TypeKind: TypeKind.Enum }) + { + throw new CompilationException(arguments[0], DiagnosticId.InvalidType, "Unable to determine enum type from first argument"); + } + + var enumMembers = enumTypeSymbol.GetMembers().OfType() + .Where(field => field is { HasConstantValue: true, IsImplicitlyDeclared: false }).ToArray(); + + JumpTarget endTarget = new(); + methodConvert.Drop(); + foreach (var t in enumMembers) + { + methodConvert.Dup(); // Stack: [..., inputString, inputString] + methodConvert.Push(t.Name); // Stack: [..., inputString, inputString, enumName] + + // Equal comparison + methodConvert.Equal(); // Stack: [..., inputString, isEqual] + + JumpTarget nextCheck = new(); + methodConvert.JumpIfNot(nextCheck); + + // If equal: + methodConvert.Drop(2); // Remove the inputString + methodConvert.Push(t.ConstantValue); // Stack: [..., enumValue] + methodConvert.AccessSlot(OpCode.STSFLD, index); // Store enum value in out parameter + methodConvert.Push(true); // Stack: [..., true] + methodConvert.Ret(); + + nextCheck.Instruction = methodConvert.AddInstruction(OpCode.NOP); + } + + // No match found + methodConvert.Drop(2); // Remove the inputString + methodConvert.Push(0); // Default enum value + methodConvert.AccessSlot(OpCode.STSFLD, index); // Store default value in out parameter + methodConvert.Push(false); // Success flag set to false + + endTarget.Instruction = methodConvert.AddInstruction(OpCode.NOP); + } + + private static void HandleEnumGetNames(MethodConvert methodConvert, SemanticModel model, IMethodSymbol symbol, + ExpressionSyntax? instanceExpression, IReadOnlyList? arguments) + { + if (instanceExpression is not null) + methodConvert.ConvertExpression(model, instanceExpression); + if (arguments is not null) + methodConvert.PrepareArgumentsForMethod(model, symbol, arguments, CallingConvention.StdCall); + + // Get the enum type from the first argument (typeof(enum)) + ITypeSymbol? enumTypeSymbol = null; + if (arguments is { Count: > 0 }) + { + if ((arguments[0] as ArgumentSyntax).Expression is TypeOfExpressionSyntax typeOfExpression) + { + var typeInfo = model.GetTypeInfo(typeOfExpression.Type); + enumTypeSymbol = typeInfo.Type; + } + else + { + throw new CompilationException(arguments[0], DiagnosticId.InvalidArgument, "First argument must be a typeof(enum)"); + } + } + else + { + throw new CompilationException(symbol.Locations.FirstOrDefault().MetadataModule, DiagnosticId.InvalidArgument, "Enum.GetNames requires one argument"); + } + + if (enumTypeSymbol is not { TypeKind: TypeKind.Enum }) + { + throw new CompilationException(arguments![0], DiagnosticId.InvalidType, "Unable to determine enum type from first argument"); + } + + var enumMembers = enumTypeSymbol.GetMembers().OfType() + .Where(field => field is { HasConstantValue: true, IsImplicitlyDeclared: false }).ToArray(); + + // Create an array of names + methodConvert.Push(enumMembers.Length); // Stack: [..., size] + methodConvert.AddInstruction(OpCode.NEWARRAY); // Stack: [..., array] + + for (int i = 0; i < enumMembers.Length; i++) + { + methodConvert.Dup(); // Duplicate array reference + methodConvert.Push(i); // Index + methodConvert.Push(enumMembers[i].Name); // Name + methodConvert.AddInstruction(OpCode.SETITEM); // array[i] = name + } + + methodConvert.AddInstruction(OpCode.RET); + } + + private static void HandleEnumGetValues(MethodConvert methodConvert, SemanticModel model, IMethodSymbol symbol, + ExpressionSyntax? instanceExpression, IReadOnlyList? arguments) + { + if (instanceExpression is not null) + methodConvert.ConvertExpression(model, instanceExpression); + if (arguments is not null) + methodConvert.PrepareArgumentsForMethod(model, symbol, arguments, CallingConvention.StdCall); + + // Get the enum type from the first argument (typeof(enum)) + ITypeSymbol? enumTypeSymbol = null; + if (arguments is { Count: > 0 }) + { + if ((arguments[0] as ArgumentSyntax).Expression is TypeOfExpressionSyntax typeOfExpression) + { + var typeInfo = model.GetTypeInfo(typeOfExpression.Type); + enumTypeSymbol = typeInfo.Type; + } + else + { + throw new CompilationException(arguments[0], DiagnosticId.InvalidArgument, "First argument must be a typeof(enum)"); + } + } + else + { + throw new CompilationException(symbol.Locations.FirstOrDefault().MetadataModule, DiagnosticId.InvalidArgument, "Enum.GetValues requires one argument"); + } + + if (enumTypeSymbol is not { TypeKind: TypeKind.Enum }) + { + throw new CompilationException(arguments![0], DiagnosticId.InvalidType, "Unable to determine enum type from first argument"); + } + + var enumMembers = enumTypeSymbol.GetMembers().OfType() + .Where(field => field is { HasConstantValue: true, IsImplicitlyDeclared: false }).ToArray(); + + // Create an array of values + methodConvert.Push(enumMembers.Length); // Stack: [..., size] + methodConvert.AddInstruction(OpCode.NEWARRAY); // Stack: [..., array] + + for (int i = 0; i < enumMembers.Length; i++) + { + methodConvert.Dup(); // Duplicate array reference + methodConvert.Push(i); // Index + methodConvert.Push(enumMembers[i].ConstantValue); // Value + methodConvert.AddInstruction(OpCode.SETITEM); // array[i] = value + } + + methodConvert.AddInstruction(OpCode.RET); + } + + private static void HandleEnumIsDefined(MethodConvert methodConvert, SemanticModel model, IMethodSymbol symbol, + ExpressionSyntax? instanceExpression, IReadOnlyList? arguments) + { + if (instanceExpression is not null) + methodConvert.ConvertExpression(model, instanceExpression); + if (arguments is not null) + methodConvert.PrepareArgumentsForMethod(model, symbol, arguments, CallingConvention.StdCall); + + // Get the enum type from the first argument (typeof(enum)) + ITypeSymbol? enumTypeSymbol = null; + if (arguments is { Count: > 0 }) + { + if ((arguments[0] as ArgumentSyntax).Expression is TypeOfExpressionSyntax typeOfExpression) + { + var typeInfo = model.GetTypeInfo(typeOfExpression.Type); + enumTypeSymbol = typeInfo.Type; + } + else + { + throw new CompilationException(arguments[0], DiagnosticId.InvalidArgument, "First argument must be a typeof(enum)"); + } + } + else + { + throw new CompilationException(symbol.Locations.FirstOrDefault().MetadataModule, DiagnosticId.InvalidArgument, "Enum.IsDefined requires at least two arguments"); + } + + if (enumTypeSymbol is not { TypeKind: TypeKind.Enum }) + { + throw new CompilationException(arguments![0], DiagnosticId.InvalidType, "Unable to determine enum type from first argument"); + } + + var enumMembers = enumTypeSymbol.GetMembers().OfType() + .Where(field => field is { HasConstantValue: true, IsImplicitlyDeclared: false }).ToArray(); + + var valueType = model.GetTypeInfo((arguments[1] as ArgumentSyntax)?.Expression).Type; + + // We need to compare the input value against the enum values + var endLabel = new JumpTarget(); + // here add check logic to verify if valueType is string + var isName = valueType is INamedTypeSymbol { Name: "String" }; + + foreach (var t in enumMembers) + { + methodConvert.Dup(); // Duplicate value to compare + if (isName) + { + methodConvert.Push(t.Name); + } + else + { + methodConvert.Push(t.ConstantValue); + } + + // Equal comparison + methodConvert.Equal(); // Stack: [..., isEqual] + + var nextCheck = new JumpTarget(); + methodConvert.JumpIfNot(nextCheck); + + // If equal, set result to true + methodConvert.Drop(); + methodConvert.Drop(); // Remove the false + methodConvert.Push(true); // Set result to true + methodConvert.Ret(); + nextCheck.Instruction = methodConvert.AddInstruction(OpCode.NOP); + } + methodConvert.Drop(); + methodConvert.Drop(); // Remove the duplicated value + methodConvert.Push(false); + } +} diff --git a/src/Neo.Compiler.CSharp/MethodConvert/System/SystemCall.Register.cs b/src/Neo.Compiler.CSharp/MethodConvert/System/SystemCall.Register.cs index 3d0876cde..ca0a2ecae 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/System/SystemCall.Register.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/System/SystemCall.Register.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Numerics; @@ -35,6 +36,8 @@ private static void RegisterSystemCallHandlers() // BitOperations handlers RegisterBitOperationsHandlers(); + + RegisterEnumHandlers(); } private static void RegisterBigIntegerHandlers() @@ -618,7 +621,6 @@ private static void RegisterCharHandlers() RegisterHandler((char c) => char.IsLetterOrDigit(c), HandleCharIsLetterOrDigit); RegisterHandler((char x, char min, char max) => char.IsBetween(x, min, max), HandleCharIsBetween); - // Add missing char methods RegisterHandler((char c) => char.ToLowerInvariant(c), HandleCharToLowerInvariant); RegisterHandler((char c) => char.ToUpperInvariant(c), HandleCharToUpperInvariant); RegisterHandler((char c) => char.IsAscii(c), HandleCharIsAscii); @@ -747,4 +749,25 @@ private static void RegisterBitOperationsHandlers() RegisterHandler((ulong value, int offset) => BitOperations.RotateRight(value, offset), HandleULongRotateRight); } + private static void RegisterEnumHandlers() + { + RegisterHandler((Type enumType, string value) => Enum.Parse(enumType, value), HandleEnumParse); + RegisterHandler((Type enumType, string value, bool ignoreCase) => Enum.Parse(enumType, value, ignoreCase), HandleEnumParseIgnoreCase); + RegisterHandler((Type enumType, string value, object result) => Enum.TryParse(enumType, value, out result), HandleEnumTryParse); + RegisterHandler((Type enumType, string value, bool ignoreCase, object result) => Enum.TryParse(enumType, value, ignoreCase, out result), HandleEnumTryParseIgnoreCase); + RegisterHandler((Type enumType) => Enum.GetNames(enumType), HandleEnumGetNames); + RegisterHandler((Type enumType) => Enum.GetValues(enumType), HandleEnumGetValues); + RegisterHandler((Type enumType, object value) => Enum.IsDefined(enumType, value), HandleEnumIsDefined); + RegisterHandler((Type enumType, string name) => Enum.IsDefined(enumType, name), HandleEnumIsDefinedByName); + RegisterHandler((Enum value) => Enum.GetName(value.GetType(), value), HandleEnumGetName, "System.Enum.GetName<>()"); + RegisterHandler((Type enumType, object value) => Enum.GetName(enumType, value), HandleEnumGetNameWithType, "System.Enum.GetName()"); + + // these two methods will not be supported, since we cannot apply format logic. + // RegisterHandler((Enum value) => Enum.Format(value.GetType(), value, "G"), HandleEnumFormat); + // RegisterHandler((Type enumType, object value, string format) => Enum.Format(enumType, value, format), HandleEnumFormatWithType); + + // these two methods will not be supported, since we don't have `Type` class support in neo csharp. + // RegisterHandler((Enum value) => Enum.GetUnderlyingType(value.GetType()), HandleEnumGetUnderlyingType); + // RegisterHandler((Type enumType) => Enum.GetUnderlyingType(enumType), HandleEnumGetUnderlyingTypeWithType); + } } diff --git a/src/Neo.Compiler.CSharp/MethodConvert/System/SystemCall.cs b/src/Neo.Compiler.CSharp/MethodConvert/System/SystemCall.cs index e3280b1dc..2af124b3f 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/System/SystemCall.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/System/SystemCall.cs @@ -41,15 +41,15 @@ private static void RegisterHandler(Expression> expressio SystemCallHandlers[key] = handler; } - private static void RegisterHandler(Expression> expression, SystemCallHandler handler) + private static void RegisterHandler(Expression> expression, SystemCallHandler handler, string? key = null) { - var key = GetKeyFromExpression(expression, typeof(T)); + key = key ?? GetKeyFromExpression(expression, typeof(T)); SystemCallHandlers[key] = handler; } - private static void RegisterHandler(Expression> expression, SystemCallHandler handler) + private static void RegisterHandler(Expression> expression, SystemCallHandler handler, string? key = null) { - var key = GetKeyFromExpression(expression, typeof(T1), typeof(T2)); + key = key ?? GetKeyFromExpression(expression, typeof(T1), typeof(T2)); SystemCallHandlers[key] = handler; } @@ -60,7 +60,13 @@ private static void RegisterHandler(Expression(Expression> expression, SystemCallHandler handler) { - var key = GetKeyFromExpression(expression); + var key = GetKeyFromExpression(expression, typeof(T1), typeof(T2), typeof(T3), typeof(T4)); + SystemCallHandlers[key] = handler; + } + + private static void RegisterHandler(Expression> expression, SystemCallHandler handler) + { + var key = GetKeyFromExpression(expression, typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5)); SystemCallHandlers[key] = handler; } @@ -200,6 +206,8 @@ private static string GetShortTypeName(Type type) _ when type == typeof(BigInteger) => "System.Numerics.BigInteger", _ when type == typeof(Array) => "System.Array", _ when type == typeof(Math) => "System.Math", + _ when type == typeof(Type) => "System.Type", + _ when type == typeof(Enum) => "System.Enum", _ when type.IsGenericType => $"{type.Name.Split('`')[0]}<{string.Join(", ", type.GetGenericArguments().Select(GetShortTypeName))}>", _ => type.Name, }; @@ -263,6 +271,8 @@ private bool TryProcessSystemMethods(SemanticModel model, IMethodSymbol symbol, var key = symbol.ToString()!.Replace("out ", ""); key = (from parameter in symbol.Parameters let parameterType = parameter.Type.ToString() where !parameter.Type.IsValueType && parameterType!.EndsWith('?') select parameterType).Aggregate(key, (current, parameterType) => current.Replace(parameterType, parameterType[..^1])); if (key == "string.ToString()") key = "object.ToString()"; + if (key.StartsWith("System.Enum.GetName<")) key = "System.Enum.GetName<>()"; + if (key.StartsWith("System.Enum.GetName(")) key = "System.Enum.GetName()"; if (!SystemCallHandlers.TryGetValue(key, out var handler)) return false; handler(this, model, symbol, instanceExpression, arguments); return true; diff --git a/src/Neo.SmartContract.Analyzer/AnalyzerReleases.Unshipped.md b/src/Neo.SmartContract.Analyzer/AnalyzerReleases.Unshipped.md index 478fb74a8..c36dd353b 100644 --- a/src/Neo.SmartContract.Analyzer/AnalyzerReleases.Unshipped.md +++ b/src/Neo.SmartContract.Analyzer/AnalyzerReleases.Unshipped.md @@ -24,4 +24,5 @@ | NC4022 | Usage | Warning | BigIntegerUsingUsageAnalyzer | | NC4023 | Usage | Error | StaticFieldInitializationAnalyzer | | NC4024 | Usage | Error | MultipleCatchBlockAnalyzer | -| NC4025 | Usage | Error | SystemDiagnosticsUsageAnalyzer | +| NC4025 | Method | Error | EnumMethodsUsageAnalyzer | +| NC4026 | Usage | Error | SystemDiagnosticsUsageAnalyzer | diff --git a/src/Neo.SmartContract.Analyzer/EnumMethodsUsageAnalyzer.cs b/src/Neo.SmartContract.Analyzer/EnumMethodsUsageAnalyzer.cs new file mode 100644 index 000000000..89d285c6e --- /dev/null +++ b/src/Neo.SmartContract.Analyzer/EnumMethodsUsageAnalyzer.cs @@ -0,0 +1,54 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; +using System.Linq; + +namespace Neo.SmartContract.Analyzer +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class EnumMethodsUsageAnalyzer : DiagnosticAnalyzer + { + public const string DiagnosticId = "NC4025"; + + private readonly string[] _unsupportedEnumMethods = { + "Format", + "GetUnderlyingType" + }; + + private static readonly DiagnosticDescriptor Rule = new( + DiagnosticId, + "Unsupported Enum method is used", + "Unsupported Enum method: {0}", + "Method", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.InvocationExpression); + } + + private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + { + if (context.Node is not InvocationExpressionSyntax invocationExpression) return; + + var methodSymbol = context.SemanticModel.GetSymbolInfo(invocationExpression).Symbol as IMethodSymbol; + + if (methodSymbol is not { ContainingType.SpecialType: SpecialType.System_Enum } || + !_unsupportedEnumMethods.Contains(methodSymbol.Name)) return; + + var diagnostic = Diagnostic.Create(Rule, + invocationExpression.GetLocation(), + methodSymbol.Name); + + context.ReportDiagnostic(diagnostic); + } + } +} diff --git a/tests/Neo.Compiler.CSharp.TestContracts/Contract_Enum.cs b/tests/Neo.Compiler.CSharp.TestContracts/Contract_Enum.cs new file mode 100644 index 000000000..b08d3ce51 --- /dev/null +++ b/tests/Neo.Compiler.CSharp.TestContracts/Contract_Enum.cs @@ -0,0 +1,66 @@ +using Neo.SmartContract.Framework.Attributes; +using System.Collections; +using System.Numerics; + +namespace Neo.Compiler.CSharp.TestContracts +{ + public class Contract_Enum : SmartContract.Framework.SmartContract + { + public enum TestEnum + { + Value1 = 1, + Value2 = 2, + Value3 = 3 + } + + public static object TestEnumParse(string value) + { + return System.Enum.Parse(typeof(TestEnum), value); + } + + public static object TestEnumParseIgnoreCase(string value, bool ignoreCase) + { + return System.Enum.Parse(typeof(TestEnum), value, ignoreCase); + } + + public static bool TestEnumTryParse(string value) + { + return System.Enum.TryParse(typeof(TestEnum), value, out object result); + } + + public static bool TestEnumTryParseIgnoreCase(string value, bool ignoreCase) + { + return System.Enum.TryParse(typeof(TestEnum), value, ignoreCase, out object result); + } + + public static string[] TestEnumGetNames() + { + return System.Enum.GetNames(typeof(TestEnum)); + } + + public static TestEnum[] TestEnumGetValues() + { + return (TestEnum[])System.Enum.GetValues(typeof(TestEnum)); + } + + public static bool TestEnumIsDefined(object value) + { + return System.Enum.IsDefined(typeof(TestEnum), value); + } + + public static bool TestEnumIsDefinedByName(string name) + { + return System.Enum.IsDefined(typeof(TestEnum), name); + } + + public static string TestEnumGetName(TestEnum value) + { + return System.Enum.GetName(value); + } + + public static string TestEnumGetNameWithType(object value) + { + return System.Enum.GetName(typeof(TestEnum), value); + } + } +} diff --git a/tests/Neo.Compiler.CSharp.UnitTests/TestingArtifacts/Contract_Enum.cs b/tests/Neo.Compiler.CSharp.UnitTests/TestingArtifacts/Contract_Enum.cs new file mode 100644 index 000000000..bb118b4b4 --- /dev/null +++ b/tests/Neo.Compiler.CSharp.UnitTests/TestingArtifacts/Contract_Enum.cs @@ -0,0 +1,86 @@ +using Neo.Cryptography.ECC; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Numerics; + +namespace Neo.SmartContract.Testing; + +public abstract class Contract_Enum(Neo.SmartContract.Testing.SmartContractInitialize initialize) : Neo.SmartContract.Testing.SmartContract(initialize), IContractInfo +{ + #region Compiled data + + public static Neo.SmartContract.Manifest.ContractManifest Manifest => Neo.SmartContract.Manifest.ContractManifest.Parse(@"{""name"":""Contract_Enum"",""groups"":[],""features"":{},""supportedstandards"":[],""abi"":{""methods"":[{""name"":""testEnumParse"",""parameters"":[{""name"":""value"",""type"":""String""}],""returntype"":""Any"",""offset"":0,""safe"":false},{""name"":""testEnumParseIgnoreCase"",""parameters"":[{""name"":""value"",""type"":""String""},{""name"":""ignoreCase"",""type"":""Boolean""}],""returntype"":""Any"",""offset"":87,""safe"":false},{""name"":""testEnumTryParse"",""parameters"":[{""name"":""value"",""type"":""String""}],""returntype"":""Boolean"",""offset"":241,""safe"":false},{""name"":""testEnumTryParseIgnoreCase"",""parameters"":[{""name"":""value"",""type"":""String""},{""name"":""ignoreCase"",""type"":""Boolean""}],""returntype"":""Boolean"",""offset"":319,""safe"":false},{""name"":""testEnumGetNames"",""parameters"":[],""returntype"":""Array"",""offset"":465,""safe"":false},{""name"":""testEnumGetValues"",""parameters"":[],""returntype"":""Array"",""offset"":511,""safe"":false},{""name"":""testEnumIsDefined"",""parameters"":[{""name"":""value"",""type"":""Any""}],""returntype"":""Boolean"",""offset"":536,""safe"":false},{""name"":""testEnumIsDefinedByName"",""parameters"":[{""name"":""name"",""type"":""String""}],""returntype"":""Boolean"",""offset"":581,""safe"":false},{""name"":""testEnumGetName"",""parameters"":[{""name"":""value"",""type"":""Integer""}],""returntype"":""String"",""offset"":647,""safe"":false},{""name"":""testEnumGetNameWithType"",""parameters"":[{""name"":""value"",""type"":""Any""}],""returntype"":""String"",""offset"":699,""safe"":false},{""name"":""_initialize"",""parameters"":[],""returntype"":""Void"",""offset"":765,""safe"":false}],""events"":[]},""permissions"":[],""trusts"":[],""extra"":{""nef"":{""optimization"":""All""}}}"); + + /// + /// Optimization: "All" + /// + public static Neo.SmartContract.NefFile Nef => Neo.IO.Helper.AsSerializable(Convert.FromBase64String(@"TkVGM1Rlc3RpbmdFbmdpbmUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP0AA1cAAQwIVGVzdEVudW14SgwGVmFsdWUxlyYHEVNFRUBKDAZWYWx1ZTKXJgcSU0VFQEoMBlZhbHVlM5cmBxNTRUVARQwSTm8gc3VjaCBlbnVtIHZhbHVlOlcAAgwIVGVzdEVudW14eSYuDAAQSnjKtSYiSnhQzkoAYQB7uyQJUVCLUJwi6QBhnwBBnlFQi1CcItxF2yhKeSY0DAZWQUxVRTGXJgdFRUURQEp5JiAMBlZBTFVFMpcmB0VFRRJASnkmDAwGVkFMVUUzIgoMBlZhbHVlM5cmB0VFRRNARUUMEk5vIHN1Y2ggZW51bSB2YWx1ZTpXAAELYAwIVGVzdEVudW14WEVKDAZWYWx1ZTGXJghFRRFgCEBKDAZWYWx1ZTKXJghFRRJgCEBKDAZWYWx1ZTOXJghFRRNgCEBFRRBgCUBXAAILYQwIVGVzdEVudW14eVlFJjBQRQwAEEp4yrUmIkp4UM5KAGEAe7skCVFQi1CcIukAYZ8AQZ5RUItQnCLcRdsoSnkmNgwGVkFMVUUxlyYIRUURYQhASnkmIQwGVkFMVUUylyYIRUUSYQhASnkmDAwGVkFMVUUzIgoMBlZhbHVlM5cmCEVFE2EIQEVFEGEJQAwIVGVzdEVudW0Tw0oQDAZWYWx1ZTHQShEMBlZhbHVlMtBKEgwGVmFsdWUz0EAMCFRlc3RFbnVtE8NKEBHQShES0EoSE9BAVwABDAhUZXN0RW51bXhKEZcmBkVFCEBKEpcmBkVFCEBKE5cmBkVFCEBFRQlAVwABDAhUZXN0RW51bXhKDAZWYWx1ZTGXJgZFRQhASgwGVmFsdWUylyYGRUUIQEoMBlZhbHVlM5cmBkVFCEBFRQlAVwABeEoRlyYMRQwGVmFsdWUxQEoSlyYMRQwGVmFsdWUyQEoTlyYMRQwGVmFsdWUzQEULQFcAAQwIVGVzdEVudW14ShGXJg1FRQwGVmFsdWUxQEoSlyYNRUUMBlZhbHVlMkBKE5cmDUVFDAZWYWx1ZTNARUULQFYCQCsBkiY=")); + + #endregion + + #region Unsafe methods + + /// + /// Unsafe method + /// + [DisplayName("testEnumGetName")] + public abstract string? TestEnumGetName(BigInteger? value); + + /// + /// Unsafe method + /// + [DisplayName("testEnumGetNames")] + public abstract IList? TestEnumGetNames(); + + /// + /// Unsafe method + /// + [DisplayName("testEnumGetNameWithType")] + public abstract string? TestEnumGetNameWithType(object? value = null); + + /// + /// Unsafe method + /// + [DisplayName("testEnumGetValues")] + public abstract IList? TestEnumGetValues(); + + /// + /// Unsafe method + /// + [DisplayName("testEnumIsDefined")] + public abstract bool? TestEnumIsDefined(object? value = null); + + /// + /// Unsafe method + /// + [DisplayName("testEnumIsDefinedByName")] + public abstract bool? TestEnumIsDefinedByName(string? name); + + /// + /// Unsafe method + /// + [DisplayName("testEnumParse")] + public abstract object? TestEnumParse(string? value); + + /// + /// Unsafe method + /// + [DisplayName("testEnumParseIgnoreCase")] + public abstract object? TestEnumParseIgnoreCase(string? value, bool? ignoreCase); + + /// + /// Unsafe method + /// + [DisplayName("testEnumTryParse")] + public abstract bool? TestEnumTryParse(string? value); + + /// + /// Unsafe method + /// + [DisplayName("testEnumTryParseIgnoreCase")] + public abstract bool? TestEnumTryParseIgnoreCase(string? value, bool? ignoreCase); + + #endregion + +} diff --git a/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_Enum.cs b/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_Enum.cs new file mode 100644 index 000000000..196e65fed --- /dev/null +++ b/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_Enum.cs @@ -0,0 +1,127 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.SmartContract.Testing; +using Neo.SmartContract.Testing.Exceptions; +using Neo.VM.Types; + +namespace Neo.Compiler.CSharp.UnitTests +{ + [TestClass] + public class UnitTest_Enum : DebugAndTestBase + { + [TestMethod] + public void TestEnumParse() + { + Assert.AreEqual(new Integer(1), Contract.TestEnumParse("Value1")); + AssertGasConsumed(1049490); + Assert.AreEqual(new Integer(2), Contract.TestEnumParse("Value2")); + AssertGasConsumed(1050810); + Assert.AreEqual(new Integer(3), Contract.TestEnumParse("Value3")); + AssertGasConsumed(1052130); + Assert.ThrowsException(() => Contract.TestEnumParse("InvalidValue")); + AssertGasConsumed(1067580); + } + + [TestMethod] + public void TestEnumParseIgnoreCase() + { + Assert.AreEqual(new Integer(1), Contract.TestEnumParseIgnoreCase("value1", true)); + AssertGasConsumed(1688250); + Assert.AreEqual(new Integer(2), Contract.TestEnumParseIgnoreCase("VALUE2", true)); + AssertGasConsumed(1686990); + Assert.AreEqual(new Integer(3), Contract.TestEnumParseIgnoreCase("VaLuE3", true)); + AssertGasConsumed(1689570); + Assert.ThrowsException(() => Contract.TestEnumParseIgnoreCase("value1", false)); + AssertGasConsumed(1065270); + Assert.ThrowsException(() => Contract.TestEnumParseIgnoreCase("InvalidValue", true)); + AssertGasConsumed(2098560); + } + + [TestMethod] + public void TestEnumTryParse() + { + Assert.IsTrue(Contract.TestEnumTryParse("Value1")); + AssertGasConsumed(1049730); + Assert.IsTrue(Contract.TestEnumTryParse("Value2")); + AssertGasConsumed(1051050); + Assert.IsTrue(Contract.TestEnumTryParse("Value3")); + AssertGasConsumed(1052370); + Assert.IsFalse(Contract.TestEnumTryParse("InvalidValue")); + AssertGasConsumed(1052370); + } + + [TestMethod] + public void TestEnumTryParseIgnoreCase() + { + Assert.IsTrue(Contract.TestEnumTryParseIgnoreCase("value1", true)); + AssertGasConsumed(1688610); + Assert.IsTrue(Contract.TestEnumTryParseIgnoreCase("VALUE2", true)); + AssertGasConsumed(1687350); + Assert.IsTrue(Contract.TestEnumTryParseIgnoreCase("VaLuE3", true)); + AssertGasConsumed(1689930); + Assert.IsFalse(Contract.TestEnumTryParseIgnoreCase("value1", false)); + AssertGasConsumed(1050000); + Assert.IsFalse(Contract.TestEnumTryParseIgnoreCase("InvalidValue", true)); + AssertGasConsumed(2083410); + } + + [TestMethod] + public void TestEnumIsDefined() + { + Assert.IsTrue(Contract.TestEnumIsDefined(1)); + AssertGasConsumed(1049010); + Assert.IsTrue(Contract.TestEnumIsDefined(2)); + AssertGasConsumed(1050120); + Assert.IsTrue(Contract.TestEnumIsDefined(3)); + AssertGasConsumed(1051230); + Assert.IsFalse(Contract.TestEnumIsDefined(0)); + AssertGasConsumed(1051230); + Assert.IsFalse(Contract.TestEnumIsDefined(4)); + AssertGasConsumed(1051230); + } + + [TestMethod] + public void TestEnumIsDefinedByName() + { + Assert.IsTrue(Contract.TestEnumIsDefinedByName("Value1")); + AssertGasConsumed(1049430); + Assert.IsTrue(Contract.TestEnumIsDefinedByName("Value2")); + AssertGasConsumed(1050750); + Assert.IsTrue(Contract.TestEnumIsDefinedByName("Value3")); + AssertGasConsumed(1052070); + Assert.IsFalse(Contract.TestEnumIsDefinedByName("value1")); + AssertGasConsumed(1052070); + Assert.IsFalse(Contract.TestEnumIsDefinedByName("InvalidValue")); + AssertGasConsumed(1052070); + } + + [TestMethod] + public void TestEnumGetName() + { + Assert.AreEqual("Value1", Contract.TestEnumGetName(1)); + AssertGasConsumed(1048920); + Assert.AreEqual("Value2", Contract.TestEnumGetName(2)); + AssertGasConsumed(1050030); + Assert.AreEqual("Value3", Contract.TestEnumGetName(3)); + AssertGasConsumed(1051140); + Assert.IsNull(Contract.TestEnumGetName(0)); + AssertGasConsumed(1050930); + Assert.IsNull(Contract.TestEnumGetName(4)); + AssertGasConsumed(1050930); + } + + [TestMethod] + public void TestEnumGetNameWithType() + { + Assert.AreEqual("Value1", Contract.TestEnumGetNameWithType(1)); + AssertGasConsumed(1049220); + Assert.AreEqual("Value2", Contract.TestEnumGetNameWithType(2)); + AssertGasConsumed(1050330); + Assert.AreEqual("Value3", Contract.TestEnumGetNameWithType(3)); + AssertGasConsumed(1051440); + Assert.IsNull(Contract.TestEnumGetNameWithType(0)); + AssertGasConsumed(1051230); + Assert.IsNull(Contract.TestEnumGetNameWithType(4)); + AssertGasConsumed(1051230); + } + } +} diff --git a/tests/Neo.SmartContract.Analyzer.UnitTests/EnumMethodsUsageAnalyzerUnitTests.cs b/tests/Neo.SmartContract.Analyzer.UnitTests/EnumMethodsUsageAnalyzerUnitTests.cs new file mode 100644 index 000000000..18a003314 --- /dev/null +++ b/tests/Neo.SmartContract.Analyzer.UnitTests/EnumMethodsUsageAnalyzerUnitTests.cs @@ -0,0 +1,75 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Threading.Tasks; +using VerifyCS = Microsoft.CodeAnalysis.CSharp.Testing.XUnit.AnalyzerVerifier< + Neo.SmartContract.Analyzer.EnumMethodsUsageAnalyzer>; + +namespace Neo.SmartContract.Analyzer.UnitTests +{ + [TestClass] + public class EnumMethodsUsageAnalyzerUnitTests + { + private const string DiagnosticId = EnumMethodsUsageAnalyzer.DiagnosticId; + + [TestMethod] + public async Task UnsupportedEnumFormat_ReportsDiagnostic() + { + var test = @" +using System; + +class TestClass +{ + void TestMethod() + { + var result = Enum.Format(typeof(DayOfWeek), DayOfWeek.Monday, ""G""); + } +}"; + + var expected = VerifyCS.Diagnostic(DiagnosticId) + .WithLocation(8, 22) // Line 8, column 22 + .WithArguments("Format"); + await VerifyCS.VerifyAnalyzerAsync(test, expected); + } + + [TestMethod] + public async Task UnsupportedEnumGetUnderlyingType_ReportsDiagnostic() + { + var test = @" +using System; + +class TestClass +{ + void TestMethod() + { + var result = Enum.GetUnderlyingType(typeof(DayOfWeek)); + } +}"; + + var expected = VerifyCS.Diagnostic(DiagnosticId) + .WithSpan(8, 22, 8, 63) + .WithArguments("GetUnderlyingType"); + await VerifyCS.VerifyAnalyzerAsync(test, expected); + } + + [TestMethod] + public async Task SupportedEnumMethods_NoDiagnostic() + { + var test = @" +using System; + +class TestClass +{ + void TestMethod() + { + var parsed = Enum.Parse(typeof(DayOfWeek), ""Monday""); + var tryParsed = Enum.TryParse(typeof(DayOfWeek), ""Tuesday"", out var result); + var names = Enum.GetNames(typeof(DayOfWeek)); + var values = Enum.GetValues(typeof(DayOfWeek)); + var isDefined = Enum.IsDefined(typeof(DayOfWeek), ""Wednesday""); + var name = Enum.GetName(typeof(DayOfWeek), DayOfWeek.Thursday); + } +}"; + + await VerifyCS.VerifyAnalyzerAsync(test); + } + } +}