Skip to content
Merged
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
3 changes: 2 additions & 1 deletion package-versions.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<!-- Published dependencies (only update on major version change) -->
<CodeAnalysisFrozenVersion>4.1.0</CodeAnalysisFrozenVersion>
<CodeAnalysisFrozenVersion>4.13.0</CodeAnalysisFrozenVersion>
<DemystifierFrozenVersion>0.4.1</DemystifierFrozenVersion>
<HumanizerFrozenVersion>2.14.1</HumanizerFrozenVersion>
<NewtonsoftJsonFrozenVersion>13.0.4</NewtonsoftJsonFrozenVersion>
Expand All @@ -24,6 +24,7 @@
<MiniValidationVersion>0.9.*</MiniValidationVersion>
<NSwagApiClientVersion>14.6.*</NSwagApiClientVersion>
<NewtonsoftJsonVersion>13.0.*</NewtonsoftJsonVersion>
<PolyfillVersion>8.8.*</PolyfillVersion>
<ReadableExpressionsVersion>4.1.*</ReadableExpressionsVersion>
<ScalarAspNetCoreVersion>2.9.*</ScalarAspNetCoreVersion>
<SwashbuckleVersion>9.*-*</SwashbuckleVersion>
Expand Down
318 changes: 232 additions & 86 deletions src/JsonApiDotNetCore.SourceGenerators/ControllerSourceGenerator.cs

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions src/JsonApiDotNetCore.SourceGenerators/CoreControllerInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Microsoft.CodeAnalysis;

namespace JsonApiDotNetCore.SourceGenerators;

/// <summary>
/// Basic outcome from the code analysis.
/// </summary>
internal readonly record struct CoreControllerInfo(
TypeInfo ResourceType, TypeInfo IdType, string ControllerNamespace, JsonApiEndpointsCopy Endpoints, bool WriteNullableEnable)
{
// Using readonly fields, so they can be passed by reference (using 'in' modifier, to avoid making copies) during code generation.
public readonly TypeInfo ResourceType = ResourceType;
public readonly TypeInfo IdType = IdType;
public readonly string ControllerNamespace = ControllerNamespace;
public readonly JsonApiEndpointsCopy Endpoints = Endpoints;
public readonly bool WriteNullableEnable = WriteNullableEnable;

public static CoreControllerInfo? TryCreate(INamedTypeSymbol resourceTypeSymbol, ITypeSymbol idTypeSymbol, JsonApiEndpointsCopy endpoints,
string controllerNamespace)
{
TypeInfo? resourceTypeInfo = TypeInfo.CreateFromQualified(resourceTypeSymbol);
TypeInfo? idTypeInfo = TypeInfo.TryCreateFromQualifiedOrPossiblyNullableKeyword(idTypeSymbol);

if (idTypeInfo == null)
{
return null;
}

bool writeNullableEnable = idTypeSymbol is { IsReferenceType: true, NullableAnnotation: NullableAnnotation.Annotated };

return new CoreControllerInfo(resourceTypeInfo.Value, idTypeInfo.Value, controllerNamespace, endpoints, writeNullableEnable);
}
}
29 changes: 29 additions & 0 deletions src/JsonApiDotNetCore.SourceGenerators/FullControllerInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace JsonApiDotNetCore.SourceGenerators;

