File: AbstractFileWriter.cs
Web Access
Project: src\src\Razor\src\Compiler\tools\RazorSyntaxGenerator\RazorSyntaxGenerator.csproj (dotnet-razorsyntaxgenerator)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
 
namespace RazorSyntaxGenerator;
 
internal abstract class AbstractFileWriter
{
    private readonly TextWriter _writer;
    private readonly Tree _tree;
    private readonly IDictionary<string, string> _parentMap;
    private readonly ILookup<string, string> _childMap;
    private readonly IDictionary<string, Node> _nodeMap;
    private readonly IDictionary<string, TreeType> _typeMap;
 
    private const int INDENT_SIZE = 4;
    private int _indentLevel;
    private bool _needIndent = true;
 
    protected AbstractFileWriter(TextWriter writer, Tree tree)
    {
        _writer = writer;
        _tree = tree;
        _nodeMap = tree.Types.OfType<Node>().ToDictionary(n => n.Name);
        _typeMap = tree.Types.ToDictionary(n => n.Name);
        _parentMap = tree.Types.ToDictionary(n => n.Name, n => n.Base);
        _parentMap.Add(tree.Root, null);
        _childMap = tree.Types.ToLookup(n => n.Base, n => n.Name);
    }
 
    protected IDictionary<string, string> ParentMap { get { return _parentMap; } }
    protected ILookup<string, string> ChildMap { get { return _childMap; } }
    protected Tree Tree { get { return _tree; } }
 
    #region Output helpers
 
    protected void IncreaseIndent()
    {
        _indentLevel++;
    }
 
    protected void DescreaseIndent()
    {
        if (_indentLevel <= 0)
        {
            throw new InvalidOperationException("Cannot unindent from base level");
        }
        _indentLevel--;
    }
 
    protected IndentScope Indent() => new(this);
 
    protected readonly ref struct IndentScope
    {
        private readonly AbstractFileWriter _writer;
 
        public IndentScope(AbstractFileWriter writer)
        {
            _writer = writer;
            _writer.IncreaseIndent();
        }
 
        public readonly void Dispose()
        {
            _writer.DescreaseIndent();
        }
    }
 
    protected void Write(string msg)
    {
        WriteIndentIfNeeded();
        _writer.Write(msg);
    }
 
    protected void Write(string msg, params object[] args)
    {
        WriteIndentIfNeeded();
        _writer.Write(msg, args);
    }
 
    protected void WriteLine()
    {
        WriteLine("");
    }
 
    protected void WriteIndentedLine(string msg)
    {
        using (Indent())
        {
            WriteLine(msg);
        }
    }
 
    protected void WriteIndentedLine(string msg, params object[] args)
    {
        using (Indent())
        {
            WriteLine(msg, args);
        }
    }
 
    protected void WriteLine(string msg)
    {
        if (msg.Length > 0)
        {
            // Don't write the indent if we're writing a blank line.
            WriteIndentIfNeeded();
        }
 
        _writer.WriteLine(msg);
        _needIndent = true; //need an indent after each line break
    }
 
    protected void WriteLine(string msg, params object[] args)
    {
        WriteIndentIfNeeded();
        _writer.WriteLine(msg, args);
        _needIndent = true; //need an indent after each line break
    }
 
    private void WriteIndentIfNeeded()
    {
        if (_needIndent)
        {
            _writer.Write(new string(' ', _indentLevel * INDENT_SIZE));
            _needIndent = false;
        }
    }
 
    /// <summary>
    ///  Writes all <paramref name="values"/> with each value separated by a comma.
    /// </summary>
    /// <remarks>
    ///  Values can be either <see cref="string"/>s or <see cref="IEnumerable{T}"/>s of
    ///  <see cref="string"/>.  All of these are flattened into a single sequence that is joined.
    ///  Empty strings are ignored.
    /// </remarks>
    protected void WriteCommaSeparatedList(params IEnumerable<object> values)
    {
        Write(CommaJoin(values));
    }
 
    /// <summary>
    /// Joins all the values together in <paramref name="values"/> into one string with each
    /// value separated by a comma.  Values can be either <see cref="string"/>s or <see
    /// cref="IEnumerable{T}"/>s of <see cref="string"/>.  All of these are flattened into a
    /// single sequence that is joined. Empty strings are ignored.
    /// </summary>
    protected static string CommaJoin(params IEnumerable<object> values)
        => Join(", ", values);
 
