Skip to content

Support lists in expressions#1769

Open
TomasEng wants to merge 5 commits into
mainfrom
support-lists-in-expressions
Open

Support lists in expressions#1769
TomasEng wants to merge 5 commits into
mainfrom
support-lists-in-expressions

Conversation

@TomasEng
Copy link
Copy Markdown
Contributor

@TomasEng TomasEng commented May 27, 2026

Description

This pull request adds support for arrays, along with a list function, in expressions.

  • list function is added
  • Array support is added to ExpressionValue (most of the code was already there, hidden in comments)
  • Made the tests support test files with several test cases and undefined root expression (currently, this is only supported for functions with a simple test file, but that is not the case for dataModel, of which the new file lookup-list.json contain several test cases)

Here is the corresponding change in App Frontend: Altinn/altinn-studio#18974

Related Issue(s)

Verification

  • Your code builds clean without any errors or warnings
  • Not possible to test manually until we have added aggregate functions (without which there is no use case for lists), but the tests should be sufficient
  • Relevant automated test added (if you find this hard, leave it and we'll help out)
  • All tests run green

Documentation

Summary by CodeRabbit

  • New Features

    • Added list expression support so expressions can create, return and evaluate array/list values.
  • Tests

    • Added and expanded test fixtures and unit tests for list behavior: creation, empty lists, nested/mixed-type arrays, lookups, parsing/serialization, and evaluation edge cases.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

📝 Walkthrough

Walkthrough

Adds end-to-end array/list support: ExpressionValue gains array storage and conversions, JSON (de)serialization handles arrays, ExpressionFunction.list is added and evaluated by ExpressionEvaluator, and tests plus fixtures were added or updated for list behavior and lookups.

Changes

Array Value Support in Expression Evaluator

Layer / File(s) Summary
Array value support in ExpressionValue
src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs
ExpressionValue adds _arrayValue, constructor accepting ExpressionValue[], implicit conversion operator, public Array property with validation, and extends FromObject/JsonElementToObject to convert enumerables and JSON arrays. ToObject() and ExpressionTypeUnionConverter read/write now handle arrays.
List expression function definition and evaluation
src/Altinn.App.Core/Models/Expressions/ExpressionFunction.cs, src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs
ExpressionFunction enum gains new list member; ExpressionEvaluator routes ExpressionFunction.list in EvaluateExpression_internal to a new List helper that returns arguments unchanged as ExpressionValue[].
Test harness updates for nullable expressions and array handling
test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ExpressionTestCaseRoot.cs, test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestBackendExclusiveFunctions.cs, test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs, test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestInvalid.cs
ExpressionTestCaseRoot.Expression becomes nullable; test files add using directives and cast test.Expression to Expression with null-forgiving operators. TestFunctions adds List_Theory and updates RunTestCase to skip expression evaluation when test.Expression is null. Invalid-test invocation formatting updated to pass explicit awaited context.
Array value tests and test fixtures
test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/ExpressionValueTests.cs, test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/EqualsTests.cs, test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/list/list.json, test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/lookup-list.json, test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/lookup-list.json
ExpressionValueTests expanded with JSON array parsing cases (empty, numeric, nested, mixed-type) and asserts that accessing .Array on Undefined/Null throws InvalidCastException. EqualsTests removes unsupported object[]/byte[] nonsense entries. New fixtures exercise list creation syntax, heterogeneous and nested lists, and lookups via data model/component expressions.
Test utilities: DynamicClassBuilder
test/Altinn.App.Core.Tests/LayoutExpressions/TestUtilities/DynamicClassBuilder.cs
DynamicClassBuilder updated to return object for empty or heterogeneous JSON arrays and adds HasDifferentTypes(JsonElement) helper.
Public API surface verification
test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt
Public API snapshot records the new ExpressionValue constructor, Array property, implicit conversion operator, and list function enum member.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • ivarne
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Support lists in expressions' directly and clearly describes the main change: adding array/list support to the expression system. It matches the PR's primary objective of enabling list functionality in expressions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch support-lists-in-expressions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@TomasEng TomasEng force-pushed the support-lists-in-expressions branch 2 times, most recently from 51411b4 to 3b4b94b Compare May 27, 2026 08:17
@TomasEng TomasEng force-pushed the support-lists-in-expressions branch from 3b4b94b to e084500 Compare May 27, 2026 08:27
@TomasEng TomasEng added backport-ignore This PR is a new feature and should not be cherry-picked onto release branches feature Label Pull requests with new features. Used when generation releasenotes labels May 27, 2026
@TomasEng
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs (1)

651-654: ⚡ Quick win

Deserialize array directly to avoid an extra allocation.

Line 652 deserializes to List<ExpressionValue> and immediately copies with ToArray(). Deserializing directly to ExpressionValue[] avoids one allocation/copy.

Suggested refactor
-        var values =
-            JsonSerializer.Deserialize<List<ExpressionValue>>(ref reader, options)
-            ?? throw new JsonException("Expected EndArray token.");
-        return new ExpressionValue(values.ToArray());
+        var values =
+            JsonSerializer.Deserialize<ExpressionValue[]>(ref reader, options)
+            ?? throw new JsonException("Expected EndArray token.");
+        return new ExpressionValue(values);

As per coding guidelines: "Write efficient code - don't allocate unnecessarily (e.g., avoid calling ToString twice, prefer for loops over LINQ when appropriate)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs` around lines 651
- 654, The code in ExpressionValue.cs deserializes into List<ExpressionValue>
then calls ToArray(), causing an extra allocation; change the
JsonSerializer.Deserialize call to deserialize directly into ExpressionValue[]
(i.e., JsonSerializer.Deserialize<ExpressionValue[]>(ref reader, options)) and
keep the null-check/JsonException behavior and the return new
ExpressionValue(array) using the deserialized array; update the local variable
name (e.g., values) as needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs`:
- Around line 94-98: Constructor ExpressionValue(ExpressionValue[] value) stores
the caller's array reference into the private field _arrayValue and the array is
also exposed directly elsewhere (when ValueKind == JsonValueKind.Array),
allowing external mutation or a null value to leave the struct in an
inconsistent state; fix by null-guarding and defensively copying: in the
constructor set _arrayValue = (value == null) ? Array.Empty<ExpressionValue>() :
(ExpressionValue[])value.Clone() and ensure ValueKind is set to
JsonValueKind.Array only when the (non-null) copy is assigned, and in the public
accessor that currently exposes the internal array (the member that returns the
array when ValueKind == JsonValueKind.Array) return a copy (e.g.,
_arrayValue.Length == 0 ? Array.Empty<ExpressionValue>() :
(ExpressionValue[])_arrayValue.Clone()) instead of returning the internal
_arrayValue reference.

---

Nitpick comments:
In `@src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs`:
- Around line 651-654: The code in ExpressionValue.cs deserializes into
List<ExpressionValue> then calls ToArray(), causing an extra allocation; change
the JsonSerializer.Deserialize call to deserialize directly into
ExpressionValue[] (i.e., JsonSerializer.Deserialize<ExpressionValue[]>(ref
reader, options)) and keep the null-check/JsonException behavior and the return
new ExpressionValue(array) using the deserialized array; update the local
variable name (e.g., values) as needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 08d275f1-888a-497c-9f83-05b7c9a6e7ee

📥 Commits

Reviewing files that changed from the base of the PR and between 10da3b0 and e084500.

📒 Files selected for processing (15)
  • src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs
  • src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs
  • src/Altinn.App.Core/Models/Expressions/ExpressionFunction.cs
  • test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ExpressionTestCaseRoot.cs
  • test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestBackendExclusiveFunctions.cs
  • test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs
  • test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestInvalid.cs
  • test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/lookup-list.json
  • test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/array-is-null.json
  • test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/lookup-list.json
  • test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/list/list.json
  • test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/EqualsTests.cs
  • test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/ExpressionValueTests.cs
  • test/Altinn.App.Core.Tests/LayoutExpressions/TestUtilities/DynamicClassBuilder.cs
  • test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt
💤 Files with no reviewable changes (2)
  • test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/array-is-null.json
  • test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/EqualsTests.cs

Comment thread src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs
@TomasEng
Copy link
Copy Markdown
Contributor Author

@coderabbitai

Deserialize array directly to avoid an extra allocation.

Fixed.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

Tip

For best results, initiate chat on the files or code changes.

@TomasEng Thanks for the fix!

🐇 ✨


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@TomasEng
Copy link
Copy Markdown
Contributor Author

The Sonar errors on this pull request seem to be related to existing code only.

@TomasEng TomasEng added the squad/data Issues that belongs to the named squad. label May 27, 2026
@TomasEng TomasEng moved this to 🔎 In review in Team Altinn Studio May 27, 2026
@TomasEng TomasEng marked this pull request as ready for review May 27, 2026 09:57
@TomasEng TomasEng mentioned this pull request May 29, 2026
4 tasks
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs (1)

199-210: ⚠️ Potential issue | 🔴 Critical

Fix/clarify ExpressionValue.ToObject() array return type contract

ExpressionValue.ToObject() maps JsonValueKind.Array to the Array property, and Array is typed as ExpressionValue[], so ToObject() returns the internal wrapper array (not an object?[] of unwrapped CLR element values). Callers that need CLR-shaped arrays must explicitly unwrap elements (e.g., via ExpressionValue[]ToObject() per element), or this behavior needs to be clearly documented as the intended API contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs` around lines 199
- 210, The ToObject() method currently returns the internal ExpressionValue[]
wrapper for JsonValueKind.Array via the Array property, which leaks internal
wrappers; change the JsonValueKind.Array branch in ExpressionValue.ToObject() to
return a CLR object?[] by unwrapping each element (call
ExpressionValue.ToObject() on each item of the Array property and produce an
object?[] result) so callers get a native array of element values rather than
ExpressionValue instances; ensure you still return null/primitive CLR types for
other kinds and keep the method signature as object?.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs`:
- Around line 199-210: The ToObject() method currently returns the internal
ExpressionValue[] wrapper for JsonValueKind.Array via the Array property, which
leaks internal wrappers; change the JsonValueKind.Array branch in
ExpressionValue.ToObject() to return a CLR object?[] by unwrapping each element
(call ExpressionValue.ToObject() on each item of the Array property and produce
an object?[] result) so callers get a native array of element values rather than
ExpressionValue instances; ensure you still return null/primitive CLR types for
other kinds and keep the method signature as object?.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: df09f1b0-d9ab-443c-9b17-dc4762959fcf

📥 Commits

Reviewing files that changed from the base of the PR and between b58a3af and 739cc4e.

📒 Files selected for processing (1)
  • src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
B Maintainability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

await ExpressionEvaluator.EvaluateExpression(
state,
test.Expression,
(Expression)test.Expression!,
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 think these should be test.Expression.Value

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-ignore This PR is a new feature and should not be cherry-picked onto release branches feature Label Pull requests with new features. Used when generation releasenotes squad/data Issues that belongs to the named squad.

Projects

Status: 🔎 In review

Development

Successfully merging this pull request may close these issues.

2 participants