Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4af47e1
Adding upgrade command path for next, moving the legacy ones out of t…
Nov 17, 2025
720b742
Claude experiment, trying to parse out rules and generate expressions
Nov 17, 2025
718e120
Updates in rule config parser. All apps seems to be parsing correctly…
Nov 18, 2025
b09d748
Removing things we don't need
Nov 18, 2025
6307375
Converting to a proper JS parser
Nov 18, 2025
8c0ecaa
Fixing errors parsing arrow functions
Nov 18, 2025
937cdbf
Better parser, conditional expression matcher
Nov 18, 2025
cf95354
Improvements
Nov 18, 2025
90a84a7
Updates
Nov 18, 2025
cd63f7f
Minor adjustments for better stats
Nov 18, 2025
9bdc112
Moving files to a better named folder
Nov 18, 2025
49d2f3e
Attempt at implementing conversion to data processors for the legeacy…
Nov 18, 2025
5705614
Cleanup
Nov 25, 2025
7f0210b
Improvements to data processing rules conversion
Nov 25, 2025
36e0ca7
Better support for converting JS -> C# in data processors
Nov 25, 2025
b361442
Proper indentation, fixes to build, upgrading package file as well
Nov 26, 2025
2612d8b
Upgrading to project references instead
Nov 26, 2025
cf10ecc
Adding a Set() method for generating a setter for a JSON-path in a da…
Nov 26, 2025
8045144
Using the new Set() in generated classes
Nov 26, 2025
1a73600
Improvments to code generation, I hope
Nov 26, 2025
05ccd86
More fixes
Nov 27, 2025
a941305
Improvements, type detection in data models
Nov 27, 2025
ffbc299
Converting rules, adding them to layout JSON files
Nov 28, 2025
40030ad
Rename/move
Nov 28, 2025
5eb756d
Converting data processing rules as well
Nov 28, 2025
8d9a111
Minor fixes by jetbrains, removing some unused code
Nov 28, 2025
85942a5
Minor fixes
Nov 28, 2025
79fdb80
Merge branch 'refs/heads/main' into chore/removing-rulehandler
Nov 28, 2025
1a2a3a8
Minor fixes
Nov 28, 2025
7bb1031
Removing IgnoreWarnings line
Nov 28, 2025
797da12
Fixes. Using Roslyn for adding lines to Program.cs instead
Nov 28, 2025
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
@@ -0,0 +1,147 @@
using System.Text;

namespace Altinn.App.Analyzers.SourceTextGenerator;

