| File: Language\MarkupSplitter.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; using System.Collections.Immutable; using Microsoft.AspNetCore.Razor.Language.Extensions; using Microsoft.AspNetCore.Razor.Language.Intermediate; using Microsoft.AspNetCore.Razor.PooledObjects; namespace Microsoft.AspNetCore.Razor.Language; /// <summary> /// Decides, for a component's primary class body, which parts of the user's <c>@code</c> belong in /// the markup-free "decl" half (the tag-helper descriptor surface) and which markup-bearing parts /// belong in the "impl" half (lowered after tag-helper resolution). /// </summary> /// <remarks> /// <para> /// The <c>@code</c> contents arrive on the primary <see cref="ClassDeclarationIntermediateNode"/> as a /// flat sequence of raw C# text (<see cref="CSharpCodeIntermediateNode"/> holding /// <see cref="CSharpIntermediateToken"/>) interleaved with markup nodes. The vast majority of /// <c>@code</c> is pure C# with no markup, so a cheap structural gate (<see cref="HasClassBodyMarkup"/>) /// runs first: with no class-body markup there is nothing to route to the impl half, so it reports /// <see cref="SplitDecision.NoSplit"/> without parsing anything -- the whole class body stays in the /// decl half. /// </para> /// <para> /// The split decision is a pure function of the class body's IR content and the parse options; it does /// not branch on the language version, so every caller reaches the same decision for the same document. /// The markup-split phase computes it once, produces the decl half, and rewrites the working node into /// the impl half -- all before tag-helper resolution. /// </para> /// </remarks> internal static partial class MarkupSplitter { /// <summary> /// Identifier emitted into the throwaway analysis document to stand in for a markup transition, so /// the class body parses as ordinary C# without needing resolved tag helpers. It never appears in /// generated output. Markup is detected from the analysis document's per-child placements, never by /// matching this name (user code may legitimately contain a call of the same name). /// </summary> public const string MarkerMethodName = "__RazorMarkupTransition"; /// <summary> /// Computes the split decision for the given primary class body: gates on class-body markup, builds /// the analysis document, and classifies it. Pure and uncached. /// </summary> public static SplitDecision Split( ClassDeclarationIntermediateNode primaryClass, MethodDeclarationIntermediateNode renderMethod, RazorParserOptions parserOptions) { ArgHelper.ThrowIfNull(primaryClass); ArgHelper.ThrowIfNull(renderMethod); ArgHelper.ThrowIfNull(parserOptions); // Fast path: with no class-body markup there is nothing to move to the impl half. if (!HasClassBodyMarkup(primaryClass, renderMethod)) { return SplitDecision.NoSplit; } var children = CollectClassBodyChildren(primaryClass, renderMethod); var analysis = BuildAnalysisDocument(children); return ClassifyFromAnalysis(analysis, parserOptions.CSharpParseOptions); } /// <summary> /// The routable plan for the given classified class body, or <see langword="null"/> when the body /// keeps its unsplit shape. This is the <em>fallback</em> entry point: the primary decl/impl split /// runs early (before tag-helper resolution) over the raw <c>@code</c>, but a component whose raw /// shape the early analysis can't partition falls through to the late lowering phases, which route /// its markup here over the already-classified tree instead. /// </summary> public static SplitDecision.SplitPlan? GetRoutablePlan( ClassDeclarationIntermediateNode primaryClass, MethodDeclarationIntermediateNode renderMethod, RazorParserOptions parserOptions) => Split(primaryClass, renderMethod, parserOptions) as SplitDecision.SplitPlan; /// <summary> /// True if any line of the analysis text begins (after leading whitespace) with a preprocessor /// directive. A line-anchored scan avoids misfiring on a <c>#</c> inside a string or interpolation; /// a rare false positive only costs an unnecessary fallback, never a mis-split. /// </summary> internal static bool HasPreprocessorDirective(string text) { var atLineStart = true; foreach (var c in text) { if (c is '\n' or '\r') { atLineStart = true; } else if (!atLineStart) { continue; } else if (c == '#') { return true; } else if (!char.IsWhiteSpace(c)) { atLineStart = false; } } return false; } /// <summary> /// True if the primary class body contains a markup transition (a node that can only be lowered /// after tag-helper resolution). Runs in O(children) with no parsing. /// </summary> public static bool HasClassBodyMarkup( ClassDeclarationIntermediateNode primaryClass, MethodDeclarationIntermediateNode renderMethod) { foreach (var child in primaryClass.Children) { if (ReferenceEquals(child, renderMethod) || child.IsSynthesizedHelper) { continue; } if (IsClassBodyMarkup(child)) { return true; } } return false; } /// <summary> /// Classifies a class-body child as markup rather than C#. Defined as the complement of the known /// C#/structured-declaration node kinds so that an unrecognized (e.g. newly introduced) markup node /// is treated as markup: erring toward running the split machinery is a harmless cost, whereas /// missing a markup node would let it leak into the resolution-free decl half. /// </summary> /// <remarks> /// This is the deliberately over-eager <em>gate</em> classifier. It can flag a non-markup extension /// node (an <c>@inject</c>) as "markup"; that only causes <see cref="Split"/> to run, which then sees /// the node isn't a kind it can route (<see cref="IsSupportedClassBodyNode"/>) and falls back. Routing /// itself uses the precise allow-list <see cref="IsMarkupNode"/>, never this predicate. /// </remarks> internal static bool IsClassBodyMarkup(IntermediateNode node) => node is not (CSharpCodeIntermediateNode or FieldDeclarationIntermediateNode or PropertyDeclarationIntermediateNode or MethodDeclarationIntermediateNode); /// <summary> /// The precise allow-list of markup intermediate node kinds the splitter knows how to route to the /// impl half: an expression-position <see cref="TemplateIntermediateNode"/> (from <c>@<...></c>) /// and the statement-position markup nodes. The split runs before tag-helper resolution, so a /// statement-position element is still an <see cref="UnresolvedElementIntermediateNode"/> (it may bind /// to a component/tag helper or to plain HTML); either way it is markup and lifts to the impl half, /// where resolution runs. Unlike the fail-safe <see cref="IsClassBodyMarkup"/> gate, this is positive: /// a class-body node that is neither raw C# nor one of these kinds -- e.g. an <c>@inject</c> /// (<c>ComponentInjectIntermediateNode</c>, itself an <see cref="ExtensionIntermediateNode"/> just like /// <see cref="TemplateIntermediateNode"/>) or a structured member declaration -- is not treated as /// routable markup. /// </summary> internal static bool IsMarkupNode(IntermediateNode node) => node is TemplateIntermediateNode or MarkupElementIntermediateNode or UnresolvedElementIntermediateNode or MarkupBlockIntermediateNode or HtmlContentIntermediateNode; /// <summary> /// A class-body node the splitter can route: raw C# text (which stays in decl or lifts to impl with /// its member) or a recognized markup node (which lifts to impl). Any other kind -- a structured or /// extension member such as <c>@inject</c> -- means the file can't be split and must fall back. /// </summary> internal static bool IsSupportedClassBodyNode(IntermediateNode node) => node is CSharpCodeIntermediateNode || IsMarkupNode(node); /// <summary> /// The ordered user-authored class-body children -- everything that isn't the render method or a /// synthesized helper -- in source order. This is the flat sequence of raw C# chunks and markup /// transitions the analysis document and routing operate over. /// </summary> internal static ImmutableArray<IntermediateNode> CollectClassBodyChildren( ClassDeclarationIntermediateNode primaryClass, MethodDeclarationIntermediateNode renderMethod) { using var builder = new PooledArrayBuilder<IntermediateNode>(); foreach (var child in primaryClass.Children) { if (ReferenceEquals(child, renderMethod) || child.IsSynthesizedHelper) { continue; } builder.Add(child); } return builder.ToImmutableAndClear(); } }