Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,99 @@ protected override RazorCodeDocument ExecuteCore(RazorCodeDocument codeDocument,
throw new InvalidOperationException(message);
}

// Fork: when the decl phase produced its half, this phase produces the matching
// impl half -- a minimal partial class containing just the render method body and
// any compiler-synthesized plumbing. Otherwise (non-component, suppressed primary
// method body, malformed primary structure, or the decl phase didn't run for any
// other reason) fall through to the original single-file lowering path.
if (codeDocument.GetDeclCSharpDocument() is not null &&
TryWriteImplDocument(documentNode, codeDocument, cancellationToken, out var implDocument))
{
return codeDocument.WithCSharpDocument(implDocument);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm reviewing commit-at-a-time, and I'm 99% sure this will be fixed in a future commit, but just noting here that its super weird this says WithCSharpDocument when its one of two.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, this didn't get changed, so I guess I'll be explicit: Consider a future change here to change the API so its explicit that methods that deal with documents are specific to a decl or impl. ie, WithImplCSharpDocument etc.

}

var csharpDocument = RazorCSharpDocumentWriter.Write(documentNode, codeDocument, cancellationToken: cancellationToken);
return codeDocument.WithCSharpDocument(csharpDocument);
}

private static bool TryWriteImplDocument(
DocumentIntermediateNode documentNode,
RazorCodeDocument codeDocument,
CancellationToken cancellationToken,
out RazorCSharpDocument implDocument)
{
var primaryClass = documentNode.FindPrimaryClass();
var renderMethod = documentNode.FindPrimaryMethod();
var primaryNamespace = documentNode.FindPrimaryNamespace();

// Defensive: splittability was already verified by the decl phase before it
// produced a decl document, so this should not happen in practice. If a caller
// bypasses the decl phase or the IR shape changes between phases, fall through
// to the single-file path rather than crash.
if (primaryClass is null || renderMethod is null || primaryNamespace is null)
{
implDocument = null!;
return false;
}

// Build the impl synthetic tree: brand-new spine containing just the namespace,
// its using directives, and a partial class wrapping renderMethod plus any
// IsSynthesizedHelper nodes lifted from primaryClass / primaryNamespace. The
// namespace-level walk preserves original order relative to primaryClass so an
// attribute decoration that preceded the class in the original tree continues
// to do so. IsGenericTyped helper namespaces are also lifted as siblings of the
// primary namespace.
var usings = primaryNamespace.FindDescendantNodes<UsingDirectiveIntermediateNode>();
var implDocNode = RazorCSharpDocumentWriter.CloneContainer(documentNode);
var implNamespace = RazorCSharpDocumentWriter.CloneContainer(primaryNamespace);
var implClass = RazorCSharpDocumentWriter.CloneContainer(primaryClass);

implClass.Children.Add(renderMethod);
foreach (var classChild in primaryClass.Children)
{
if (classChild.IsSynthesizedHelper)
{
implClass.Children.Add(classChild);
}
}

foreach (var usingDirective in usings)
{
implNamespace.Children.Add(usingDirective);
}

foreach (var nsChild in primaryNamespace.Children)
{
if (nsChild == primaryClass)
{
implNamespace.Children.Add(implClass);
}
else if (nsChild.IsSynthesizedHelper)
{
implNamespace.Children.Add(nsChild);
}
}

implDocNode.Children.Add(implNamespace);

foreach (var docChild in documentNode.Children)
{
if (docChild is NamespaceDeclarationIntermediateNode { IsGenericTyped: true } genericNs)
{
implDocNode.Children.Add(genericNs);
}
}

// Seed the impl root with the full diagnostic set (deduped by checksum) so any
// diagnostics attached to documentNode / primaryNamespace / primaryClass themselves
// -- which aren't reachable from the synthetic clone -- still surface on the impl
// document.
foreach (var diagnostic in documentNode.GetAllDiagnostics())
{
implDocNode.AddDiagnostic(diagnostic);
}

implDocument = RazorCSharpDocumentWriter.Write(implDocNode, codeDocument, cancellationToken: cancellationToken);
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ protected override RazorCodeDocument ExecuteCore(RazorCodeDocument codeDocument,
// - renderMethod
// - any IsSynthesizedHelper node (compiler plumbing)
// - IsGenericTyped namespaces (type-inference helpers)
var declDocNode = CloneContainer(documentNode);
var declNamespace = CloneContainer(primaryNamespace);
var declClass = CloneContainer(primaryClass);
var declDocNode = RazorCSharpDocumentWriter.CloneContainer(documentNode);
var declNamespace = RazorCSharpDocumentWriter.CloneContainer(primaryNamespace);
var declClass = RazorCSharpDocumentWriter.CloneContainer(primaryClass);

foreach (var classChild in primaryClass.Children)
{
Expand Down Expand Up @@ -138,38 +138,4 @@ protected override RazorCodeDocument ExecuteCore(RazorCodeDocument codeDocument,

return codeDocument.WithDeclCSharpDocument(declDocument);
}

private static DocumentIntermediateNode CloneContainer(DocumentIntermediateNode node)
=> new()
{
DocumentKind = node.DocumentKind,
Options = node.Options,
Target = node.Target,
Source = node.Source,
IsImported = node.IsImported,
};

private static NamespaceDeclarationIntermediateNode CloneContainer(NamespaceDeclarationIntermediateNode node)
=> new()
{
Name = node.Name,
IsPrimaryNamespace = node.IsPrimaryNamespace,
IsGenericTyped = node.IsGenericTyped,
Source = node.Source,
IsImported = node.IsImported,
};

private static ClassDeclarationIntermediateNode CloneContainer(ClassDeclarationIntermediateNode node)
=> new()
{
Name = node.Name,
BaseType = node.BaseType,
Modifiers = node.Modifiers,
Interfaces = node.Interfaces,
TypeParameters = node.TypeParameters,
IsPrimaryClass = node.IsPrimaryClass,
NullableContext = node.NullableContext,
Source = node.Source,
IsImported = node.IsImported,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,48 @@ public static RazorCSharpDocument Write(
context.GetLinePragmas());
}

/// <summary>
/// Shallow-clones a <see cref="DocumentIntermediateNode"/>: copies its container-level
/// metadata (kind, options, target, source) into a new node with empty children. Used by
/// the decl and impl phases to build synthetic spines that share leaf nodes with the
/// original tree by reference rather than mutating it in place.
/// </summary>
public static DocumentIntermediateNode CloneContainer(DocumentIntermediateNode node)
=> new()
{
DocumentKind = node.DocumentKind,
Options = node.Options,
Target = node.Target,
Source = node.Source,
IsImported = node.IsImported,
};

/// <inheritdoc cref="CloneContainer(DocumentIntermediateNode)"/>
public static NamespaceDeclarationIntermediateNode CloneContainer(NamespaceDeclarationIntermediateNode node)
=> new()
{
Name = node.Name,
IsPrimaryNamespace = node.IsPrimaryNamespace,
IsGenericTyped = node.IsGenericTyped,
Source = node.Source,
IsImported = node.IsImported,
};

/// <inheritdoc cref="CloneContainer(DocumentIntermediateNode)"/>
public static ClassDeclarationIntermediateNode CloneContainer(ClassDeclarationIntermediateNode node)
=> new()
{
Name = node.Name,
BaseType = node.BaseType,
Modifiers = node.Modifiers,
Interfaces = node.Interfaces,
TypeParameters = node.TypeParameters,
IsPrimaryClass = node.IsPrimaryClass,
NullableContext = node.NullableContext,
Source = node.Source,
IsImported = node.IsImported,
};

private sealed class Visitor(
CodeRenderingContext context,
CodeTarget codeTarget,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ internal static string GetIdentifierFromPath(ReadOnlySpan<char> filePath)
return builder.ToString();
}

/// <summary>
/// Returns the hint name for the decl half of a Razor component generated source given
/// the impl half's hint name. The decl file substitutes <c>.decl.g.cs</c> for the
/// trailing <c>.g.cs</c> -- e.g. <c>Component1_razor.g.cs</c> →
/// <c>Component1_razor.decl.g.cs</c> -- so both halves keep the <c>.g.cs</c> suffix
/// (which the editor and MSBuild use to identify generated files) without stacking it.
/// </summary>
internal static string GetDeclIdentifierFromHintName(string implHintName)
{
const string ImplSuffix = ".g.cs";
const string DeclSuffix = ".decl.g.cs";

return implHintName.EndsWith(ImplSuffix, StringComparison.Ordinal)
? implHintName.Substring(0, implHintName.Length - ImplSuffix.Length) + DeclSuffix
: implHintName + DeclSuffix;
}

internal static void BuildIdentifierFromPath(StringBuilder builder, ReadOnlySpan<char> filePath)
{
for (var i = 0; i < filePath.Length; i++)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,23 +334,38 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
return (
hintName: GetIdentifierFromPath(filePath),
codeDocument: document.CodeDocument,
csharpDocument: document.CodeDocument.GetRequiredCSharpDocument());
csharpDocument: document.CodeDocument.GetRequiredCSharpDocument(),
declCSharpDocument: document.CodeDocument.GetDeclCSharpDocument());
})
.WithLambdaComparer(static (a, b) =>
{
// If either side has diagnostics on either document, force uncached output.
if (a.csharpDocument.Diagnostics.Length > 0 || b.csharpDocument.Diagnostics.Length > 0)
{
// if there are any diagnostics, treat the documents as unequal and force RegisterSourceOutput to be called uncached.
return false;
}
if ((a.declCSharpDocument?.Diagnostics.Length ?? 0) > 0 || (b.declCSharpDocument?.Diagnostics.Length ?? 0) > 0)
{
return false;
}

return a.csharpDocument.Text.ContentEquals(b.csharpDocument.Text);
if (!a.csharpDocument.Text.ContentEquals(b.csharpDocument.Text))
{
return false;
}

return (a.declCSharpDocument, b.declCSharpDocument) switch
{
(null, null) => true,
(not null, not null) => a.declCSharpDocument.Text.ContentEquals(b.declCSharpDocument.Text),
_ => false,
};
})
.WithTrackingName("CSharpDocuments");

context.RegisterImplementationSourceOutput(csharpDocuments, static (context, pair) =>
{
var (hintName, _, csharpDocument) = pair;
var (hintName, _, csharpDocument, declCSharpDocument) = pair;

RazorSourceGeneratorEventSource.Log.AddSyntaxTrees(hintName);
foreach (var razorDiagnostic in csharpDocument.Diagnostics)
Expand All @@ -359,6 +374,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
context.ReportDiagnostic(csharpDiagnostic);
}

if (declCSharpDocument is not null)
{
foreach (var razorDiagnostic in declCSharpDocument.Diagnostics)
{
var csharpDiagnostic = razorDiagnostic.AsDiagnostic();
context.ReportDiagnostic(csharpDiagnostic);
}

context.AddSource(GetDeclIdentifierFromHintName(hintName), declCSharpDocument.Text);
}

context.AddSource(hintName, csharpDocument.Text);
});

