Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ src/Altinn.Apps/AppTemplates/AspNet/App/Properties/launchSettings.json

*.received.txt
*.received.json
*.received.cs

# Benchmark Results
BenchmarkDotNet.Artifacts/
Expand Down
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<PackageVersion Include="Microsoft.Build.Tasks.Core" Version="17.10.29"/>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.17" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="8.0.17" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.VisualBasic.Workspaces" Version="4.14.0" />
Expand Down Expand Up @@ -57,6 +58,7 @@
<PackageVersion Include="Verify.Xunit" Version="30.4.0" />
<PackageVersion Include="Verify.Http" Version="6.6.0" />
<PackageVersion Include="WireMock.Net" Version="1.8.13" />
<PackageVersion Include="Verify.SourceGenerators" Version="2.5.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.assert" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.1" />
Expand Down
33 changes: 33 additions & 0 deletions solutions/All.sln
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Altinn.App.Api.Tests", "..\test\Altinn.App.Api.Tests\Altinn.App.Api.Tests.csproj", "{6F762771-DC24-4139-B1B2-DD0F6A4E2B5B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Altinn.App.Integration.Tests", "..\test\Altinn.App.Integration.Tests\Altinn.App.Integration.Tests.csproj", "{0DE9F775-FE1A-4604-B239-4CFE4E105B1D}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Altinn.App.SourceGenerator.Tests", "..\test\Altinn.App.SourceGenerator.Tests\Altinn.App.SourceGenerator.Tests.csproj", "{976E3CF9-26CC-4825-92F7-5EEBD99A8B6A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C5968C89-1529-42F3-B082-DC1184C18395}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{B96EA64C-2F26-42FE-A260-F6A4C6AA48E7}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{D8D6ED6A-EF63-48FC-B267-17FB76C5DD07}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Altinn.App.SourceGenerator.Integration.Tests", "..\test\Altinn.App.SourceGenerator.Integration.Tests\Altinn.App.SourceGenerator.Integration.Tests.csproj", "{836CECBF-5518-4276-BE2C-B0F255BF19F2}"
ProjectSection(ProjectDependencies) = postProject
{E8F29FE8-6B62-41F1-A08C-2A318DD08BB4} = {E8F29FE8-6B62-41F1-A08C-2A318DD08BB4}
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -72,5 +84,26 @@ Global
{0DE9F775-FE1A-4604-B239-4CFE4E105B1D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0DE9F775-FE1A-4604-B239-4CFE4E105B1D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0DE9F775-FE1A-4604-B239-4CFE4E105B1D}.Release|Any CPU.Build.0 = Release|Any CPU
{976E3CF9-26CC-4825-92F7-5EEBD99A8B6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{976E3CF9-26CC-4825-92F7-5EEBD99A8B6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{976E3CF9-26CC-4825-92F7-5EEBD99A8B6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{976E3CF9-26CC-4825-92F7-5EEBD99A8B6A}.Release|Any CPU.Build.0 = Release|Any CPU
{836CECBF-5518-4276-BE2C-B0F255BF19F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{836CECBF-5518-4276-BE2C-B0F255BF19F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{836CECBF-5518-4276-BE2C-B0F255BF19F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{836CECBF-5518-4276-BE2C-B0F255BF19F2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{D6C1901B-BED6-44AC-A061-5981A2DEB937} = {C5968C89-1529-42F3-B082-DC1184C18395}
{E8F29FE8-6B62-41F1-A08C-2A318DD08BB4} = {C5968C89-1529-42F3-B082-DC1184C18395}
{8006DE72-9E93-47A0-AD15-C4B5244B08DA} = {C5968C89-1529-42F3-B082-DC1184C18395}
{3C1E1C25-9072-42C5-8058-162ADF136B3F} = {B96EA64C-2F26-42FE-A260-F6A4C6AA48E7}
{6F762771-DC24-4139-B1B2-DD0F6A4E2B5B} = {B96EA64C-2F26-42FE-A260-F6A4C6AA48E7}
{F5732D5E-FBF2-4905-AE49-9D0171AE0698} = {B96EA64C-2F26-42FE-A260-F6A4C6AA48E7}
{976E3CF9-26CC-4825-92F7-5EEBD99A8B6A} = {B96EA64C-2F26-42FE-A260-F6A4C6AA48E7}
{C69F2010-B4CF-42FE-9DEC-81F3B1C06294} = {B96EA64C-2F26-42FE-A260-F6A4C6AA48E7}
{A1CD82E1-D0B7-4C04-9DF4-8E7277706D3F} = {C5968C89-1529-42F3-B082-DC1184C18395}
{626CC2E1-1D1D-45A4-B07C-A120BB4EE973} = {D8D6ED6A-EF63-48FC-B267-17FB76C5DD07}
{836CECBF-5518-4276-BE2C-B0F255BF19F2} = {B96EA64C-2F26-42FE-A260-F6A4C6AA48E7}
EndGlobalSection
EndGlobal
1 change: 1 addition & 0 deletions src/Altinn.App.Analyzers/Altinn.App.Analyzers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<LangVersion>12</LangVersion>

<IncludeBuildOutput>false</IncludeBuildOutput>
<NoPackageAnalysis>true</NoPackageAnalysis>
Expand Down
1 change: 1 addition & 0 deletions src/Altinn.App.Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
ALTINNAPP0001 | General | Warning | Project not found
ALTINNAPP0002 | Metadata | Warning | Error in applicationmetadata.json
ALTINNAPP9999 | General | Warning | Unknown error
ALTINNAPP0500 | CodeSmells | Warning | CodeSmells
13 changes: 13 additions & 0 deletions src/Altinn.App.Analyzers/Diagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,25 @@ public static class CodeSmells
);
}

public static class FormDataWrapperGenerator
{
public static readonly DiagnosticDescriptor AppMetadataError = Warning(
"ALTINNAPP0002",
Category.Metadata,
"Application metadata error",
"Error in applicationmetadata.json: {0}"
);
}

private const string DocsRoot = "https://docs.altinn.studio/nb/altinn-studio/reference/analysis/";
private const string RulesRoot = DocsRoot + "rules/";

private static DiagnosticDescriptor Warning(string id, string category, string title, string messageFormat) =>
Create(id, title, messageFormat, category, DiagnosticSeverity.Warning);

private static DiagnosticDescriptor Error(string id, string category, string title, string messageFormat) =>
Create(id, title, messageFormat, category, DiagnosticSeverity.Error);

private static DiagnosticDescriptor Create(
string id,
string title,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
using Altinn.App.Analyzers.SourceTextGenerator;
using Altinn.App.Analyzers.Utils;
using NanoJsonReader;

namespace Altinn.App.Analyzers.IncrementalGenerator;

/// <summary>
/// Generate IFormDataWrapper implementations for classes in models/*.cs in the app.
/// </summary>
[Generator]
public class FormDataWrapperGenerator : IIncrementalGenerator
{
private sealed record Result<T>(T? Value, EquatableArray<EquatableDiagnostic> Diagnostics)
where T : class
{
public Result(EquatableDiagnostic diagnostics)
: this(null, new EquatableArray<EquatableDiagnostic>([diagnostics])) { }

public Result(T value)
: this(value, EquatableArray<EquatableDiagnostic>.Empty) { }
};

private sealed record ModelClassOrDiagnostic(
string? ClassName,
Location? Location,
EquatableArray<EquatableDiagnostic> Diagnostics
)
{
public ModelClassOrDiagnostic(EquatableDiagnostic diagnostic)
: this(null, null, new([diagnostic])) { }

public ModelClassOrDiagnostic(string className, Location? location)
: this(className, location, EquatableArray<EquatableDiagnostic>.Empty) { }
};

/// <inheritdoc />
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var rootClasses = context
.AdditionalTextsProvider.Where(text =>
text.Path.Replace('\\', '/').EndsWith("config/applicationmetadata.json")
)
.SelectMany(ParseModelClassOrDiagnostic);

var modelPathNodesProvider = rootClasses.Combine(context.CompilationProvider).Select(CreateNodeTree);

context.RegisterSourceOutput(modelPathNodesProvider, GenerateFromNode);
}

private static ImmutableArray<ModelClassOrDiagnostic> ParseModelClassOrDiagnostic(
AdditionalText text,
CancellationToken token
)
{
try
{
var textContent = text.GetText(token)?.ToString();
if (textContent is null)
{
return
[
new(
new EquatableDiagnostic(
Diagnostics.FormDataWrapperGenerator.AppMetadataError,
FileLocationHelper.GetLocation(text, 0, null),
["Failed to read applicationmetadata.json"]
)
),
];
}

var appMetadata = JsonValue.Parse(textContent);
if (appMetadata.Type != JsonType.Object)
{
return
[
new(
new EquatableDiagnostic(
Diagnostics.FormDataWrapperGenerator.AppMetadataError,
FileLocationHelper.GetLocation(text, appMetadata.Start, appMetadata.End),
["applicationmetadata.json is not a valid JSON object"]
)
),
];
}

var dataTypes = appMetadata.GetProperty("dataTypes");
if (dataTypes?.Type != JsonType.Array)
{
return
[
new(
new(
Diagnostics.FormDataWrapperGenerator.AppMetadataError,
FileLocationHelper.GetLocation(text, appMetadata.Start, appMetadata.End),
["the property dataTypes is not a valid JSON array"]
)
),
];
}

List<ModelClassOrDiagnostic> rootClasses = [];
foreach (var dataType in dataTypes.GetArrayValues())
{
if (dataType.Type != JsonType.Object)
{
continue;
Copy link
Contributor

Choose a reason for hiding this comment

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

This branch could emit a diagnostic, all the values in dataTypes should be objects

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I definitely could add more diagnostics. Not sure I want to do that, because diagnostics from incremental source generators is pretty buggy. I wanted to have a diagnostic for not finding "ClassRef" in compilation, and added some extra just because they were easy.

Copy link
Contributor

Choose a reason for hiding this comment

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

Reading up on the whole "diagnostics from incremental generators" topic, it seems like there is no intention to have a create story there. Maybe we should go in the opposite direction then? Drop all diagnostics here and rather implement analyzers if we feel it is necessary?

}

var appLogic = dataType.GetProperty("appLogic");
if (appLogic?.Type != JsonType.Object)
{
continue;
}

var classRef = appLogic.GetProperty("classRef");
if (classRef?.Type != JsonType.String)
{
continue;
}

rootClasses.Add(
new(classRef.GetString(), FileLocationHelper.GetLocation(text, classRef.Start, classRef.End))
);
}

return [.. rootClasses];
}
catch (NanoJsonException e)
{
return
[
new(
new EquatableDiagnostic(
Diagnostics.FormDataWrapperGenerator.AppMetadataError,
FileLocationHelper.GetLocation(text, e.StartIndex, e.EndIndex),
[e.Message]
)
),
];
}
}

private static Result<ModelPathNode> CreateNodeTree(
(ModelClassOrDiagnostic, Compilation) tuple,
CancellationToken _
)
{
var (rootSymbolFullName, compilation) = tuple;
if (rootSymbolFullName.ClassName is null)
{
return new Result<ModelPathNode>(null, rootSymbolFullName.Diagnostics);
}
var rootSymbol = compilation.GetBestTypeByMetadataName(rootSymbolFullName.ClassName);
if (rootSymbol == null)
{
return new Result<ModelPathNode>(
new EquatableDiagnostic(
Diagnostics.FormDataWrapperGenerator.AppMetadataError,
rootSymbolFullName.Location,
[$"Could not find class {rootSymbolFullName.ClassName} in the compilation"]
)
);
}

return new Result<ModelPathNode>(
new ModelPathNode("", "", SourceReaderUtils.TypeSymbolToString(rootSymbol), GetNodeProperties(rootSymbol))
);
}

private static EquatableArray<ModelPathNode>? GetNodeProperties(INamedTypeSymbol namedTypeSymbol)
{
var nodeProperties = new List<ModelPathNode>();
foreach (var property in namedTypeSymbol.GetMembers().OfType<IPropertySymbol>())
{
if (
property.IsStatic
|| property.IsReadOnly
|| property.IsWriteOnly
|| property.IsImplicitlyDeclared
|| property.IsIndexer
)
{
// Skip static, readonly, writeonly, implicitly declared and indexer properties
continue;
}
var (propertyTypeSymbol, propertyCollectionTypeSymbol) = SourceReaderUtils.GetTypeFromProperty(
property.Type
);

var cSharpName = property.Name;
var jsonName = SourceReaderUtils.GetJsonName(property) ?? cSharpName;
var typeString = SourceReaderUtils.TypeSymbolToString(propertyTypeSymbol);
var collectionTypeString = propertyCollectionTypeSymbol is null
? null
: SourceReaderUtils.TypeSymbolToString(propertyCollectionTypeSymbol);

if (
propertyTypeSymbol is INamedTypeSymbol propertyNamedTypeSymbol
&& !propertyNamedTypeSymbol.ContainingNamespace.ToString().StartsWith("System")
)
{
nodeProperties.Add(
new ModelPathNode(
cSharpName,
jsonName,
typeString,
GetNodeProperties(propertyNamedTypeSymbol),
collectionTypeString
)
);
}
else
{
nodeProperties.Add(new ModelPathNode(cSharpName, jsonName, typeString, null, collectionTypeString));
}
}
return nodeProperties;
}

private static void GenerateFromNode(SourceProductionContext context, Result<ModelPathNode> result)
{
foreach (var diagnostic in result.Diagnostics)
{
context.ReportDiagnostic(diagnostic.CreateDiagnostic());
}
if (result is not { Value: { } node })
return;
var sourceText = SourceTextGenerator.SourceTextGenerator.GenerateSourceText(node, "public");
context.AddSource(node.Name + "FormDataWrapper.g.cs", sourceText);
}
}
Loading
Loading