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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

## Fixes

- Issue #23: explicit omitted-`Source` converters receive the root object before automatic source-member matching in new-object, constructor-bound, and update mappings. Unrelated source names and patch getters no longer override that selection. [#23](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/23)
- Issue #22: generated nested and collection helpers preserve the calling mapper instance for stateful converters and mappings, while instance-free helpers remain static. Static entry methods no longer select instance-only mappings. [#22](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/22)
- Issue #21: nested and collection helpers retain each entry method's effective mapping policies and nullable shape. Equivalent recursive helpers remain shared while preserving the initiating entry's cycle metadata. [#21](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/21)
- Issue #20: release validation and each NuGet package push now stop immediately on a nonzero native exit, reporting the failed operation or package before later successes can conceal it. [#20](https://github.com/PrimordialCode/Mammoth.LiteMapper/issues/20)
Expand Down
7 changes: 7 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Mammoth.LiteMapper Decision Log

## Issue #23: resolve explicit root input before member matching (2026-10-08)

- Context: new-object planning rejected absent source-member matches before resolving a root converter. Constructor/update planning also allowed automatic ambiguity or patch captures to override the explicit selection.
- Decision: use the declared root source parameter as the selected symbol whenever `Use` is explicit and `Source` is omitted. Configured source paths keep their existing symbol selection; automatic matching runs only without either explicit selection. Root parameters never receive member-level patch guards, since the mapping entry already handles root nulls.
- Contract: sections 5.7, 9.7, 10.3, 11.1, 11.4, 12.5, and 28.2 already require these semantics. Converter signatures, result nullability, source-path boundaries, and exception identity continue through the existing resolver. No public API or normative change is needed.
- Evidence: test-first failures cover missing and case-ambiguous names, constructor/update counterparts, relaxed unmapped policies, invalid converter signatures, and an unrelated nullable patch getter. Tests verify root identity and one invocation; nullable-root guards and explicit external source-path conversion are covered separately.

## Issue #22: propagate instance requirements without changing call identity (2026-10-08)

- Context: nested and collection planners legally selected instance converters and mappings, but non-core helpers were emitted static unconditionally.
Expand Down
8 changes: 8 additions & 0 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Mammoth.LiteMapper Implementation Plan

## Issue #23: explicit root-source converter priority (2026-10-08)

- Scope: select omitted-`Source` explicit `Use` configuration before automatic source-member matching in new-object, constructor-bound, and existing-target planning. Keep source-path selection and converter validation intact.
- Test-first evidence: the initial 16 cases on unchanged develop produced 11 failures and five controls, zero skips. The Basic sample independently failed with LITEMAPPER1002 and CS8795. A later nullable-root patch case failed with CS1061 before the root-parameter guard was corrected; the explicit-path control passed.
- Implementation: reuse the root parameter as the selected source symbol at the three planning boundaries. Patch member guards exclude that parameter because root null handling is emitted separately. Existing named-converter resolution, public signatures, and target obligations are unchanged.
- Validation: focused/full generator tests, runtime and integration tests, source samples, available clean-package/matrix/API/benchmark checks, independent review, and exact-head ordinary PR CI. Record local environment gaps separately from CI.
- Boundaries: issue #23 only; no specification semantics or public API change, other issue, user-computer access, merge, release, tag, publication, workflow dispatch, tool install, or credential change. Stop at draft PR/human review.

## Issue #22: instance context in generated helpers (2026-10-08)

- Scope: preserve the calling mapper instance through nested and collection helpers that depend on instance converters or mappings. Keep instance-free helpers static and exclude instance-only candidates from static entry methods.
Expand Down
9 changes: 9 additions & 0 deletions STATUS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Mammoth.LiteMapper Status

## GitHub issue #23 root converter priority checkpoint (2026-10-08)