    protected static string Join(string separator, params IEnumerable<object> values)
        => string.Join(separator, values.SelectMany(v => (v switch
        {
            string s => [s],
            IEnumerable<string> ss => ss,
            _ => throw new InvalidOperationException("Join must be passed strings or collections of strings")
        }).Where(s => s != "")));
 
    protected void OpenBlock()
    {
        WriteLine("{");
        IncreaseIndent();
    }
 
    protected void CloseBlock(bool addSemicolon = false)
    {
        DescreaseIndent();
 
        if (addSemicolon)
        {
            WriteLine("};");
        }
        else
        {
            WriteLine("}");
        }
    }
 
    protected BlockScope Block(bool addSemicolon = false) => new(this, addSemicolon);
 
    protected readonly ref struct BlockScope
    {
        private readonly AbstractFileWriter _writer;
        private readonly bool _addSemicolon;
 
        public BlockScope(AbstractFileWriter writer, bool addSemicolon)
        {
            _writer = writer;
            _addSemicolon = addSemicolon;
            _writer.OpenBlock();
        }
 
        public readonly void Dispose()
        {
            _writer.CloseBlock(_addSemicolon);
        }
    }
 
    #endregion Output helpers
 
    #region Node helpers
 
    protected static string OverrideOrNewModifier(Field field)
    {
        return IsOverride(field) ? "override " : IsNew(field) ? "new " : "";
    }
 
    protected static bool CanBeField(Field field)
    {
        return field.Type != "SyntaxToken" && !IsAnyList(field.Type) && !IsOverride(field) && !IsNew(field);
    }
 
    protected static string GetFieldType(Field field, bool green)
    {
        if (IsAnyList(field.Type))
        {
            return green
                ? "GreenNode"
                : "SyntaxNode";
        }
 
        if (!green && field.Type == "SyntaxToken")
        {
            return "SyntaxNode";
        }
 
        return field.Type;
    }
 
    protected bool IsDerivedOrListOfDerived(string baseType, string derivedType)
    {
        return IsDerivedType(baseType, derivedType)
            || ((IsNodeList(derivedType) || IsSeparatedNodeList(derivedType))
                && IsDerivedType(baseType, GetElementType(derivedType)));
    }
 
    protected static bool IsSeparatedNodeList(string typeName)
    {
        return typeName.StartsWith("SeparatedSyntaxList<", StringComparison.Ordinal);
    }
 
    protected static bool IsNodeList(string typeName)
    {
        return typeName.StartsWith("SyntaxList<", StringComparison.Ordinal);
    }
 
    protected static bool IsAnyNodeList(string typeName)
    {
        return IsNodeList(typeName) || IsSeparatedNodeList(typeName);
    }
 
    protected bool IsNodeOrNodeList(string typeName)
    {
        return IsNode(typeName) || IsNodeList(typeName) || IsSeparatedNodeList(typeName) || typeName == "SyntaxNodeOrTokenList";
    }
 
    protected static string GetElementType(string typeName)
    {
        if (!typeName.Contains("<"))
        {
            return string.Empty;
        }
 
        var iStart = typeName.IndexOf('<');
        var iEnd = typeName.IndexOf('>', iStart + 1);
        if (iEnd < iStart)
        {
            return string.Empty;
        }
 
        var sub = typeName.Substring(iStart + 1, iEnd - iStart - 1);
        return sub;
    }
 
    protected static bool IsAnyList(string typeName)
    {
        return IsNodeList(typeName) || IsSeparatedNodeList(typeName) || typeName == "SyntaxNodeOrTokenList";
    }
 
    protected bool IsDerivedType(string typeName, string derivedTypeName)
    {
        if (typeName == derivedTypeName)
        {
            return true;
        }
 
        if (derivedTypeName != null && _parentMap.TryGetValue(derivedTypeName, out var baseType))
        {
            return IsDerivedType(typeName, baseType);
        }
        return false;
    }
 
    protected static bool IsRoot(Node n)
    {
        return n.Root != null && string.Equals(n.Root, "true", StringComparison.OrdinalIgnoreCase);
    }
 
    protected bool IsNode(string typeName)
    {
        return _parentMap.ContainsKey(typeName);
    }
 