internal static class SetterGenerator
{
public static void Generate(StringBuilder builder, ModelPathNode rootNode)
{
if (rootNode.Properties.Count == 0)
{
builder.Append(
"""

/// <inheritdoc />
public bool Set(global::System.ReadOnlySpan<char> path, object? value) => false;

"""
);
return;
}
builder.Append(
"""

/// <inheritdoc />
public bool Set(global::System.ReadOnlySpan<char> path, object? value)
{
if (path.IsEmpty)
{
return false;
}

return SetRecursive(_dataModel, path, 0, value);
}

"""
);

GenerateRecursive(builder, rootNode, new HashSet<string>(StringComparer.Ordinal));
}

private static void GenerateRecursive(
StringBuilder builder,
ModelPathNode modelPathNode,
HashSet<string> generatedTypes
)
{
if (modelPathNode.ListType != null && generatedTypes.Add(modelPathNode.ListType))
{
builder.Append(
$$"""

private static bool SetRecursive(
{{modelPathNode.ListType}}? model,
global::System.ReadOnlySpan<char> path,
int literalIndex,
int offset,
object? value
)
{
if (model is null || literalIndex < 0 || literalIndex >= model.Count)
{
return false;
}

{{(
modelPathNode.Properties.Count == 0
? GenerateListElementSet(modelPathNode)
: "return SetRecursive(model[literalIndex], path, offset, value);"
)}}
}

"""
);
}
if (modelPathNode.IsJsonValueType || !generatedTypes.Add(modelPathNode.TypeName))
{
// Do not generate recursive setters for primitive types, or types already generated
return;
}

builder.Append(
$$"""

private static bool SetRecursive(
{{modelPathNode.TypeName}}? model,
global::System.ReadOnlySpan<char> path,
int offset,
object? value
)
{
if (model is null || offset == -1)
{
return false;
}

return ParseSegment(path, offset, out int nextOffset, out int literalIndex) switch
{

"""
);
foreach (var child in modelPathNode.Properties)
{
builder.Append(
child switch
{
{ ListType: not null } =>
$" \"{child.JsonName}\" => SetRecursive(model.{child.CSharpName}, path, literalIndex, nextOffset, value),\r\n",
{ Properties.Count: 0 } =>
$" \"{child.JsonName}\" when nextOffset is -1 && literalIndex is -1 => TrySetValue(val => model.{child.CSharpName} = val, value),\r\n",
_ =>
$" \"{child.JsonName}\" when literalIndex is -1 => SetRecursive(model.{child.CSharpName}, path, nextOffset, value),\r\n",
}
);
}

// Return false for unknown paths
builder.Append(
"""
_ => false,
};
}

"""
);

foreach (var child in modelPathNode.Properties)
{
GenerateRecursive(builder, child, generatedTypes);
}
}

private static string GenerateListElementSet(ModelPathNode modelPathNode)
{
// For list elements that are primitives, we need to handle the set differently
// Extract the element type from the list type (e.g., "List<int>" -> "int")
var elementType = modelPathNode.TypeName;

return @"if (offset == -1)
{
return TrySetValue<"
+ elementType
+ @">(val => model[literalIndex] = val, value);
}
return false;
";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ dataModel as {{rootNode.TypeName}}
builder.Append("\r\n #region Getters\r\n");
GetterGenerator.Generate(builder, rootNode);
builder.Append("\r\n #endregion Getters\r\n");
builder.Append(" #region Setters\r\n");
SetterGenerator.Generate(builder, rootNode);
builder.Append("\r\n #endregion Setters\r\n");
builder.Append(" #region AddIndexToPath\r\n");
AddIndexToPathGenerator.Generate(builder, rootNode);
builder.Append("\r\n #endregion AddIndexToPath\r\n");
Expand All @@ -68,6 +71,96 @@ dataModel as {{rootNode.TypeName}}

builder.Append(
$$"""
private static bool TrySetValue<T>(global::System.Action<T> setter, object? value)
{
if (value is null)
{
if (typeof(T).IsValueType && global::System.Nullable.GetUnderlyingType(typeof(T)) is null)
{
return false;
}
setter(default(T)!);
return true;
}

if (value is T typedValue)
{
setter(typedValue);
return true;
}

try
{
var targetType = typeof(T);
var underlyingType = global::System.Nullable.GetUnderlyingType(targetType);
if (underlyingType is not null)
{
targetType = underlyingType;
}

if (targetType == typeof(string))
{
setter((T)(object)value.ToString()!);
return true;
}

if (targetType == typeof(int))
{
setter((T)(object)global::System.Convert.ToInt32(value, global::System.Globalization.CultureInfo.InvariantCulture));
return true;
}

if (targetType == typeof(long))
{
setter((T)(object)global::System.Convert.ToInt64(value, global::System.Globalization.CultureInfo.InvariantCulture));
return true;
}

if (targetType == typeof(decimal))
{
setter((T)(object)global::System.Convert.ToDecimal(value, global::System.Globalization.CultureInfo.InvariantCulture));
return true;
}

if (targetType == typeof(double))
{
setter((T)(object)global::System.Convert.ToDouble(value, global::System.Globalization.CultureInfo.InvariantCulture));
return true;
}

if (targetType == typeof(float))
{
setter((T)(object)global::System.Convert.ToSingle(value, global::System.Globalization.CultureInfo.InvariantCulture));
return true;
}

if (targetType == typeof(bool))
{
setter((T)(object)global::System.Convert.ToBoolean(value, global::System.Globalization.CultureInfo.InvariantCulture));
return true;
}

if (targetType == typeof(global::System.DateTime))
{
setter((T)(object)global::System.Convert.ToDateTime(value, global::System.Globalization.CultureInfo.InvariantCulture));
return true;
}

if (targetType.IsEnum)
{
setter((T)global::System.Enum.Parse(targetType, value.ToString() ?? string.Empty));
return true;
}

setter((T)global::System.Convert.ChangeType(value, targetType, global::System.Globalization.CultureInfo.InvariantCulture));
return true;
}
catch
{
return false;
}
}

public static global::System.ReadOnlySpan<char> ParseSegment(global::System.ReadOnlySpan<char> path, int offset, out int nextOffset, out int literalIndex)
{
if (offset < 0 || offset > path.Length)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@
/// <param name="path">The dotted path to use (including inline indexes)</param>
object? Get(ReadOnlySpan<char> path);

/// <summary>
/// Set the value at the given path in the data model
/// </summary>
/// <param name="path">The dotted path to use (including inline indexes)</param>
/// <param name="value">The value to set (will be automatically converted to the target type if possible)</param>
/// <returns>True if the value was set successfully, false if the path could not be resolved or type conversion failed</returns>
bool Set(ReadOnlySpan<char> path, object? value);

/// <summary>
/// Remove the field at the given path
/// If the path points to a row (last segment is a list with an explicit [index]), the row will be removed or set to null based on rowRemovalOption
Expand Down Expand Up @@ -128,6 +136,37 @@
}
}

/// <summary>
/// Simple way to set a value in the form data model adding indexes from context
/// </summary>
/// <returns>True if the value was set successfully, false if the path could not be resolved or type conversion failed</returns>
public static bool Set(
this IFormDataWrapper formDataWrapper,
ReadOnlySpan<char> path,
object? value,
ReadOnlySpan<int> rowIndexes = default
)
{
int len = GetMaxBufferLength(path, rowIndexes);
if (len <= 512)
{
Span<char> buffer = stackalloc char[len];
var indexedPath = formDataWrapper.AddIndexToPath(path, rowIndexes, buffer);
return indexedPath.IsEmpty ? false : formDataWrapper.Set(indexedPath, value);

Check notice

Code scanning / CodeQL

Unnecessarily complex Boolean expression Note

The expression 'A ? false : B' can be simplified to '!A && B'.

Copilot Autofix

AI about 19 hours ago

To fix the unnecessarily complex boolean expression on line 155, replace the ternary statement indexedPath.IsEmpty ? false : formDataWrapper.Set(indexedPath, value) with the simplified boolean logic !indexedPath.IsEmpty && formDataWrapper.Set(indexedPath, value). This preserves the logic: the result is false if indexedPath.IsEmpty is true, and otherwise the result is whatever is returned by formDataWrapper.Set(indexedPath, value). Only a single line in method Set within the extension class FormDataWrapperExtensions in file src/App/backend/src/Altinn.App.Core/Internal/Data/IFormDataWrapper.cs needs updating.


Suggested changeset 1
src/App/backend/src/Altinn.App.Core/Internal/Data/IFormDataWrapper.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/App/backend/src/Altinn.App.Core/Internal/Data/IFormDataWrapper.cs b/src/App/backend/src/Altinn.App.Core/Internal/Data/IFormDataWrapper.cs
--- a/src/App/backend/src/Altinn.App.Core/Internal/Data/IFormDataWrapper.cs
+++ b/src/App/backend/src/Altinn.App.Core/Internal/Data/IFormDataWrapper.cs
@@ -152,7 +152,7 @@
         {
             Span<char> buffer = stackalloc char[len];
             var indexedPath = formDataWrapper.AddIndexToPath(path, rowIndexes, buffer);
-            return indexedPath.IsEmpty ? false : formDataWrapper.Set(indexedPath, value);
+            return !indexedPath.IsEmpty && formDataWrapper.Set(indexedPath, value);
         }
 
         char[] pool = System.Buffers.ArrayPool<char>.Shared.Rent(len);
EOF
@@ -152,7 +152,7 @@
{
Span<char> buffer = stackalloc char[len];
var indexedPath = formDataWrapper.AddIndexToPath(path, rowIndexes, buffer);
return indexedPath.IsEmpty ? false : formDataWrapper.Set(indexedPath, value);
return !indexedPath.IsEmpty && formDataWrapper.Set(indexedPath, value);
}

char[] pool = System.Buffers.ArrayPool<char>.Shared.Rent(len);
Copilot is powered by AI and may make mistakes. Always verify output.
}

char[] pool = System.Buffers.ArrayPool<char>.Shared.Rent(len);
try
{
var indexedPath = formDataWrapper.AddIndexToPath(path, rowIndexes, pool);
return indexedPath.IsEmpty ? false : formDataWrapper.Set(indexedPath, value);

Check notice

Code scanning / CodeQL

Unnecessarily complex Boolean expression Note

The expression 'A ? false : B' can be simplified to '!A && B'.

Copilot Autofix

AI about 19 hours ago

To fix the unnecessarily complex Boolean expression in line 162, replace the ternary conditional indexedPath.IsEmpty ? false : formDataWrapper.Set(indexedPath, value) with the logical expression !indexedPath.IsEmpty && formDataWrapper.Set(indexedPath, value). This removes the ternary operator where a logical operator suffices, making the code simpler and more idiomatic in C#. Only line 162 of file src/App/backend/src/Altinn.App.Core/Internal/Data/IFormDataWrapper.cs needs to be changed. No new imports or methods are needed.

Suggested changeset 1
src/App/backend/src/Altinn.App.Core/Internal/Data/IFormDataWrapper.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/App/backend/src/Altinn.App.Core/Internal/Data/IFormDataWrapper.cs b/src/App/backend/src/Altinn.App.Core/Internal/Data/IFormDataWrapper.cs
--- a/src/App/backend/src/Altinn.App.Core/Internal/Data/IFormDataWrapper.cs
+++ b/src/App/backend/src/Altinn.App.Core/Internal/Data/IFormDataWrapper.cs
@@ -159,7 +159,7 @@
         try
         {
             var indexedPath = formDataWrapper.AddIndexToPath(path, rowIndexes, pool);
-            return indexedPath.IsEmpty ? false : formDataWrapper.Set(indexedPath, value);
+            return !indexedPath.IsEmpty && formDataWrapper.Set(indexedPath, value);
         }
         finally
         {
EOF
@@ -159,7 +159,7 @@
try
{
var indexedPath = formDataWrapper.AddIndexToPath(path, rowIndexes, pool);
return indexedPath.IsEmpty ? false : formDataWrapper.Set(indexedPath, value);
return !indexedPath.IsEmpty && formDataWrapper.Set(indexedPath, value);
}
finally
{
Copilot is powered by AI and may make mistakes. Always verify output.
}
finally
{
System.Buffers.ArrayPool<char>.Shared.Return(pool);
}
}

/// <summary>
/// Get a string representation of the path with indexes from context added
/// </summary>
Expand Down
Loading
Loading