- Base: isolated cloud worktree from develop d80ff4cc6a5e515896b6054a454a1ee2910450a5 after PR #36 merged. Issue #23 is open with no dependencies/sub-issues or competing PR. The user requested the next issue; exec:agent and ready:agent were added and read back, preserving existing labels. AgentStack CLI is absent; authorized connector fallback is used without issue comments or claim writes.
- Red-first: initial 16-case fixture produced 11 failures and five controls, zero skips, on unchanged develop. The Basic sample separately failed with LITEMAPPER1002/CS8795. A nullable-root patch extension failed with CS1061 on the initial candidate before the member-guard fix; an explicit external source-path control passed.
- Implementation: explicit omitted-Source converters select the root parameter before automatic source matching in create, constructor, and update planning. Patch guards exclude root parameters. Basic and clean-package consumers exercise a root converter with no same-name source member; usage and changelog are synchronized.
- Final local validation: Release solution build has zero warnings/errors. All 652 generator tests pass, including 18 new cases; runtime five and integration one pass, zero failures/skips. All three source sample smoke runs pass. Available packaging suite: 220 passed, two infrastructure failures, zero skips, including all 12 framework/language combinations, clean package-backed samples, package/API checks, usage validation, and benchmark smoke. Generated Basic source was inspected and its root converter is invoked once with the root parameter. Independent read-only review found no scoped correctness blocker; CRLF-aware diff and consumer-skill validation pass. Exact-head ordinary CI belongs in the draft PR.
- Environment limits: PowerShell and clang are unavailable; 49 PowerShell-dependent cases were not run locally. Trimming and Native AOT fail at ILLink ComputeManagedAssemblies task-host creation (MSB4216 / Unix-domain SocketException 13), before publish behavior is established. No assertion, dependency, warning, or prerequisite was weakened. An earlier packaging attempt was interrupted by a tool approval cancellation and produced no final TRX; the completed retry above is the local evidence. No local trimming/AOT success is claimed.
- Boundary: issue #23 only, then human review. No user-computer access, merge, release, tag push, package publication, workflow dispatch, installation, or credential change.

## GitHub issue #22 instance helper context checkpoint (2026-10-08)

- Base: isolated cloud worktree from develop d541affbe7ca172a47ec72affeb8587955fdf6f7 after PR #35 merged. The user explicitly requested the next issue; exec:agent and ready:agent were added and read back. GitHub dependency and sub-issue reads returned empty. AgentStack CLI is absent; authorized GitHub fallback is used without issue comments or claim writes.
Expand Down
11 changes: 11 additions & 0 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,17 @@ Member candidates are public instance properties and fields. Hidden members are

Source paths in `[MapProperty]` may use dotted member paths such as `"Email.Value"`. Source paths must resolve to members and cannot use method-call syntax. Source methods are not discovered automatically; use a converter or root-source `MapProperty.Use` method when a method-derived value is needed.

Omitting `Source` with an explicit `Use` passes the complete root object to that converter. This selection takes priority over automatic name matching, so an unrelated source member with the target name is not read and no same-name source member is required. The same rule applies to constructor-bound members and existing-target mappings, including patch mode.

The [Basic sample](../samples/Mammoth.LiteMapper.Samples.Basic/Program.cs) maps `StaticSource.Name == "Ada"` to `StaticTarget.DisplayName == "Name: Ada"`:

```csharp
[MapProperty(Target = nameof(StaticTarget.DisplayName), Use = nameof(BuildDisplayName))]
public static partial StaticTarget Map(StaticSource source);

private static string BuildDisplayName(StaticSource source) => "Name: " + source.Name;
```

Nullable source paths are evaluated once per segment. When an explicit converter is selected, its parameter annotation controls traversal nulls. For example:

