using System.Text; using System.Collections.Immutable; using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Linq; namespace RobloxCS { internal class CodeGeneratorFlags { public bool ShouldCallGetAssemblyType { get; set; } public bool ClientEntryPointDefined { get; set; } public bool ServerEntryPointDefined { get; set; } } internal sealed class CodeGenerator : CSharpSyntaxWalker { private readonly SyntaxTree _tree; private readonly ConfigData _config; private readonly MemberCollectionResult _members; private readonly string _inputDirectory; private readonly CSharpCompilation _compiler; private readonly SemanticModel _semanticModel; private readonly SymbolAnalyzerResults _symbolAnalysis; private readonly INamespaceSymbol _globalNamespace; private readonly INamespaceSymbol _runtimeLibNamespace; private readonly RojoProject? _rojoProject; private readonly int _indentSize; private readonly CodeGeneratorFlags _flags = new CodeGeneratorFlags { ShouldCallGetAssemblyType = true, ClientEntryPointDefined = false, ServerEntryPointDefined = false }; private readonly StringBuilder _output = new StringBuilder(); private int _indent = 0; public CodeGenerator( SyntaxTree tree, CSharpCompilation compiler, RojoProject? rojoProject, MemberCollectionResult members, ConfigData config, string inputDirectory, int indentSize = 4 ) { _tree = tree; _config = config; _members = members; _inputDirectory = inputDirectory; _compiler = compiler; _semanticModel = compiler.GetSemanticModel(tree); var symbolAnalyzer = new SymbolAnalyzer(tree, _semanticModel); _symbolAnalysis = symbolAnalyzer.Analyze(); _globalNamespace = compiler.GlobalNamespace; _runtimeLibNamespace = _globalNamespace.GetNamespaceMembers().FirstOrDefault(ns => ns.Name == Utility.RuntimeAssemblyName)!; _rojoProject = rojoProject; _indentSize = indentSize; } public string GenerateLua() { WriteHeader(); Visit(_tree.GetRoot()); WriteFooter(); return _output.ToString().Trim(); } private void WriteHeader() { Write($"local CS = "); WriteRequire(GetLuaRuntimeLibPath()); WriteLine(); foreach (var (namespaceName, declaringFiles) in GetNamespaceFilePaths()) { var symbol = _tree.GetRoot() .DescendantNodes() .Select(node => _semanticModel.GetTypeInfo(node).Type) .OfType() .Where(namespaceSymbol => namespaceSymbol.ContainingAssembly != null && namespaceSymbol.ContainingAssembly.Name == _config.CSharpOptions.AssemblyName) .FirstOrDefault(symbol => symbol.Name == namespaceName); var namespaceSymbol = symbol == null ? null : (symbol.IsNamespace ? symbol : symbol.ContainingNamespace); if (namespaceSymbol == null || !_symbolAnalysis.TypeHasMemberUsedAsValue(namespaceSymbol)) { continue; } WriteLine($"-- using {namespaceName};"); foreach (var csharpFilePath in declaringFiles) { WriteLine($"require({GetRequirePath(csharpFilePath)})"); } WriteLine(); } } private void WriteFooter() { if (!_tree.FilePath.EndsWith(".client.cs") && !_tree.FilePath.EndsWith(".server.cs")) { WriteLine("return {}"); } } private string GetLuaRuntimeLibPath() { if (_rojoProject == null) { return $"\"{Utility.LuaRuntimeModuleName}\""; } else { return RojoReader.ResolveInstancePath(_rojoProject, $"include/{Utility.LuaRuntimeModuleName}.lua")!; } } private string GetRequirePath(string longCSharpFilePath) { var inputDirectory = _inputDirectory.EndsWith('/') ? _inputDirectory : _inputDirectory + '/'; var csharpFilePath = longCSharpFilePath.Replace(inputDirectory, "").Replace(_config.SourceFolder, _config.OutputFolder); if (csharpFilePath.StartsWith('/')) { csharpFilePath = csharpFilePath.Substring(1); } if (_rojoProject == null) { return "\"./" + csharpFilePath.Replace(".cs", "") + '"'; } else { return RojoReader.ResolveInstancePath(_rojoProject, csharpFilePath)!; } } private Dictionary> GetNamespaceFilePaths() { var globalNamespaceSymbols = _tree.GetRoot() .DescendantNodes() .Select(node => _semanticModel.GetTypeInfo(node).Type) .OfType() .Where(namespaceSymbol => namespaceSymbol.ContainingAssembly != null && namespaceSymbol.ContainingAssembly.Name == _config.CSharpOptions.AssemblyName); return new Dictionary>( Utility.FilterDuplicates(globalNamespaceSymbols, SymbolEqualityComparer.Default) .OfType() .Select(namespaceSymbol => KeyValuePair.Create(namespaceSymbol.Name, GetPathsForSymbolDeclarations(namespaceSymbol))) .Where(pair => !string.IsNullOrEmpty(pair.Key) && !pair.Value.Contains(_tree.FilePath)) ); } private HashSet GetPathsForSymbolDeclarations(ISymbol symbol) { var filePaths = new HashSet(); foreach (var syntaxReference in symbol.DeclaringSyntaxReferences) { var syntaxTree = syntaxReference.SyntaxTree; var filePath = syntaxTree.FilePath; if (!string.IsNullOrEmpty(filePath)) { filePaths.Add(filePath); } } return filePaths; } public override void VisitRefExpression(RefExpressionSyntax node) { Logger.UnsupportedError(node, "Refs"); } public override void VisitUnsafeStatement(UnsafeStatementSyntax node) { Logger.UnsupportedError(node, "Unsafe contexts"); } public override void VisitUsingStatement(UsingStatementSyntax node) { Logger.UnsupportedError(node, "Using statements"); } public override void VisitAttribute(AttributeSyntax node) { if (GetName(node) == "Native") { WriteLine("@native"); } } public override void VisitAttributeList(AttributeListSyntax node) { base.VisitAttributeList(node); } public override void VisitLocalFunctionStatement(LocalFunctionStatementSyntax node) { foreach (var attributeList in node.AttributeLists) { Visit(attributeList); } Write($"local function {GetName(node)}"); Visit(node.ParameterList); _indent++; Visit(node.Body); WriteDefaultReturn(node.Body); _indent--; WriteLine("end"); } public override void VisitCastExpression(CastExpressionSyntax node) { // ignore Visit(node.Expression); } public override void VisitThrowStatement(ThrowStatementSyntax node) { Throw(node.Expression); } public override void VisitThrowExpression(ThrowExpressionSyntax node) { Throw(node.Expression); } private void Throw(ExpressionSyntax? exception) { if (exception == null) { WriteLine("_catch_rethrow()"); } else { Visit(exception); Write(":Throw("); Write((exception.Parent?.Parent is TryStatementSyntax).ToString().ToLower()); WriteLine(')'); } } public override void VisitTryStatement(TryStatementSyntax node) { WriteLine("CS.try(function()"); _indent++; Visit(node.Block); _indent--; Write("end, "); if (node.Finally != null) { WriteLine("function()"); _indent++; Visit(node.Finally); _indent--; Write("end"); } else { Write("nil"); } WriteLine(", {"); _indent++; foreach (var catchBlock in node.Catches) { WriteLine('{'); _indent++; Write("exceptionClass = "); if (catchBlock.Declaration != null) { Write('"'); Write(catchBlock.Declaration.Type.ToString()); Write('"'); WriteLine(", "); } Write("block = function("); Write(catchBlock.Declaration != null ? (GetName(catchBlock.Declaration) + ": CS.Exception") : "_"); WriteLine(", _catch_rethrow: () -> nil)"); _indent++; Visit(catchBlock.Block); _indent--; WriteLine("end"); _indent--; WriteLine('}'); } _indent--; WriteLine("})"); } public override void VisitForEachVariableStatement(ForEachVariableStatementSyntax node) { Visit(node.Variable); } public override void VisitForEachStatement(ForEachStatementSyntax node) { Write("for _, "); Write(GetName(node)); Write(" in "); Visit(node.Expression); WriteLine(" do"); _indent++; Visit(node.Statement); _indent--; WriteLine("end"); } public override void VisitForStatement(ForStatementSyntax node) { var hasDeclaration = node.Declaration != null; if (hasDeclaration) { WriteLine("do"); _indent++; Visit(node.Declaration); } foreach (var initializer in node.Initializers) { Visit(initializer); } Write("while "); if (node.Condition != null) { Visit(node.Condition); } else { Write("true"); } WriteLine(" do"); _indent++; Visit(node.Statement); foreach (var incrementor in node.Incrementors) { Visit(incrementor); } _indent--; WriteLine("end"); if (hasDeclaration) { _indent--; WriteLine("end"); } } public override void VisitLabeledStatement(LabeledStatementSyntax node) { Logger.UnsupportedError(node, "Labels & goto statements"); } public override void VisitGotoStatement(GotoStatementSyntax node) { Logger.UnsupportedError(node, "Labels & goto statements"); } public override void VisitDoStatement(DoStatementSyntax node) { WriteLine("repeat"); _indent++; Visit(node.Statement); _indent--; Write("until "); Visit(node.Condition); WriteLine(); } public override void VisitWhileStatement(WhileStatementSyntax node) { Write("while "); Visit(node.Condition); WriteLine(" do"); _indent++; Visit(node.Statement); _indent--; WriteLine("end"); } public override void VisitIfStatement(IfStatementSyntax node) { Write("if "); Visit(node.Condition); WriteLine(" then"); _indent++; WritePatternDeclarations(node.Condition); Visit(node.Statement); _indent--; if (node.Else != null) { var isElseIf = node.Else.Statement.IsKind(SyntaxKind.IfStatement); Write("else"); if (!isElseIf) { WriteLine(); _indent++; } Visit(node.Else); if (!isElseIf) { _indent--; WriteLine("end"); } } else { WriteLine("end"); } } public override void VisitSwitchStatement(SwitchStatementSyntax node) { var condition = node.Expression; WriteLine("repeat"); _indent++; var checkNoFallthrough = (StatementSyntax statement) => !statement.IsKind(SyntaxKind.BreakStatement) && !statement.IsKind(SyntaxKind.ReturnStatement) && !statement.DescendantNodes().All(descendant => !descendant.IsKind(SyntaxKind.BreakStatement) && !descendant.IsKind(SyntaxKind.ReturnStatement)); if (node.Sections.Count > 0 && !node.Sections.Any(section => section.Statements.Any(checkNoFallthrough))) // TODO: check for break/return { WriteLine("local _fallthrough = false"); } foreach (var section in node.Sections) { var statementKinds = section.Statements.Select(stmt => stmt.Kind()); HashSet supportedPatterns = [ SyntaxKind.VarPattern, SyntaxKind.DeclarationPattern ]; var caseLabels = section.Labels.Where(label => !label.IsKind(SyntaxKind.DefaultSwitchLabel) && !(label is CasePatternSwitchLabelSyntax casePattern && supportedPatterns.Contains(casePattern.Pattern.Kind())) ); foreach (var label in caseLabels) { Write("if "); if (label != caseLabels.FirstOrDefault()) { Write("_fallthrough or "); } var expressions = label.ChildNodes(); var expression = expressions.First(); Write('('); Visit(condition); var op = expression.IsKind(SyntaxKind.NotPattern) ? "~=" : expression is RelationalPatternSyntax relationalPattern ? Utility.GetMappedOperator(relationalPattern.OperatorToken.Text) : "=="; Write($" {op} "); Visit(expression); if (label is CasePatternSwitchLabelSyntax casePattern && casePattern.WhenClause != null) { Write(" and "); Visit(casePattern.WhenClause.Condition); } WriteLine(") then"); _indent++; if (label != caseLabels.Last()) { WriteLine("_fallthrough = true"); } else { foreach (var statement in section.Statements) { Visit(statement); if (statement == section.Statements.Last() && !statement.IsKind(SyntaxKind.ReturnStatement)) { WriteLine("break"); } } } _indent--; WriteLine("end"); } } void visitDefaultStatements(SwitchLabelSyntax defaultLabel) { var section = (SwitchSectionSyntax)defaultLabel.Parent!; foreach (var statement in section.Statements) { Visit(statement); } } var casePatternLabels = node.Sections.SelectMany(section => section.Labels.Where(label => label.IsKind(SyntaxKind.CasePatternSwitchLabel))); foreach (var casePatternLabel in casePatternLabels) { var pattern = casePatternLabel.ChildNodes().FirstOrDefault(); if (pattern.IsKind(SyntaxKind.VarPattern) || pattern.IsKind(SyntaxKind.DeclarationPattern)) { Visit(pattern); Write(" = "); Visit(condition); WriteLine(); visitDefaultStatements(casePatternLabel); } } var defaultLabel = node.Sections.SelectMany(section => section.Labels.Where(label => label.IsKind(SyntaxKind.DefaultSwitchLabel))).FirstOrDefault(); if (defaultLabel != null) { visitDefaultStatements(defaultLabel); } _indent--; WriteLine("until true"); } public override void VisitParenthesizedLambdaExpression(ParenthesizedLambdaExpressionSyntax node) { Write("function"); Visit(node.ParameterList); WriteLine(); _indent++; if (node.Block != null || node.ExpressionBody.IsKind(SyntaxKind.SimpleAssignmentExpression)) { Visit(node.Body); } else { Write("return "); Visit(node.ExpressionBody); WriteLine(); } _indent--; Write("end"); } public override void VisitSimpleLambdaExpression(SimpleLambdaExpressionSyntax node) { Write("function("); Visit(node.Parameter); Write(')'); WriteLine(); _indent++; if (node.Block != null || node.ExpressionBody.IsKind(SyntaxKind.SimpleAssignmentExpression)) { Visit(node.Body); } else { Write("return "); Visit(node.ExpressionBody); WriteLine(); } _indent--; Write("end"); } public override void VisitBracketedArgumentList(BracketedArgumentListSyntax node) { Write('['); if (node.Arguments.Count > 1) { Logger.CodegenError(node.Arguments.Last(), "Cannot have more than one argument between brackets."); } foreach (var argument in node.Arguments) { if (argument.Expression is LiteralExpressionSyntax numericLiteral && numericLiteral.IsKind(SyntaxKind.NumericLiteralExpression)) { int.TryParse(numericLiteral.Token.ValueText, out var indexValue); Write((indexValue + 1).ToString()); } else { Visit(argument); var typeSymbol = _semanticModel.GetTypeInfo(argument.Expression).Type; if (typeSymbol != null && Constants.INTEGER_TYPES.Contains(typeSymbol.Name)) { Write(" + 1"); } } } Write(']'); } public override void VisitConditionalAccessExpression(ConditionalAccessExpressionSyntax node) { Write("if "); Visit(node.Expression); Write(" == nil then nil else "); Visit(node.WhenNotNull); } public override void VisitConditionalExpression(ConditionalExpressionSyntax node) { Write("if "); Visit(node.Condition); Write(" then "); Visit(node.WhenTrue); Write(" else "); Visit(node.WhenFalse); } public override void VisitParenthesizedExpression(ParenthesizedExpressionSyntax node) { Write('('); Visit(node.Expression); Write(')'); } public override void VisitPostfixUnaryExpression(PostfixUnaryExpressionSyntax node) { Visit(node.Operand); if (node.IsKind(SyntaxKind.SuppressNullableWarningExpression)) { var typeSymbol = _semanticModel.GetTypeInfo(node.Operand).Type; if (typeSymbol == null) { Write(" :: any"); } return; } var mappedOperator = Utility.GetMappedOperator(node.OperatorToken.Text); WriteLine($" {mappedOperator} 1"); } public override void VisitPrefixUnaryExpression(PrefixUnaryExpressionSyntax node) { var mappedOperator = Utility.GetMappedOperator(node.OperatorToken.Text); var bit32Method = Utility.GetBit32MethodName(mappedOperator); if (bit32Method != mappedOperator) { Write($"bit32.{bit32Method}("); Visit(node.Operand); Write(")"); return; } Write(mappedOperator); Visit(node.Operand); } public override void VisitBinaryExpression(BinaryExpressionSyntax node) { var operatorText = node.OperatorToken.Text; var leftType = _semanticModel.GetTypeInfo(node.Left).Type; var rightType = _semanticModel.GetTypeInfo(node.Right).Type; var perTypeOperators = Constants.PER_TYPE_BINARY_OPERATOR_MAP; if ( (leftType != null && perTypeOperators.Any(pair => pair.Key.Contains(leftType.Name))) || (rightType != null && perTypeOperators.Any(pair => pair.Key.Contains(rightType.Name))) ) { var typeList = perTypeOperators.FirstOrDefault(pair => pair.Key.Contains(leftType!.Name) || pair.Key.Contains(rightType!.Name)).Key; var operatorMap = perTypeOperators[typeList]; if (operatorText == operatorMap.Item1) { operatorText = operatorMap.Item2; } } if (Constants.IGNORED_BINARY_OPERATORS.Contains(operatorText)) { Visit(node.Left); return; } var mappedOperator = Utility.GetMappedOperator(operatorText); switch (mappedOperator) { case "??": Write("if "); Visit(node.Left); Write(" == nil then "); Visit(node.Right); Write(" else "); Visit(node.Left); return; } var bit32Method = Utility.GetBit32MethodName(mappedOperator); if (bit32Method != mappedOperator) { Write($"bit32.{bit32Method}("); Visit(node.Left); Write(", "); Visit(node.Right); Write(")"); if ((leftType == null || rightType == null) || (!Constants.UNSUPPORTED_BITWISE_TYPES.Contains(leftType.Name) && !Constants.UNSUPPORTED_BITWISE_TYPES.Contains(rightType.Name))) return; Logger.CodegenWarning(node, "Using 128/64 bit integers when performing binary operations on Luau may result in undefined behaviour."); WriteLine(); Write("--> rbxcsc warn: long/Int64 or Int128/UInt128 bit operations may lead to undefined behaviour (above this warning)."); return; } Visit(node.Left); Write($" {mappedOperator} "); Visit(node.Right); } public override void VisitTupleExpression(TupleExpressionSyntax node) { WriteListTable(node.Arguments.Select(arg => arg.Expression).ToList()); } public override void VisitCollectionExpression(CollectionExpressionSyntax node) { Write('{'); foreach (var element in node.Elements) { var children = element.ChildNodes(); foreach (var child in children) { Visit(child); } if (element != node.Elements.Last()) { Write(", "); } } Write('}'); } public override void VisitArrayCreationExpression(ArrayCreationExpressionSyntax node) { Visit(node.Initializer); } public override void VisitImplicitArrayCreationExpression(ImplicitArrayCreationExpressionSyntax node) { Visit(node.Initializer); } public override void VisitInitializerExpression(InitializerExpressionSyntax node) { WriteListTable(node.Expressions.ToList()); } public override void VisitAnonymousObjectMemberDeclarator(AnonymousObjectMemberDeclaratorSyntax node) { if (node.NameEquals != null) { Write(GetName(node.NameEquals)); Write(" = "); } Visit(node.Expression); } public override void VisitAnonymousObjectCreationExpression(AnonymousObjectCreationExpressionSyntax node) { WriteLine('{'); _indent++; foreach (var initializer in node.Initializers) { Visit(initializer); if (initializer != node.Initializers.Last()) { Write(", "); } WriteLine(); } _indent--; WriteLine('}'); } public override void VisitObjectCreationExpression(ObjectCreationExpressionSyntax node) { Visit(node.Type); Write(".new"); Visit(node.ArgumentList); } public override void VisitUsingDirective(UsingDirectiveSyntax node) { // do nothing (for now) } public override void VisitContinueStatement(ContinueStatementSyntax node) { Write("continue"); } public override void VisitReturnStatement(ReturnStatementSyntax node) { Write("return "); Visit(node.Expression); WriteLine(); } public override void VisitExpressionStatement(ExpressionStatementSyntax node) { base.VisitExpressionStatement(node); WriteLine(); } public override void VisitArgumentList(ArgumentListSyntax node) { Write('('); foreach (var argument in node.Arguments) { Visit(argument); if (argument != node.Arguments.Last()) { Write(", "); } } Write(')'); } public override void VisitInvocationExpression(InvocationExpressionSyntax node) { if (node.Expression is MemberAccessExpressionSyntax memberAccess) { var objectType = _semanticModel.GetTypeInfo(memberAccess.Expression).Type; var name = GetName(memberAccess.Name); switch (name) { case "ToString": Write("tostring("); Visit(memberAccess.Expression); Write(')'); return; case "Equals": Visit(memberAccess.Expression); Write(" == "); var value = node.ArgumentList.Arguments.FirstOrDefault(); if (value != null) { Visit(value); } else { Write("nil"); } return; case "Length": if (objectType == null || !Constants.LENGTH_READABLE_TYPES.Contains(objectType.Name)) return; Write('#'); Visit(memberAccess.Expression); return; case "ToLower": case "ToUpper": case "Replace": case "Split": if (objectType == null || objectType.Name.ToLower() != "string") return; Write('('); Visit(memberAccess.Expression); Write(')'); Write(':'); var methodName = GetName(memberAccess.Name); var mappedMethodName = Constants.MAPPED_STRING_METHODS.GetValueOrDefault(methodName, methodName); Write(mappedMethodName); Visit(node.ArgumentList); return; case "Create": { if (objectType == null || objectType.Name != "Instance") break; var symbol = _semanticModel.GetSymbolInfo(node).Symbol; if (symbol is IMethodSymbol methodSymbol) { if (!methodSymbol.IsGenericMethod) Logger.CompilerError("Attempt to macro Instance.Create() but it is not generic"); var arguments = node.ArgumentList.Arguments; var instanceType = methodSymbol.TypeArguments.First(); Visit(memberAccess.Expression); Write($".new(\"{instanceType.Name}\""); if (arguments.Count > 0) { Write(", "); foreach (var argument in arguments) { Visit(argument.Expression); if (argument != arguments.Last()) { Write(", "); } } } Write(')'); return; } break; } case "GetService": case "FindFirstChildOfClass": case "FindFirstChildWhichIsA": case "FindFirstAncestorOfClass": case "FindFirstAncestorWhichIsA": case "IsA": { if (objectType == null) return; var superclasses = objectType.AllInterfaces.ToList(); if (objectType.Name != "Instance" && !superclasses.Select(@interface => @interface.Name).Contains("Instance")) return; var symbol = _semanticModel.GetSymbolInfo(node).Symbol; var methodSymbol = (IMethodSymbol)symbol!; if (!methodSymbol.IsGenericMethod) Logger.CompilerError($"Attempt to macro {objectType.Name}.{name}() but it is not generic"); var arguments = node.ArgumentList.Arguments; var instanceType = methodSymbol.TypeArguments.First(); Visit(memberAccess.Expression); Write($":{name}(\"{instanceType.Name}\""); if (arguments.Count > 0) { Write(", "); foreach (var argument in arguments) { Visit(argument.Expression); if (argument != arguments.Last()) { Write(", "); } } } else { Write(')'); } return; } } } else if (node.Expression is IdentifierNameSyntax || node.Expression is GenericNameSyntax) { var name = GetName(node.Expression); switch (name) { case "TypeOf": Write("typeof"); Visit(node.ArgumentList); return; case "ToNumber": case "ToFloat": case "ToDouble": case "ToInt": case "ToUInt": case "ToShort": case "ToUShort": case "ToByte": case "ToSByte": Write("tonumber"); Visit(node.ArgumentList); return; case "nameof": Write('"'); Write(_semanticModel.GetConstantValue(node).Value?.ToString() ?? $"??? nameof({node}) ???"); Write('"'); return; } } Visit(node.Expression); Visit(node.ArgumentList); } public override void VisitThisExpression(ThisExpressionSyntax node) { Write("self"); } public override void VisitMemberAccessExpression(MemberAccessExpressionSyntax node) { var leftIsLiteral = node.Expression is LiteralExpressionSyntax; var objectSymbol = _semanticModel.GetSymbolInfo(node.Expression).Symbol?.OriginalDefinition; var objectType = _semanticModel.GetTypeInfo(node.Expression).Type; var nameType = _semanticModel.GetSymbolInfo(node.Name).Symbol?.OriginalDefinition; var operatorText = '.'; if (objectSymbol != null) { if (objectType is INamedTypeSymbol objectDefinitionSymbol && (objectDefinitionSymbol.Name == "Services" || objectDefinitionSymbol.AllInterfaces.Select(@interface => @interface.Name).Contains("Services"))) { Write("game:GetService(\""); Visit(node.Name); Write("\")"); return; } if (nameType is IMethodSymbol methodSymbol) { operatorText = methodSymbol.IsStatic ? '.' : ':'; } } var usings = GetUsings(); var containingNamespace = objectSymbol?.ContainingNamespace; if (objectSymbol != null && (objectSymbol.Kind == SymbolKind.Namespace || (objectSymbol.Kind == SymbolKind.NamedType && objectSymbol.IsStatic))) { var namespaceName = objectSymbol.ToDisplayString(); var filePathsContainingType = objectSymbol.Locations .Where(location => location.SourceTree != null && location.SourceTree.FilePath != _tree.FilePath) .Select(location => location.SourceTree!.FilePath); var isNoFullQualificationType = Constants.NO_FULL_QUALIFICATION_TYPES.Contains(namespaceName) || (containingNamespace != null ? Constants.NO_FULL_QUALIFICATION_TYPES.Contains(containingNamespace.Name) : false); var noFullQualification = isNoFullQualificationType && (objectType == null || !Constants.GLOBAL_LIBRARIES.Contains(objectType.Name)); var typeIsImported = usings.Any(usingDirective => usingDirective.Name != null && Utility.GetNamesFromNode(usingDirective).Any(name => namespaceName.StartsWith(name))); if (noFullQualification && namespaceName != "System") { if (node.Expression is IdentifierNameSyntax identifier) { // TODO: check parent classes of parent class var parentClass = FindFirstAncestor(node); var parentClassSymbol = parentClass != null ? _semanticModel.GetDeclaredSymbol(parentClass) : null; if (parentClass != null && SymbolEqualityComparer.Default.Equals(objectType, parentClassSymbol)) { Write("class"); } else { if (GetName(node.Name) == "Globals") return; Visit(node.Name); } } else if (node.Expression is GenericNameSyntax genericName) { Visit(genericName); } else if (node.Expression is MemberAccessExpressionSyntax memberAccess) { if (namespaceName == Utility.RuntimeAssemblyName && Utility.GetNamesFromNode(memberAccess.Expression).LastOrDefault() != "Globals") { Visit(memberAccess.Name); Write('.'); } Visit(node.Name); } else { throw new NotSupportedException("Unsupported node.Expression type for a NO_FULL_QUALIFICATION_TYPES member"); } return; } } if (objectSymbol?.OriginalDefinition is ILocalSymbol typeSymbol) { switch (typeSymbol.Type.Name) { case "ValueTuple": var name = GetName(node.Name); if (name.StartsWith("Item")) { var itemIndex = name.Split("Item").Last(); if (leftIsLiteral) { Write('('); } Visit(node.Expression); if (leftIsLiteral) { Write(')'); } Write($"[{itemIndex}]"); return; } break; } } if (leftIsLiteral) { Write('('); } Visit(node.Expression); if (leftIsLiteral) { Write(')'); } Write(operatorText); Visit(node.Name); } private List GetUsings() { var root = _tree.GetRoot(); var usings = root.DescendantNodes().OfType().ToList(); var compilationUnit = root as CompilationUnitSyntax; if (compilationUnit != null) { foreach (var usingDirective in compilationUnit.Usings) { usings.Add(usingDirective); } } return usings; } private void FullyQualifyMemberAccess(INamespaceSymbol? namespaceType, List usings) { if (namespaceType == null) return; var typeIsImported = usings.Any(usingDirective => namespaceType.ContainingNamespace != null && usingDirective.Name != null && Utility.GetNamesFromNode(usingDirective).Contains(namespaceType.ContainingNamespace.Name)); Write($"CS.getAssemblyType(\"{namespaceType.Name}\")."); _flags.ShouldCallGetAssemblyType = false; } public override void VisitIsPatternExpression(IsPatternExpressionSyntax node) { var objectType = _semanticModel.GetTypeInfo(node.Expression).Type; var superclasses = objectType == null ? [] : objectType.AllInterfaces.ToList(); var pattern = node.Pattern; void writeTypePattern(TypeSyntax type) { var typeSymbol = _semanticModel.GetTypeInfo(type).Type; var mappedType = Utility.GetMappedType(type.ToString()); HashSet valueTypes = [TypeKind.Class, TypeKind.Struct, TypeKind.Enum]; var isValueType = typeSymbol != null && valueTypes.Contains(typeSymbol.TypeKind); if (!isValueType) { Write('"'); } Write(mappedType); if (!isValueType) { Write('"'); } } (bool, Action) getPatternWriter() { if (pattern is TypePatternSyntax typePattern) { return (true, () => writeTypePattern(typePattern.Type)); } else if (pattern is DeclarationPatternSyntax declarationPattern) { return (true, () => writeTypePattern(declarationPattern.Type)); } else if (pattern is VarPatternSyntax varPattern) { return (true, () => Write("true")); } else { return (false, () => Visit(pattern)); } } var (willBeHandled, writePattern) = getPatternWriter(); if (!willBeHandled && pattern.IsKind(SyntaxKind.NotPattern)) { Write("not "); } Write("CS.is("); Visit(node.Expression); Write(", "); writePattern(); Write(')'); } public override void VisitDeclarationPattern(DeclarationPatternSyntax node) { Visit(node.Designation); WriteTypeAnnotation(node.Type); } public override void VisitDiscardDesignation(DiscardDesignationSyntax node) { // do nothing (discard) } public override void VisitSingleVariableDesignation(SingleVariableDesignationSyntax node) { Write($"local {GetName(node)}"); } public override void VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax node) { Visit(node.Declaration); } public override void VisitVariableDeclaration(VariableDeclarationSyntax node) { foreach (var declarator in node.Variables) { Visit(declarator); } } public override void VisitVariableDeclarator(VariableDeclaratorSyntax node) { var localDeclaration = FindFirstAncestor(node); var classDeclaration = FindFirstAncestor(node); if ( localDeclaration != null && classDeclaration != null && !IsDescendantOf(node) && !IsDescendantOf(node)) { var isStatic = HasSyntax(classDeclaration.Modifiers, SyntaxKind.StaticKeyword) || HasSyntax(localDeclaration.Modifiers, SyntaxKind.StaticKeyword); Write(isStatic ? "class" : "self"); Write("."); } else { Write("local "); } Write($"{GetName(node)}"); var parent = node.Parent; if (parent is VariableDeclarationSyntax declaration) { WriteTypeAnnotation(declaration.Type); } if (node.Initializer != null) { Write(" = "); Visit(node.Initializer); if (node.Initializer.Value.IsKind(SyntaxKind.IsPatternExpression)) { WriteLine(); } WritePatternDeclarations(node.Initializer.Value); } WriteLine(); } public override void VisitAssignmentExpression(AssignmentExpressionSyntax node) { Visit(node.Left); Write(" = "); Visit(node.Right); if (node.Right.IsKind(SyntaxKind.IsPatternExpression)) { WriteLine(); } WritePatternDeclarations(node.Right); } public override void VisitGenericName(GenericNameSyntax node) { WriteName(node, node.Identifier); } public override void VisitIdentifierName(IdentifierNameSyntax node) { WriteName(node, node.Identifier); } public override void VisitInterpolatedStringText(InterpolatedStringTextSyntax node) { Write(node.TextToken.Text); } public override void VisitInterpolation(InterpolationSyntax node) { Write('{'); Visit(node.Expression); Write('}'); } public override void VisitInterpolatedStringExpression(InterpolatedStringExpressionSyntax node) { Write('`'); foreach (var content in node.Contents) { Visit(content); } Write('`'); } public override void VisitLiteralExpression(LiteralExpressionSyntax node) { switch (node.Kind()) { case SyntaxKind.StringLiteralExpression: case SyntaxKind.Utf8StringLiteralExpression: case SyntaxKind.CharacterLiteralExpression: Write($"\"{node.Token.ValueText}\""); break; case SyntaxKind.TrueLiteralExpression: case SyntaxKind.FalseLiteralExpression: case SyntaxKind.NumericLiteralExpression: Write(node.Token.ValueText); break; case SyntaxKind.DefaultLiteralExpression: var typeSymbol = _semanticModel.GetTypeInfo(node).Type; if (typeSymbol == null) break; if (Constants.INTEGER_TYPES.Contains(typeSymbol.Name) || Constants.DECIMAL_TYPES.Contains(typeSymbol.Name)) { Write("0"); break; } switch (typeSymbol.Name) { case "char": case "Char": case "string": case "String": Write("\"\""); break; case "bool": case "Boolean": Write("false"); break; default: Write("nil"); break; } break; case SyntaxKind.NullLiteralExpression: Write("nil"); break; } base.VisitLiteralExpression(node); } public override void VisitParameter(ParameterSyntax node) { if (node.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.ParamsKeyword))) { Write("..."); if (node.Type != null) WriteTypeAnnotation(node.Type); return; } Write(GetName(node)); if (node.Type != null) { WriteTypeAnnotation(node.Type); } } public override void VisitParameterList(ParameterListSyntax node) { var callable = node.Parent; Write('('); { if (callable is MethodDeclarationSyntax method && !HasSyntax(method.Modifiers, SyntaxKind.StaticKeyword)) { Write('_'); if (node.Parameters.Count > 0) { Write(", "); } } } ParameterSyntax? vaargParameter = null; foreach (var parameter in node.Parameters) { Visit(parameter); if (parameter != node.Parameters.Last()) { Write(", "); } else { if (parameter.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.ParamsKeyword))) vaargParameter = parameter; } } Write(')'); switch (callable) { case ConstructorDeclarationSyntax constructor: WriteLine($": {GetName(constructor)}"); break; case MethodDeclarationSyntax method: WriteTypeAnnotation(method.ReturnType, true); break; case LocalFunctionStatementSyntax localFunction: WriteTypeAnnotation(localFunction.ReturnType, true); break; default: WriteLine(); break; } _indent++; if (vaargParameter != null) { WriteLine("--> rbxcsc: vararg parameter conversion"); Write($"local {GetName(vaargParameter)} = {{ ... }}"); WriteLine(); } foreach (var parameter in node.Parameters) { if (parameter.Default == null) continue; var name = GetName(parameter); Write(name); Write(" = "); Write($"if {name} == nil then "); Visit(parameter.Default); WriteLine($" else {name}"); } _indent--; } public override void VisitNamespaceDeclaration(NamespaceDeclarationSyntax node) { var isWithinNamespace = IsDescendantOf(node); var allNames = Utility.GetNamesFromNode(node); var firstName = allNames.First(); allNames.Remove(firstName); var nativeAttribute = _config.EmitNativeAttributeOnClassOrNamespaceCallbacks ? "@native " : ""; WriteLine($"{(isWithinNamespace ? "namespace:" : "CS.")}namespace(\"{firstName}\", {nativeAttribute}function(namespace: CS.Namespace)"); _indent++; if (allNames.Count > 0) { foreach (var name in allNames) { WriteLine($"namespace:namespace(\"{name}\", {nativeAttribute}function(namespace: CS.Namespace)"); _indent++; foreach (var member in node.Members) { Visit(member); } _indent--; WriteLine("end)"); } } else { foreach (var member in node.Members) { Visit(member); } } _indent--; WriteLine("end)"); WriteLine(); } public override void VisitEnumDeclaration(EnumDeclarationSyntax node) { var isWithinNamespace = IsDescendantOf(node); Write($"CS.enum(\"{GetName(node)}\", {{"); if (node.Members.Count > 0) { WriteLine(); _indent++; var firstValue = node.Members.FirstOrDefault()?.EqualsValue?.Value; var lastIndex = firstValue != null ? (firstValue as LiteralExpressionSyntax)?.Token.Value as int? ?? -1 : -1; foreach (var enumMember in node.Members) { Write(GetName(enumMember)); Write(" = "); if (enumMember.EqualsValue != null) { Visit(enumMember.EqualsValue); lastIndex = (int)((LiteralExpressionSyntax)enumMember.EqualsValue.Value).Token.Value!; } else { var index = (enumMember.EqualsValue?.Value is LiteralExpressionSyntax literal ? literal.Token.Value as int? : null) ?? lastIndex + 1; lastIndex = index; Write(index.ToString()); } WriteLine(enumMember != node.Members.Last() ? ", " : ""); } _indent--; } WriteLine($"}}, {(isWithinNamespace ? "namespace" : "nil")})"); } public override void VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) { foreach (var member in node.Members) { if (HasSyntax(member.Modifiers, SyntaxKind.VirtualKeyword)) { Logger.UnsupportedError(node, "Virtual methods on interfaces"); } } } public override void VisitClassDeclaration(ClassDeclarationSyntax node) { if (HasSyntax(node.Modifiers, SyntaxKind.AbstractKeyword)) { Logger.UnsupportedError(node, "Abstract classes"); return; } if (HasSyntax(node.Modifiers, SyntaxKind.PartialKeyword)) { Logger.UnsupportedError(node, "Partial classes"); return; } var isWithinNamespace = IsDescendantOf(node); var isStatic = HasSyntax(node.Modifiers, SyntaxKind.StaticKeyword); var className = GetName(node); var nativeAttribute = _config.EmitNativeAttributeOnClassOrNamespaceCallbacks ? "@native " : ""; WriteLine($"{(isWithinNamespace ? "namespace:" : "CS.")}class(\"{className}\", {nativeAttribute}function(namespace: CS.Namespace)"); _indent++; Write($"local class = CS.classDef(\"{GetName(node)}\", "); Write(isWithinNamespace ? "namespace" : "nil"); // TODO: check if superclass or mixin var ancestorIdentifiers = (node.BaseList?.Types ?? []).Select(ancestor => { var typeSymbol = _semanticModel.GetTypeInfo(ancestor.Type).Type; return $"\"{Regex.Replace(typeSymbol?.ToString() ?? ancestor.Type.ToString(), @"<[^>]*>", "")}\""; }); if (ancestorIdentifiers.Count() > 0) { Write(", "); } Write(string.Join(", ", ancestorIdentifiers)); WriteLine(')'); WriteLine(); InitializeFields( node.Members .OfType() .Where(member => isStatic || HasSyntax(member.Modifiers, SyntaxKind.StaticKeyword)) ); InitializeProperties( node.Members .OfType() .Where(member => isStatic || HasSyntax(member.Modifiers, SyntaxKind.StaticKeyword)) ); var constructors = node.Members.OfType().ToList(); constructors.Sort((a, b) => a.ParameterList.Parameters.Count - b.ParameterList.Parameters.Count); var constructor = constructors.FirstOrDefault(); if (!isStatic) { if (constructor == null) { CreateDefaultConstructor(node); } else { VisitConstructorDeclaration(constructor); } } var methods = node.Members.OfType(); var staticMethods = methods.Where(method => HasSyntax(method.Modifiers, SyntaxKind.StaticKeyword)); foreach (var method in staticMethods) { Visit(method); } var isEntryPointClass = GetName(node) == _config.CSharpOptions.EntryPointName; if (isEntryPointClass) { var filePath = Path.TrimEndingDirectorySeparator(_tree.FilePath); var isClientFile = filePath.EndsWith(".client.cs"); if ((_flags.ClientEntryPointDefined && isClientFile) || (_flags.ServerEntryPointDefined && !isClientFile)) { Logger.CodegenError(node, $"No more than one main method can be defined on the {(isClientFile ? "client" : "server")}."); } if (isClientFile) { _flags.ClientEntryPointDefined = true; } else { _flags.ServerEntryPointDefined = true; } var mainMethod = methods.FirstOrDefault(method => GetName(method) == _config.CSharpOptions.MainMethodName); if (mainMethod == null) { Logger.CodegenError(node.Identifier, $"No main method \"{_config.CSharpOptions.MainMethodName}\" found in entry point class"); return; } if (!HasSyntax(mainMethod.Modifiers, SyntaxKind.StaticKeyword)) { Logger.CodegenError(node.Identifier, $"Main method must be static."); } WriteLine(); WriteLine("if namespace == nil then"); _indent++; WriteLine($"class.{_config.CSharpOptions.MainMethodName}()"); _indent--; WriteLine("else"); _indent++; WriteLine($"namespace[\"$onLoaded\"](namespace, class.{_config.CSharpOptions.MainMethodName})"); _indent--; Write("end"); } WriteLine(); WriteLine($"return class"); _indent--; WriteLine("end)"); } public override void VisitMethodDeclaration(MethodDeclarationSyntax node) { if (HasSyntax(node.Modifiers, SyntaxKind.ExternKeyword)) { Logger.UnsupportedError(node, "Extern methods", useYet: false); } foreach (var attributeList in node.AttributeLists) { Visit(attributeList); } var isStatic = HasSyntax(node.Modifiers, SyntaxKind.StaticKeyword); var isMetamethod = Constants.METAMETHODS.Contains(GetName(node)); var objectName = "self"; if (isStatic) { objectName = "class"; } else if (isMetamethod) { objectName = "mt"; } var name = GetName(node); Write($"function {objectName}.{name}"); Visit(node.ParameterList); _indent++; Visit(node.Body); WriteDefaultReturn(node.Body); _indent--; WriteLine("end"); } public override void VisitBaseExpression(BaseExpressionSyntax node) { Write("self[\"$superclass\"]"); } public override void VisitConstructorDeclaration(ConstructorDeclarationSyntax node) { // TODO: struct support? foreach (var attributeList in node.AttributeLists) { Visit(attributeList); } Write($"function class.new"); Visit(node.ParameterList); _indent++; VisitConstructorBody(FindFirstAncestor(node)!, node.Body, node.Initializer?.ArgumentList); _indent--; WriteLine("end"); } private void VisitConstructorBody(ClassDeclarationSyntax parentClass, BlockSyntax? block, ArgumentListSyntax? initializerArguments) { var isWithinNamespace = IsDescendantOf(parentClass); WriteLine("local mt = {}"); Write("local self = CS.classInstance(class, mt"); if (isWithinNamespace) { Write(", namespace"); } WriteLine(')'); // TODO: (when typedefs are generated for classes) $") :: {GetName(parentClass)}" WriteLine(); if (initializerArguments != null) { Write("self[\"$base\"]"); Visit(initializerArguments); } var isNotStatic = (MemberDeclarationSyntax member) => !HasSyntax(parentClass.Modifiers, SyntaxKind.StaticKeyword) && !HasSyntax(member.Modifiers, SyntaxKind.StaticKeyword); var isNotAbstract = (MemberDeclarationSyntax member) => !HasSyntax(member.Modifiers, SyntaxKind.AbstractKeyword); var nonStaticFields = parentClass.Members .Where(isNotStatic) .Where(isNotAbstract) .OfType(); var nonStaticProperties = parentClass.Members .Where(isNotStatic) .Where(isNotAbstract) .OfType(); var nonStaticMethods = parentClass.Members .Where(isNotStatic) .Where(isNotAbstract) .OfType(); InitializeFields(nonStaticFields); InitializeProperties(nonStaticProperties); if (block != null) { WriteLine(); Visit(block); } if (nonStaticMethods.Count() > 0) { WriteLine(); } foreach (var method in nonStaticMethods) { Visit(method); } WriteLine(); WriteLine("return self"); } private void CreateDefaultConstructor(ClassDeclarationSyntax node) { WriteLine($"function class.new()"); _indent++; VisitConstructorBody(node, null, null); _indent--; WriteLine("end"); } private void InitializeFields(IEnumerable fields) { foreach (var field in fields) { var classDeclaration = FindFirstAncestor(field)!; var isStatic = HasSyntax(classDeclaration.Modifiers, SyntaxKind.StaticKeyword) || HasSyntax(field.Modifiers, SyntaxKind.StaticKeyword); foreach (var declarator in field.Declaration.Variables) { if (declarator.Initializer == null) continue; Write($"{(isStatic ? "class" : "self")}.{GetName(declarator)} = "); Visit(declarator.Initializer); WriteLine(); } } } private void InitializeProperties(IEnumerable properties) { foreach (var property in properties) { if (property.Initializer == null) continue; var classDeclaration = FindFirstAncestor(property)!; var isStatic = HasSyntax(classDeclaration.Modifiers, SyntaxKind.StaticKeyword) || HasSyntax(property.Modifiers, SyntaxKind.StaticKeyword); Write($"{(isStatic ? "class" : "self")}.{GetName(property)} = "); Visit(property.Initializer); WriteLine(); } } private void WriteName(SyntaxNode node, SyntaxToken identifier) { var identifierText = identifier.ValueText; var originalIdentifierName = identifier.Text; if (identifierText == "var") return; if (Constants.LUAU_KEYWORDS.Contains(identifierText)) { Logger.CodegenError(node, $"Using reserved Luau keywords as identifier names is unsupported!"); } var isWithinClass = IsDescendantOf(node); var prefix = ""; if (isWithinClass) { // Check the fields in classes that this node is a descendant of for the identifier name var ancestorClasses = GetAncestors(node); for (int i = 0; i < ancestorClasses.Length; i++) { var ancestorClass = ancestorClasses[i]; var fields = ancestorClass.Members.OfType(); foreach (var field in fields) { var classDeclaration = FindFirstAncestor(field)!; var isStatic = HasSyntax(classDeclaration.Modifiers, SyntaxKind.StaticKeyword) || HasSyntax(field.Modifiers, SyntaxKind.StaticKeyword); foreach (var declarator in field.Declaration.Variables) { var name = GetName(declarator); if (name != identifierText) continue; prefix = (isStatic ? "class" : "self") + '.'; } } } } if (prefix == "") { var symbol = _semanticModel.GetSymbolInfo(node).Symbol; var parentNamespace = FindFirstAncestor(node); var parentNamespaceSymbol = parentNamespace != null ? _semanticModel.GetDeclaredSymbol(parentNamespace) : null; var pluginClassesNamespace = _runtimeLibNamespace.GetNamespaceMembers().FirstOrDefault(ns => ns.Name == "PluginClasses"); var runtimeNamespaceIncludesIdentifier = symbol != null ? ( IsDescendantOfNamespaceSymbol(symbol, _runtimeLibNamespace) || (pluginClassesNamespace != null && IsDescendantOfNamespaceSymbol(symbol, pluginClassesNamespace)) ) : false; HashSet fullyQualifiedParentKinds = [SyntaxKind.SimpleMemberAccessExpression, SyntaxKind.ObjectCreationExpression]; if ( symbol != null && symbol is ITypeSymbol typeSymbol && node.Parent != null && fullyQualifiedParentKinds.Contains(node.Parent.Kind()) && typeSymbol.ContainingNamespace != null && (parentNamespace != null ? Utility.GetNamesFromNode(parentNamespace.Name).LastOrDefault() != typeSymbol.ContainingNamespace.Name : true) && !Constants.NO_FULL_QUALIFICATION_TYPES.Contains(typeSymbol.ContainingNamespace.Name) ) { var usings = GetUsings(); FullyQualifyMemberAccess(typeSymbol.ContainingNamespace, usings); } var parentAccessExpression = FindFirstAncestor(node); var isLeftSide = parentAccessExpression == null ? true : node == parentAccessExpression.Expression; var parentBlocks = GetAncestors(node); var localScopeIncludesIdentifier = parentBlocks.Any(block => { var descendants = block.DescendantNodes(); var localFunctions = descendants.OfType(); var variableDesignations = descendants.OfType(); var variableDeclarators = descendants.OfType(); var forEachStatements = descendants.OfType(); var forStatements = descendants.OfType(); var parameters = descendants.OfType(); var catchDeclarations = descendants.OfType(); var checkNamePredicate = (SyntaxNode node) => TryGetName(node) == identifierText; return localFunctions.Any(checkNamePredicate) || variableDesignations.Any(checkNamePredicate) || variableDeclarators.Any(checkNamePredicate) || parameters.Any(checkNamePredicate) || catchDeclarations.Any(checkNamePredicate) || forEachStatements.Any(checkNamePredicate) || forStatements.Any(forStatement => forStatement.Initializers.Count() > 0); }); if (isLeftSide && !localScopeIncludesIdentifier && !runtimeNamespaceIncludesIdentifier) { var namespaceSymbol = parentNamespace != null ? _semanticModel.GetDeclaredSymbol(parentNamespace) : null; var namespaceIncludesIdentifier = namespaceSymbol != null && Utility.FindMember(namespaceSymbol, originalIdentifierName) != null; var parentClass = FindFirstAncestor(node); var classSymbol = parentClass != null ? _semanticModel.GetDeclaredSymbol(parentClass) : null; var classMemberSymbol = classSymbol != null ? Utility.FindMemberDeep(classSymbol, originalIdentifierName) : null; if (namespaceIncludesIdentifier) { Write($"namespace[\"$getMember\"](namespace, \"{identifierText}\")"); } else if (classMemberSymbol != null) { Write($"{(classMemberSymbol.IsStatic ? "class" : "self")}.{identifierText}"); } else { if (_flags.ShouldCallGetAssemblyType) { Write($"CS.getAssemblyType(\"{identifierText}\")"); } else { Write(identifierText); _flags.ShouldCallGetAssemblyType = true; } } } else { Write(identifierText); } } else { Write(prefix + identifierText); } } private void WriteListTable(List expressions) { Write('{'); foreach (var expression in expressions) { Visit(expression); if (expression != expressions.Last()) { Write(", "); } } Write('}'); } private void WriteDefaultReturn(BlockSyntax? block) { if (block != null && !block.Statements.Any(stmt => stmt.IsKind(SyntaxKind.ReturnStatement))) { WriteLine("return nil :: any"); } } private void WritePatternDeclarations(SyntaxNode node) { if (node is IsPatternExpressionSyntax isPattern) { void writeInitializer() { Write(" = "); Visit(isPattern.Expression); WriteLine(); } if (isPattern.Pattern is DeclarationPatternSyntax declarationPattern) { Visit(declarationPattern.Designation); writeInitializer(); } else if (isPattern.Pattern is VarPatternSyntax varPattern) { Visit(varPattern.Designation); writeInitializer(); } } } private void WriteTypeAnnotation(TypeSyntax type, bool isReturnType = false) { if (!type.IsVar) { var mappedType = Utility.GetMappedType(type.ToString()); Write($": {mappedType}"); if (isReturnType) { WriteLine(); } } } private void WriteRequire(string path) { WriteLine($"require({path})"); } private void WriteLine() { WriteLine(""); } private void WriteLine(char text) { WriteLine(text.ToString()); } private void WriteLine(string text) { if (text == null) { _output.AppendLine(); return; } WriteTab(); _output.AppendLine(text); } private void Write(char text) { Write(text.ToString()); } private void Write(string text) { WriteTab(); _output.Append(text); } private void WriteTab() { _output.Append(MatchLastCharacter('\n') ? GetTabString() : ""); } private string GetTabString() { return string.Concat(Enumerable.Repeat(" ", _indentSize * _indent)); } private void RemoveLastCharacters(int amount) { _output.Remove(_output.Length - amount, amount); } private bool HasSyntax(SyntaxTokenList tokens, SyntaxKind syntax) { return tokens.Any(token => token.IsKind(syntax)); } private bool IsDescendantOfNamespaceSymbol(ISymbol symbol, INamespaceSymbol ancestor) { var namespaceSymbol = symbol.ContainingNamespace; while (namespaceSymbol != null) { if (SymbolEqualityComparer.Default.Equals(namespaceSymbol, ancestor)) { return true; } namespaceSymbol = namespaceSymbol.ContainingNamespace; } return false; } private bool IsDescendantOf(SyntaxNode node) where T : SyntaxNode { return FindFirstAncestor(node) != null; } private T? FindFirstAncestor(SyntaxNode node) where T : SyntaxNode { return GetAncestors(node).FirstOrDefault(); } private T[] GetAncestors(SyntaxNode node) where T : SyntaxNode { return node.Ancestors().OfType().ToArray(); } private string? TryGetName(SyntaxNode node) { return Utility.GetNamesFromNode(node).FirstOrDefault(); } private string GetName(SyntaxNode node) { return Utility.GetNamesFromNode(node).First(); } private bool MatchLastCharacter(char character) { if (_output.Length == 0) return false; return _output[_output.Length - 1] == character; } } }