Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serialize Razor code document #8832

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions eng/Version.Details.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@
<Uri>https://github.com/dotnet/roslyn</Uri>
<Sha>f6725f6f04ce03574fcf89faa4b20b72ef83e4dd</Sha>
</Dependency>
<Dependency Name="Microsoft.SourceBuild.Intermediate.source-build-externals" Version="8.0.0-alpha.1.23312.1">
<Uri>https://github.com/dotnet/source-build-externals</Uri>
<Sha>50600f7fcd6e56a496233a859ed7d4d90c173b0d</Sha>
<SourceBuild RepoName="source-build-externals" ManagedOnly="true" />
</Dependency>
</ProductDependencies>
<ToolsetDependencies>
<!-- Listed as a dependency to workaround https://github.com/dotnet/cli/issues/10528 -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public static class RazorCodeDocumentExtensions
private static readonly char[] NamespaceSeparators = new char[] { '.' };
private static readonly object CssScopeKey = new object();
private static readonly object NamespaceKey = new object();
private static readonly object NoNamespaceToken = new object();

public static TagHelperDocumentContext GetTagHelperContext(this RazorCodeDocument document)
{
Expand Down Expand Up @@ -282,6 +283,16 @@ public static void SetCssScope(this RazorCodeDocument document, string cssScope)
document.Items[CssScopeKey] = cssScope;
}

internal static void SetNamespace(this RazorCodeDocument document, string @namespace)
{
if (document == null)
{
throw new ArgumentNullException(nameof(document));
}

document.Items[NamespaceKey] = @namespace;
}

