File: Language\SplitDecision.cs
Web Access
Project: src\roslyn\src\Razor\src\Compiler\Microsoft.CodeAnalysis.Razor.Compiler\src\Microsoft.CodeAnalysis.Razor.Compiler.csproj (Microsoft.CodeAnalysis.Razor.Compiler)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using Microsoft.AspNetCore.Razor.Language.Intermediate;

namespace Microsoft.AspNetCore.Razor.Language;

/// <summary>
/// The outcome of analyzing a component's primary class body for the decl/impl markup split. One of
/// three cases:
/// <list type="bullet">
/// <item><see cref="NoSplit"/> -- no class-body markup, so the caller keeps the single-file behavior.</item>
/// <item><see cref="SplitPlan"/> -- the class body mixes markup and C# and can be split safely into a
/// markup-free decl half and a markup-bearing impl half; describes how the pieces route.</item>
/// <item><see cref="SplitFallback"/> -- the class body has markup but cannot be split safely (a markup
/// property, an unsupported node, a directive, or an unrecoverable parse), so the caller retains the
/// original class-body layout and can select its fallback pipeline.</item>
/// </list>
/// </summary>
/// <remarks>
/// This is a closed hierarchy, produced once per primary class and shared by both lowering phases. The
/// decision is a pure function of the class body's IR and the parse options, and it never branches on
/// the language version, so all callers reach the same decision for the same document. Split plans are
/// consumed by the lowering phases; an explicit fallback result lets pipeline callers preserve unsplit
/// processing for shapes that cannot be routed safely.
/// </remarks>
internal abstract class SplitDecision
{
    private protected SplitDecision()
    {
    }

    /// <summary>
    /// The class body has no markup (or nothing that needs splitting); the caller keeps the single-file
    /// behavior. Shared singleton.
    /// </summary>
    public static SplitDecision NoSplit { get; } = new NoSplitDecision();

    /// <summary>
    /// The class body has markup but cannot be split safely; the caller retains the original class-body
    /// layout and can select its fallback pipeline.
    /// </summary>
    public static SplitFallback Fallback(FallbackReason reason) => new(reason);

    /// <summary>True when this decision requires the caller to build separate decl and impl halves.</summary>
    public bool RequiresSplit => this is SplitPlan;

    /// <summary>
    /// True when the file has markup but cannot be split, so the caller must retain the original
    /// class-body layout instead of routing members between declaration and implementation output.
    /// </summary>
    public bool IsFallback => this is SplitFallback;

    private sealed class NoSplitDecision : SplitDecision
    {
    }

    /// <summary>
    /// The class body has markup that cannot be split safely, so its original layout must be retained.
    /// This preserves correctness while allowing the surrounding pipeline to choose a fallback path.
    /// </summary>
    public sealed class SplitFallback : SplitDecision
    {
        public SplitFallback(FallbackReason reason)
        {
            Reason = reason;
        }

        /// <summary>Why the file falls back instead of splitting (for diagnostics/telemetry and tests).</summary>
        public FallbackReason Reason { get; }
    }

    /// <summary>
    /// Describes how each class-body member routes between the decl and impl halves. Produced only when
    /// the class body mixes markup and C# and the file can be split safely.
    /// </summary>
    public sealed class SplitPlan : SplitDecision
    {
        public SplitPlan(ImmutableArray<RoutedMember> members)
        {
            Members = members.NullToEmpty();
        }

        /// <summary>The routed class-body members in original order; each drives what its half emits.</summary>
        public ImmutableArray<RoutedMember> Members { get; }
    }
}

/// <summary>
/// A user-authored class-body member after routing, already resolved into the IR pieces each half emits:
/// <see cref="DeclPieces"/> for the decl half and <see cref="ImplPieces"/> for the impl half. Original
/// nodes are shared by reference (keeping their source mappings). A member is either markup-free (all its
/// pieces stay in decl) or a markup-bearing method (all its pieces lift to impl); a markup property
/// never reaches routing because it produces a fallback decision. The lowering phases simply append the
/// pieces for their half.
/// </summary>
internal readonly struct RoutedMember
{
    public RoutedMember(
        ImmutableArray<IntermediateNode> declPieces,
        ImmutableArray<IntermediateNode> implPieces)
    {
        DeclPieces = declPieces.NullToEmpty();
        ImplPieces = implPieces.NullToEmpty();
    }

    /// <summary>The pieces this member contributes to the decl half, in order.</summary>
    public ImmutableArray<IntermediateNode> DeclPieces { get; }

    /// <summary>The pieces this member contributes to the impl half, in order.</summary>
    public ImmutableArray<IntermediateNode> ImplPieces { get; }
}

/// <summary>
/// Why a markup-bearing class body retains its original layout instead of being routed between
/// declaration and implementation output.
/// </summary>
internal enum FallbackReason
{
    /// <summary>
    /// A property/indexer carries markup. A property is tag-helper descriptor surface (a
    /// <c>[Parameter]</c> shapes the component's attributes), so it must stay in the decl half -- but
    /// markup cannot live in the markup-free decl half. Rather than reshape the property, the splitter
    /// reports fallback so the property remains in place.
    /// </summary>
    MarkupProperty,

    /// <summary>
    /// A non-method, non-property member carries markup -- a field/event (whose initializer runs in
    /// declaration order, which splitting across partials would perturb), a nested type, a
    /// constructor/operator, or an incomplete member. It cannot be safely lifted, so the splitter reports
    /// fallback.
    /// </summary>
    UnsupportedMarkupMember,

    /// <summary>
    /// The analysis parse is unrecoverable (brace mismatch, or a markup marker isn't contained by any
    /// member), so member boundaries can't be trusted. Not triggered by ordinary transient typos, which
    /// still recover member boundaries.
    /// </summary>
    UnrecoverableParse,

    /// <summary>
    /// The class body contains a node the splitter can't route -- neither raw C# nor a recognized markup
    /// node -- such as an <c>@inject</c> or another structured/extension member. Retaining the original
    /// layout avoids moving descriptor surface into the impl half.
    /// </summary>
    UnsupportedClassBodyNode,

    /// <summary>
    /// The class body contains a preprocessor directive (<c>#if</c>/<c>#endif</c>, <c>#region</c>,
    /// <c>#pragma</c>, <c>#nullable</c>). Splitting could route a member out of the directive's scope and
    /// orphan it in the other half; retaining the original layout keeps directives balanced.
    /// </summary>
    ClassBodyHasDirectives,
}