Skip to content

[patch] Read an input pin's default from its C# initializer - #442

Merged
matt-edmondson merged 2 commits into
mainfrom
claude/nodeeditor-439-initializer-defaults
Sep 23, 2026
Merged

matt-edmondson merged 2 commits into
mainfrom
claude/nodeeditor-439-initializer-defaults

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #439

What was wrong

PinDefinition.DefaultValue was populated only from an explicit [InputPin(DefaultValue = ...)], never from the property's C# initializer. The initializer form is what NodeGraph/Library uses throughout — RandomNode declares Min { get; set; } = 0.0, CounterNode declares IncrementBy { get; set; } = 1 — so GetAllNodeDefinitions() reported null for every default in the shipped library, and a menu or inspector built from it had to invent values that would not match what the node holds when it runs.

Method parameters and constructor parameters already fell back to parameter.DefaultValue. Properties and fields had no equivalent.

The change

ScanPins now builds a prototype instance of the declaring type and reads the member off it, which is what the initializer wrote. It is used only where the attribute supplied nothing, so an explicit DefaultValue still wins — the same precedence AddParameterPins already applies.

The prototype is a Lazy<object?>, so it is constructed at most once per type and only when a pin actually needs it. A type that is abstract, open-generic, or has no parameterless constructor is not constructed at all, and a constructor that throws is caught: registration is metadata only, so those cases report no default exactly as before.

One consequence worth naming: a value-type pin with neither an initializer nor an attribute now reports its zero rather than null. That is what the node will actually hold once constructed, which is the point of the field.

Tests

Four tests in AttributeBasedNodeFactoryTests, against a new InitializedDefaultsNode fixture that mirrors how the library writes its defaults:

  • ReadsAnInputDefaultFromItsPropertyInitializer — property and field initializers both come through
  • PrefersTheAttributeDefaultOverTheInitializer — precedence
  • ReportsTheZeroForAnUninitializedValueTypePin — pins the consequence above
  • SurvivesATypeItCannotConstruct — a throwing constructor and a type with only a parameterised constructor both still register

The first and third fail on main (expected: 128, actual: null and expected: 0, actual: null); verified by reverting the production change and re-running before finalising.

tests/ImGui.NodeEditor.Tests 102/102 and tests/NodeGraph.Tests 106/106 pass on this branch, on .NET 10.0.401 / Linux.

🤖 Generated with Claude Code

https://claude.ai/code/session_01P6bYWX7oFj1YTAAw9M516w


Generated by Claude Code

PinDefinition.DefaultValue was populated only from an explicit
[InputPin(DefaultValue = ...)]. Every default in the shipped node library
is written as a property initializer instead, so GetAllNodeDefinitions()
reported null for all of them and any menu or inspector built from the
definitions had to invent its own, which would not match what the node
holds when it runs.

ScanPins now falls back to reading the member off a prototype instance of
the declaring type, which is the value the initializer wrote. The
attribute still wins where it supplied one, matching what parameter and
constructor-parameter pins already did. The prototype is built lazily,
once per type, and a type that cannot be constructed without arguments or
whose constructor throws simply reports no default, as before.

Fixes #439

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6bYWX7oFj1YTAAw9M516w
…cannot fire

SonarCloud's quality gate failed the branch at 68.2% coverage on new code
against an 80% floor. Collecting cobertura locally named the gaps exactly:
the TargetInvocationException arm in ReadDeclaredDefault, the
!constructible early return in CreatePrototype, and both of the extra
catch arms.

Two of those were dead rather than untested. CreatePrototype's guard turns
away abstract and open-generic types and reference types with no public
parameterless constructor before Activator.CreateInstance is reached,
which is where MissingMethodException and MemberAccessException would have
come from, and NotSupportedException only arises for types that cannot
carry a [Node] attribute in the first place. Removing the two catches
leaves the one case the guard lets through: a constructor that runs and
throws. The reasoning is now in a remarks block rather than implied.

The other two were real paths with no test. The cannot-construct test used
ConstructedNode, whose pins are all outputs, so it never forced the
prototype at all and its assertion was vacuous; it now uses a node with an
input pin and a constructor argument, which reaches the early return.
TouchyGetterNode covers the throwing getter.

New-code coverage in the file is 26/26 lines measured, 0 uncovered.
103/103 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6bYWX7oFj1YTAAw9M516w
@sonarqubecloud

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PinDefinition.DefaultValue ignores C# property initializers, so the shipped node library's defaults are invisible

2 participants