    protected Node GetNode(string typeName)
        => _nodeMap.TryGetValue(typeName, out var node) ? node : null;
 
    protected TreeType GetTreeType(string typeName)
        => _typeMap.TryGetValue(typeName, out var node) ? node : null;
 
    protected static bool IsOptional(Field f)
    {
        return f.Optional != null && string.Equals(f.Optional, "true", StringComparison.OrdinalIgnoreCase);
    }
 
    protected static bool IsOverride(Field f)
    {
        return f.Override != null && string.Equals(f.Override, "true", StringComparison.OrdinalIgnoreCase);
    }
 
    protected static bool IsNew(Field f)
    {
        return f.New != null && string.Equals(f.New, "true", StringComparison.OrdinalIgnoreCase);
    }
 
    protected static bool HasErrors(Node n)
    {
        return n.Errors == null || string.Equals(n.Errors, "true", StringComparison.OrdinalIgnoreCase);
    }
 
    protected static string CamelCase(string name)
    {
        // Special logic to handle 'CSharp' correctly
        if (name.StartsWith("CSharp", StringComparison.OrdinalIgnoreCase))
        {
            name = "csharp" + name[6..];
        }
        else if (char.IsUpper(name[0]))
        {
            name = char.ToLowerInvariant(name[0]) + name[1..];
        }
 
        return FixKeyword(name);
    }
 
    protected static string FixKeyword(string name)
    {
        if (IsKeyword(name))
        {
            return "@" + name;
        }
        return name;
    }
 
    protected static string UnderscoreCamelCase(string name)
    {
        return "_" + CamelCase(name);
    }
 
    protected string StripNode(string name)
    {
        return (_tree.Root.EndsWith("Node", StringComparison.Ordinal)) ? _tree.Root.Substring(0, _tree.Root.Length - 4) : _tree.Root;
    }
 
    protected string StripRoot(string name)
    {
        var root = StripNode(_tree.Root);
        if (name.EndsWith(root, StringComparison.Ordinal))
        {
            return name.Substring(0, name.Length - root.Length);
        }
        return name;
    }
 
    protected static string StripPost(string name, string post)
    {
        return name.EndsWith(post, StringComparison.Ordinal)
            ? name.Substring(0, name.Length - post.Length)
            : name;
    }
 
    protected static bool IsKeyword(string name)
    {
        switch (name)
        {
            case "bool":
            case "byte":
            case "sbyte":
            case "short":
            case "ushort":
            case "int":
            case "uint":
            case "long":
            case "ulong":
            case "double":
            case "float":
            case "decimal":
            case "string":
            case "char":
            case "object":
            case "typeof":
            case "sizeof":
            case "null":
            case "true":
            case "false":
            case "if":
            case "else":
            case "while":
            case "for":
            case "foreach":
            case "do":
            case "switch":
            case "case":
            case "default":
            case "lock":
            case "try":
            case "throw":
            case "catch":
            case "finally":
            case "goto":
            case "break":
            case "continue":
            case "return":
            case "public":
            case "private":
            case "internal":
            case "protected":
            case "static":
            case "readonly":
            case "sealed":
            case "const":
            case "new":
            case "override":
            case "abstract":
            case "virtual":
            case "partial":
            case "ref":
            case "out":
            case "in":
            case "where":
            case "params":
            case "this":
            case "base":
            case "namespace":
            case "using":
            case "class":
            case "struct":
            case "interface":
            case "delegate":
            case "checked":
            case "get":
            case "set":
            case "add":
            case "remove":
            case "operator":
            case "implicit":
            case "explicit":
            case "fixed":
            case "extern":
            case "event":
            case "enum":
            case "unsafe":
                return true;
            default:
                return false;
        }
    }
 
    protected List<Kind> GetKindsOfFieldOrNearestParent(TreeType treeType, Field field)
    {
        while ((field.Kinds is null || field.Kinds.Count == 0) && IsOverride(field))
        {
            treeType = GetTreeType(treeType.Base);
            field = (treeType switch
            {
                Node node => node.Fields,
                AbstractNode abstractNode => abstractNode.Fields,
                _ => throw new InvalidOperationException("Unexpected node type.")
            }).Single(f => f.Name == field.Name);
        }
 
        return field.Kinds.Distinct().ToList();
    }
 
    #endregion Node helpers
}