#nullable enable
public static bool TryComputeClassName(this RazorCodeDocument codeDocument, [NotNullWhen(true)] out string? className)
{
Expand All @@ -303,12 +314,22 @@ public static bool TryComputeClassName(this RazorCodeDocument codeDocument, [Not
// However all kinds of thing are possible in tools. We shouldn't barf here if the document isn't
// set up correctly.
public static bool TryComputeNamespace(this RazorCodeDocument document, bool fallbackToRootNamespace, out string @namespace)
=> TryComputeNamespace(document, fallbackToRootNamespace, check: true, out @namespace);

internal static bool TryComputeNamespace(this RazorCodeDocument document, bool fallbackToRootNamespace, bool check, out string @namespace)
{
if (document == null)
{
throw new ArgumentNullException(nameof(document));
}

var namespaceValue = document.Items[NamespaceKey];
if (namespaceValue == NoNamespaceToken)
{
@namespace = null;
return false;
}

@namespace = (string)document.Items[NamespaceKey];
if (@namespace is null)
{
Expand All @@ -318,11 +339,14 @@ public static bool TryComputeNamespace(this RazorCodeDocument document, bool fal
}

#if DEBUG
// In debug mode, even if we're cached, lets take the hit to run this again and make sure the cached value is correct.
// This is to help us find issues with caching logic during development.
var validateResult = TryComputeNamespaceCore(document, fallbackToRootNamespace, out var validateNamespace);
Debug.Assert(validateResult, "We couldn't compute the namespace, but have a cached value, so something has gone wrong");
Debug.Assert(validateNamespace == @namespace, $"We cached a namespace of {@namespace} but calculated that it should be {validateNamespace}");
if (check)
{
// In debug mode, even if we're cached, lets take the hit to run this again and make sure the cached value is correct.
// This is to help us find issues with caching logic during development.
var validateResult = TryComputeNamespaceCore(document, fallbackToRootNamespace, out var validateNamespace);
Debug.Assert(validateResult, "We couldn't compute the namespace, but have a cached value, so something has gone wrong");
Debug.Assert(validateNamespace == @namespace, $"We cached a namespace of {@namespace} but calculated that it should be {validateNamespace}");
}
#endif

return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.ExternalAccess.RazorCompiler" ExcludeAssets="all" GeneratePathProperty="true" />
<PackageReference Include="Newtonsoft.Json" ExcludeAssets="all" GeneratePathProperty="true" />
</ItemGroup>

<ItemGroup>
Expand All @@ -24,6 +25,7 @@
<Target Name="PackLayout" BeforeTargets="$(GenerateNuspecDependsOn)">
<ItemGroup>
<Content Include="$(PkgMicrosoft_CodeAnalysis_ExternalAccess_RazorCompiler)\lib\netstandard2.0\*.dll" PackagePath="\source-generators" />
<Content Include="$(PkgNewtonsoft_Json)\lib\netstandard2.0\*.dll" PackagePath="\source-generators" />
<Content Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.Razor.SourceGenerators\$(Configuration)\netstandard2.0\*.*" Exclude="$(ArtifactsBinDir)**\*.pdb" PackagePath="\source-generators" />
<Content Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.Razor.Tooling.Internal\$(Configuration)\netstandard2.0\Microsoft.Extensions.ObjectPool.dll" PackagePath="\source-generators" />
<Content Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.Razor.Tooling.Internal\$(Configuration)\netstandard2.0\System.Collections.Immutable.dll" PackagePath="\source-generators" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@
<ExcludeFromSourceBuild>false</ExcludeFromSourceBuild>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\shared\JsonReaderExtensions.cs" LinkBase="Shared" />
<Compile Include="..\shared\RazorDiagnosticJsonConverter.cs" LinkBase="Shared" />
<Compile Include="..\shared\TagHelperDescriptorJsonConverter.cs" LinkBase="Shared" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />

<!-- IMPORTANT: Ensure all dependencies are embedded in the transport and toolset package. -->
<PackageReference Include="Microsoft.CodeAnalysis.ExternalAccess.RazorCompiler" />
<PackageReference Include="Newtonsoft.Json" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
{
var (filePath, document) = pair;
var hintName = GetIdentifierFromPath(filePath);
context.AddOutput(hintName + ".rsg.json", RazorCodeDocumentSerializer.Instance.Serialize(document));
context.AddOutput(hintName + ".rsg.cs", document.GetCSharpDocument().GeneratedCode);
context.AddOutput(hintName + ".rsg.html", document.GetHtmlDocument().GeneratedCode);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Newtonsoft.Json.Converters;
using System;

namespace Microsoft.NET.Sdk.Razor.SourceGenerators;

internal sealed class DelegateCreationConverter<T> : CustomCreationConverter<T>
{
private readonly Func<Type, T> _factory;

public DelegateCreationConverter(Func<Type, T> factory)
{
_factory = factory;
}

public override T Create(Type objectType) => _factory(objectType);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor.Serialization;
using Newtonsoft.Json;
using System;

namespace Microsoft.NET.Sdk.Razor.SourceGenerators;

internal sealed class DirectiveDescriptorConverter : JsonConverter<DirectiveDescriptor>
{
private const string ValuePropertyName = "_";

public override DirectiveDescriptor? ReadJson(JsonReader reader, Type objectType, DirectiveDescriptor? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.TokenType != JsonToken.StartObject)
{
return null;
}

var directive = reader.ReadPropertyName(nameof(DirectiveDescriptor.Directive)).ReadAsString();
if (!Enum.TryParse<DirectiveKind>(reader.ReadPropertyName(nameof(DirectiveDescriptor.Kind)).ReadAsString(), out var kind))
{
return null;
}

reader.ReadPropertyName(ValuePropertyName).Read();
var result = DirectiveDescriptor.CreateDirective(directive, kind, builder => serializer.Populate(reader, builder));
reader.AssertTokenAndAdvance(JsonToken.EndObject);
return result;
}

public override void WriteJson(JsonWriter writer, DirectiveDescriptor? value, JsonSerializer serializer)
{
if (value == null)
{
writer.WriteNull();
return;
}

writer.WriteStartObject();
writer.WritePropertyName(nameof(DirectiveDescriptor.Directive));
writer.WriteValue(value.Directive);
writer.WritePropertyName(nameof(DirectiveDescriptor.Kind));
writer.WriteValue(value.Kind.ToString());
writer.WritePropertyName(ValuePropertyName);
writer.WriteStartObject();
writer.WritePropertyName(nameof(DirectiveDescriptor.Description));
writer.WriteValue(value.Description);
writer.WritePropertyName(nameof(DirectiveDescriptor.DisplayName));
writer.WriteValue(value.DisplayName);
writer.WritePropertyName(nameof(DirectiveDescriptor.Usage));
writer.WriteValue(value.Usage.ToString());
writer.WritePropertyName(nameof(DirectiveDescriptor.Tokens));
writer.WriteStartArray();

foreach (var token in value.Tokens)
{
writer.WriteStartObject();
writer.WritePropertyName(nameof(DirectiveTokenDescriptor.Kind));
writer.WriteValue(token.Kind.ToString());
writer.WritePropertyName(nameof(DirectiveTokenDescriptor.Optional));
writer.WriteValue(token.Optional);
writer.WritePropertyName(nameof(DirectiveTokenDescriptor.Name));
writer.WriteValue(token.Name);
writer.WritePropertyName(nameof(DirectiveTokenDescriptor.Description));
writer.WriteValue(token.Description);
writer.WriteEndObject();
}

writer.WriteEndArray();
writer.WriteEndObject();
writer.WriteEndObject();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Razor.Language;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;

namespace Microsoft.NET.Sdk.Razor.SourceGenerators;

internal sealed class DirectiveTokenDescriptorConverter : JsonConverter<DirectiveTokenDescriptor>
{
public override DirectiveTokenDescriptor? ReadJson(JsonReader reader, Type objectType, DirectiveTokenDescriptor? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
var obj = JObject.Load(reader);

if (obj == null || !Enum.TryParse<DirectiveTokenKind>(obj[nameof(DirectiveTokenDescriptor.Kind)]!.Value<string>(), out var kind))
{
return null;
}

return DirectiveTokenDescriptor.CreateToken(
kind: kind,
optional: obj[nameof(DirectiveTokenDescriptor.Optional)]!.Value<bool>(),
name: obj[nameof(DirectiveTokenDescriptor.Name)]!.Value<string>(),
description: obj[nameof(DirectiveTokenDescriptor.Description)]!.Value<string>());
}

public override void WriteJson(JsonWriter writer, DirectiveTokenDescriptor? value, JsonSerializer serializer)
{
if (value == null)
{
writer.WriteNull();
return;
}

JObject.FromObject(value).WriteTo(writer);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Newtonsoft.Json;
using System;
using System.Text;

namespace Microsoft.NET.Sdk.Razor.SourceGenerators;

internal sealed class EncodingConverter : JsonConverter<Encoding>
{
public override Encoding? ReadJson(JsonReader reader, Type objectType, Encoding? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
var name = reader.ReadAsString();
return name is null ? null : Encoding.GetEncoding(name);
}

public override void WriteJson(JsonWriter writer, Encoding? value, JsonSerializer serializer)
{
writer.WriteValue(value?.WebName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Razor.Language;
using Newtonsoft.Json;
using System;
using System.Diagnostics;

namespace Microsoft.NET.Sdk.Razor.SourceGenerators;

internal sealed class ItemCollectionConverter : JsonConverter<ItemCollection>
{
public override ItemCollection? ReadJson(JsonReader reader, Type objectType, ItemCollection? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.TokenType != JsonToken.StartObject)
{
return null;
}

var result = existingValue ?? new ItemCollection();
while (reader.Read() && reader.TokenType == JsonToken.PropertyName)
{
var propertyName = (string)reader.Value!;
var value = reader.ReadAsString();
result.Add(propertyName, value);
}

Debug.Assert(reader.TokenType == JsonToken.EndObject);
return result;
}

public override void WriteJson(JsonWriter writer, ItemCollection? value, JsonSerializer serializer)
{
if (value == null)
{
writer.WriteNull();
return;
}

writer.WriteStartObject();

foreach (var pair in value)
{
if (pair.Key is string k && pair.Value is string v)
{
writer.WritePropertyName(k);
writer.WriteValue(v);
}
else
{
Debug.Assert(false, $"Cannot serialize non-string annotation {pair}");
}
}

writer.WriteEndObject();
}
}
Loading