/// <summary>
/// Supplemental information that is derived from the core analysis, which is expensive to produce.
/// </summary>
internal readonly record struct FullControllerInfo(
CoreControllerInfo CoreController, TypeInfo ControllerType, TypeInfo LoggerFactoryInterface, string HintFileName)
{
// Using readonly fields, so they can be passed by reference (using 'in' modifier, to avoid making copies) during code generation.
public readonly CoreControllerInfo CoreController = CoreController;
public readonly TypeInfo ControllerType = ControllerType;
public readonly TypeInfo LoggerFactoryInterface = LoggerFactoryInterface;
public readonly string HintFileName = HintFileName;

public static FullControllerInfo Create(CoreControllerInfo coreController, string controllerTypeName)
{
var controllerTypeInfo = new TypeInfo(coreController.ControllerNamespace, controllerTypeName);
var loggerFactoryTypeInfo = new TypeInfo("Microsoft.Extensions.Logging", "ILoggerFactory");

return new FullControllerInfo(coreController, controllerTypeInfo, loggerFactoryTypeInfo, controllerTypeName);
}

public FullControllerInfo WithHintFileName(string hintFileName)
{
// ReSharper disable once UseWithExpressionToCopyRecord
// Justification: Workaround for bug at https://youtrack.jetbrains.com/issue/RSRP-502017/Invalid-suggestion-to-use-with-expression.
return new FullControllerInfo(CoreController, ControllerType, LoggerFactoryInterface, hintFileName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Collections.Immutable;

namespace JsonApiDotNetCore.SourceGenerators;

// This type was copied from Roslyn. The implementation looks odd, but is likely a performance tradeoff.
// Beware that the consuming code doesn't adhere to the typical pattern where a dictionary is built once, then queried many times.

internal sealed class ImmutableDictionaryEqualityComparer<TKey, TValue> : IEqualityComparer<ImmutableDictionary<TKey, TValue>?>
where TKey : notnull
{
public static readonly ImmutableDictionaryEqualityComparer<TKey, TValue> Instance = new();

public bool Equals(ImmutableDictionary<TKey, TValue>? x, ImmutableDictionary<TKey, TValue>? y)
{
if (ReferenceEquals(x, y))
{
return true;
}

if (x is null || y is null)
{
return false;
}

if (!Equals(x.KeyComparer, y.KeyComparer) || !Equals(x.ValueComparer, y.ValueComparer))
{
return false;
}

foreach ((TKey key, TValue value) in x)
{
if (!y.TryGetValue(key, out TValue? other) || !x.ValueComparer.Equals(value, other))
{
return false;
}
}

return true;
}

public int GetHashCode(ImmutableDictionary<TKey, TValue>? obj)
{
return obj?.Count ?? 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
<IsPackable>true</IsPackable>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IncludeBuildOutput>false</IncludeBuildOutput>
<NoWarn>$(NoWarn);NU5128</NoWarn>
<NoWarn>$(NoWarn);NU5128;RS2008</NoWarn>
<IsRoslynComponent>true</IsRoslynComponent>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>

<Import Project="..\..\package-versions.props" />
Expand Down Expand Up @@ -47,5 +48,6 @@
<ItemGroup>
<PackageReference Include="Humanizer.Core" Version="$(HumanizerFrozenVersion)" PrivateAssets="all" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(CodeAnalysisFrozenVersion)" PrivateAssets="all" />
<PackageReference Include="Polyfill" Version="$(PolyfillVersion)" PrivateAssets="all" />
</ItemGroup>
</Project>
32 changes: 32 additions & 0 deletions src/JsonApiDotNetCore.SourceGenerators/LocationInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

namespace JsonApiDotNetCore.SourceGenerators;

internal readonly record struct LocationInfo(string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan)
{
public static LocationInfo? TryCreateFrom(SyntaxNode node)
{
return TryCreateFrom(node.GetLocation());
}

private static LocationInfo? TryCreateFrom(Location location)
{
if (location.SourceTree is null)
{
return null;
}

return new LocationInfo(location.SourceTree.FilePath, location.SourceSpan, location.GetLineSpan().Span);
}

public Location ToLocation()
{
return Location.Create(FilePath, TextSpan, LineSpan);
}

public override string ToString()
{
return ToLocation().ToString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace JsonApiDotNetCore.SourceGenerators;

internal readonly record struct MissingInterfaceDiagnostic(string ResourceTypeName, LocationInfo? Location);
3 changes: 3 additions & 0 deletions src/JsonApiDotNetCore.SourceGenerators/SemanticResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace JsonApiDotNetCore.SourceGenerators;

internal readonly record struct SemanticResult(CoreControllerInfo? CoreController, MissingInterfaceDiagnostic? Diagnostic);
Loading