```csharp
Expand Down
7 changes: 6 additions & 1 deletion samples/Mammoth.LiteMapper.Samples.Basic/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ private static void Main()
new ChildSource { Value = 2 },
},
});
if (staticTarget.Name != "Ada" || staticTarget.Children.Count != 2 || staticTarget.Children[1].Value != 2)
if (staticTarget.Name != "Ada" || staticTarget.DisplayName != "Name: Ada" || staticTarget.Children.Count != 2 || staticTarget.Children[1].Value != 2)
{
throw new InvalidOperationException("Static mapping failed.");
}
Expand Down Expand Up @@ -53,7 +53,10 @@ private static void Main()
[LiteMapper]
public static partial class StaticMapper
{
[MapProperty(Target = nameof(StaticTarget.DisplayName), Use = nameof(BuildDisplayName))]
public static partial StaticTarget Map(StaticSource source);

private static string BuildDisplayName(StaticSource source) => "Name: " + source.Name;
}

[LiteMapper]
Expand Down Expand Up @@ -84,6 +87,8 @@ public sealed class StaticSource

public sealed class StaticTarget
{
public string DisplayName { get; set; } = string.Empty;

public string? Name { get; set; }

public List<ChildTarget> Children { get; set; } = new List<ChildTarget>();
Expand Down
19 changes: 10 additions & 9 deletions src/Mammoth.LiteMapper.Generator/LiteMapperGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,9 @@ private static MappingModel CreateMappingModel(IMethodSymbol method, Compilation

var explicitConfiguration = explicitConfigurations.FirstOrDefault(c => c.TargetName == targetMember.Name);
var selected = explicitConfiguration == null ? null : explicitConfiguration.SelectedSource;
var match = selected == null ? MatchSource(targetMember, sourceMembers, options.NameMatching) : new MatchResult(selected.SourceMember, false);
var match = selected != null ? new MatchResult(selected.SourceMember, false)
: explicitConfiguration?.Use != null ? new MatchResult(method.Parameters[0], false)
: MatchSource(targetMember, sourceMembers, options.NameMatching);
if (match.Ambiguous)
{
diagnostics.Add(Diagnostic.Create(Diagnostics.AmbiguousMemberMatch, targetMember.Locations.FirstOrDefault() ?? location, targetMember.Name));
Expand Down Expand Up @@ -706,18 +708,15 @@ private static MappingModel CreateUpdateMappingModel(IMethodSymbol method, Compi

var explicitConfiguration = explicitConfigurations.FirstOrDefault(c => c.TargetName == targetMember.Name);
var selected = explicitConfiguration == null ? null : explicitConfiguration.SelectedSource;
var match = selected == null ? MatchSource(targetMember, sourceMembers, options.NameMatching) : new MatchResult(selected.SourceMember, false);
var match = selected != null ? new MatchResult(selected.SourceMember, false)
: explicitConfiguration?.Use != null ? new MatchResult(method.Parameters[0], false)
: MatchSource(targetMember, sourceMembers, options.NameMatching);
if (match.Ambiguous)
{
diagnostics.Add(Diagnostic.Create(Diagnostics.AmbiguousMemberMatch, targetMember.Locations.FirstOrDefault() ?? location, targetMember.Name));
continue;
}

if (match.Member == null && explicitConfiguration?.Use != null && selected == null)
{
match = new MatchResult(method.Parameters[0], false);
}

if (match.Member == null)
{
ReportUnmappedTarget(targetMember, options.UnmappedTargetMembers, diagnostics);
Expand Down Expand Up @@ -3020,7 +3019,7 @@ private static string BuildNullSafeSourcePathExpression(string parameterName, So
return string.Join(" && ", sourcePath.NullCheckExpressions.Select(p => escapedParameterName + "." + EscapeMemberPath(p) + " != null"));
}

return SourceMayBeNull(sourceMember)
return !(sourceMember is IParameterSymbol) && SourceMayBeNull(sourceMember)
? EscapeIdentifier(parameterName) + "." + EscapeIdentifier(sourceMember.Name) + " != null"
: null;
}
Expand Down Expand Up @@ -3402,7 +3401,9 @@ private static MatchResult MatchSource(ISymbol targetMember, ISymbol[] sourceMem
}

var configuration = defaultTargetMatch.Member == null ? null : configurations.FirstOrDefault(c => c.TargetName == defaultTargetMatch.Member.Name);
var match = configuration?.SelectedSource != null ? new MatchResult(configuration.SelectedSource.SourceMember, false) : MatchParameter(parameter, sourceMembers, nameMatching);
var match = configuration?.SelectedSource != null ? new MatchResult(configuration.SelectedSource.SourceMember, false)
: configuration?.Use != null ? new MatchResult(method.Parameters[0], false)
: MatchParameter(parameter, sourceMembers, nameMatching);
if (match.Ambiguous)
{
diagnostics.Add(Diagnostic.Create(Diagnostics.AmbiguousMemberMatch, parameter.Locations.FirstOrDefault(), parameter.Name));
Expand Down
Loading
Loading