Expand All @@ -376,10 +402,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
using var filePathToDocument = new PooledDictionaryBuilder<string, (string, RazorCodeDocument)>();
using var hintNameToFilePath = new PooledDictionaryBuilder<string, string>();

foreach (var (hintName, codeDocument, _) in documents)
foreach (var (hintName, codeDocument, _, declCSharpDocument) in documents)
{
filePathToDocument.Add(codeDocument.Source.FilePath!, (hintName, codeDocument));
hintNameToFilePath.Add(hintName, codeDocument.Source.FilePath!);

if (declCSharpDocument is not null)
{
hintNameToFilePath.Add(GetDeclIdentifierFromHintName(hintName), codeDocument.Source.FilePath!);
}
}

context.AddOutput(nameof(RazorGeneratorResult), new RazorGeneratorResult(tagHelpers, filePathToDocument.ToImmutable(), hintNameToFilePath.ToImmutable()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ @inject IServiceProvider ServiceProvider

// Assert
Assert.Empty(result.Diagnostics);
Assert.Single(result.GeneratedSources);
Assert.Equal(2, result.GeneratedSources.Length);
result.VerifyOutputsMatchBaseline();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#pragma checksum "Shared/Component1.razor" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "7be8ebe54b4ff8c6cd9769f50cdd6dc6280903d6"
// <auto-generated/>
#pragma warning disable 1591
namespace MyApp.Shared
{
#line default
using global::System;
using global::System.Collections.Generic;
using global::System.Linq;
using global::System.Threading.Tasks;
using global::Microsoft.AspNetCore.Components;
#line default
#line hidden
#nullable restore
public partial class Component1 : global::Microsoft.AspNetCore.Components.ComponentBase
#nullable disable
{
[global::Microsoft.AspNetCore.Components.InjectAttribute] private
#nullable restore
#line (1,9)-(1,25) "Shared/Component1.razor"
IServiceProvider

#line default
#line hidden
#nullable disable

#nullable restore
#line (1,26)-(1,41) "Shared/Component1.razor"
ServiceProvider

#line default
#line hidden
#nullable disable
{ get; set; }
= default!;
}
}
#pragma warning restore 1591
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,6 @@ protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.
{
}
#pragma warning restore 1998
[global::Microsoft.AspNetCore.Components.InjectAttribute] private
#nullable restore
#line (1,9)-(1,25) "Shared/Component1.razor"
IServiceProvider

#line default
#line hidden
#nullable disable

#nullable restore
#line (1,26)-(1,41) "Shared/Component1.razor"
ServiceProvider

#line default
#line hidden
#nullable disable
{ get; set; }
= default!;
}
}
#pragma warning restore 1591
Loading