Skip to content

feat: CMTK0009 reports the side of an outer join with no match - #278

Open
CaffeinatedCoder wants to merge 5 commits into
release/2.4.0from
feat/cmtk0009-outer-joins
Open

CaffeinatedCoder wants to merge 5 commits into
release/2.4.0from
feat/cmtk0009-outer-joins

Conversation

@CaffeinatedCoder

Copy link
Copy Markdown
Owner

Closes #261

Problem

LeftJoin and RightJoin (.NET 10), and .NET 11's FullJoin, hand their result selector (or their tuple) the default of the side with no match. Over a value object, Result or [RequireCustomInitialization] struct that is an instance no factory made, and nothing reported it.

Change

  • CMTK0009 (DefaultProducingCallAnalyzer) reports every overload of LeftJoin whose inner element, RightJoin whose outer element, and FullJoin where either element is a no-default struct, a warning per side, on Enumerable, Queryable and AsyncEnumerable. That includes .NET 11's tuple-returning overloads. The message names an Option of each element in memory (inner.Select(Option.Some)) and a nullable projection in a query (inner.Select(x => (OrderId?)x)). Warning, no code fix.
  • An Option element of a join is not reported. Its default is None, and the advised rewrite produces exactly that element type, so reporting it would flag the way out. Recorded on CMTK0009: report LeftJoin, RightJoin and FullJoin whose unmatched side is a no-default struct #261, in the README and in design/analyzers.md.
  • A join that does not bind (a lambda reading a same-project .Value, as before weaving) is read from its candidates: the elements from the sequences passed, and the candidate whose outer sequence the receiver is (Queryable's for a query, which the first test run caught giving in-memory advice).
  • LinqJoins (new, internal): resolves the joins by symbol, maps arguments to parameters, and ranks candidates. CMTK0008's join check now uses it in place of its own copy (no behaviour change; its tests pass).
  • Docs: the Analyzers README's CMTK0009 rows, design/analyzers.md, a changelog line in 2.4.0's "Changed" (warnings-as-errors builds can newly fail), claims K09-* in test/oracles/claims.json, and KB-059. KB-059 records the .NET 11 join surface, and that EF Core 11.0.0-rc.3.26522.148 translates the nullable projection under FULL JOIN on SQLite and throws without it. That was measured in a scratch net11.0 project outside the solution, built by the 11.0.100-rc.1 SDK and run on the 11.0.0-rc.1 runtime, the only ones on this machine.

Tests

  • OuterJoinDefaultTests: a row per method, host and side, with and without a comparer, async key selectors, static and named forms, conditional access, and the unbound form. Controls: the side a join never fills, a class element, a nullable element, an ordinary struct, an Option element, both ways out, Join and GroupJoin. A theory over TypeArgumentKinds (79 kinds) uses NoDefaultKindTests.HasNoDefault as the oracle. FullJoin and the tuple forms compile against the real Microsoft.NETCore.App.Ref 11.0.0-rc.3.26522.148 through the verifier's ReferenceAssemblies, resolved from nuget.org as Net100 is; no stub.
  • Earlier, end to end against the RC 1 SDK's own reference assemblies (a scratch net11.0 project with the built analyzer): the same forms reported and both ways out stayed silent.
  • Verify the guard: each seed fails its tests and nothing else:
    • rule unregistered: 9 failures (4 positive facts plus the 5 no-default kinds);
    • unbound path dropped: the candidates test;
    • candidate ranking dropped: the candidates test;
    • Option exemption dropped: the silent test;
    • LeftJoin side swapped: 10 failures;
    • conditional access: dropping the name form fails both conditional rows, dropping the receiver fails the unbound one.
  • Weaving review: discovery is by symbol, resolved once per compilation, and nothing is registered without TypeKit. It found conditional access (rows?.LeftJoin(…)) silent, now fixed with a row. A join reached as a method group converted to a delegate has no invocation and isn't reported.
  • Analyzer tests 4114/4114 and the full solution through tools/BuildVerdict (SQL Server tests on azure-sql-edge, as AGENTS says for an arm64 Mac).

Follow-up: #273 (CMTK0008 doesn't list FullJoin among its joins).

🤖 Generated with Claude Code

CaffeinatedCoder and others added 3 commits October 31, 2026 21:01
LinqJoins resolves Join, GroupJoin, LeftJoin, RightJoin and FullJoin of
Enumerable, Queryable and AsyncEnumerable by symbol, maps a call's
arguments to the definition's parameters (receiver, position, name,
conditional access), and, where a call does not bind, takes the
candidate whose outer sequence the receiver is, the most derived one,
as overload resolution would. CMTK0008's key check uses it in place of
its own copy, with no change in what it reports; CMTK0009's outer
joins (#261) build on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LeftJoin and RightJoin (.NET 10) and FullJoin (.NET 11) hand their
result selector, or their tuple, the default of the side with no
match. CMTK0009 now reports every overload of LeftJoin whose inner
element, RightJoin whose outer element, and FullJoin where either
element is a no-default struct, a warning per side, on Enumerable,
Queryable and AsyncEnumerable, the .NET 11 tuple-returning overloads
included. The message names an Option of each element in memory and a
nullable projection in a query. Warning, no code fix.

An Option element of a join is not reported: its default is None,
which is the way out the message names, and reporting it would flag
that rewrite in turn. A join that does not bind, because a lambda reads
a generated member before weaving, is read from its candidates.

Input class: the receiver and arguments of a LINQ join call, and what
its element type is. Catalogues: the call forms (extension, static,
named, conditional access, unbound, a list, an array, a query, a
sequence-constrained type parameter, sync and async key selectors,
with and without a comparer, tuple forms) on each host and side, and
TypeArgumentKinds, 79 kinds, against NoDefaultKindTests' oracle. The
FullJoin rows compile against Microsoft.NETCore.App.Ref
11.0.0-rc.3.26522.148. Siblings found and fixed here: the unbound call
on a query took Enumerable's candidate and gave the in-memory advice,
and conditional access was silent. CMTK0008 not listing FullJoin is
#273.

Closes #261

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…JOIN

The .NET 11 join surface CMTK0009 resolves, and EF Core
11.0.0-rc.3.26522.148 on SQLite: a nullable projection under FullJoin
is a FULL JOIN reading null for the side with no match, and without it
EF throws "Nullable object must have a value." Measured in a scratch
net11.0 project outside the solution (11.0.100-rc.1 SDK and runtime,
the only ones installed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CaffeinatedCoder CaffeinatedCoder added this to the 2.4.0 milestone Oct 31, 2026
@CaffeinatedCoder CaffeinatedCoder added enhancement New feature or request package/analyzers Affects CodoMetis.TypeKit.Analyzers area/value-objects Value-object contracts and what the aspects generate for them labels Oct 31, 2026
CaffeinatedCoder and others added 2 commits October 31, 2026 21:16
Brings #271 (CMTK0005 SetCount) and #274 (CI tests by reach). Both
changelog lines are kept, CMTK0005's before CMTK0009's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings #277 (the RC3 spike, KB-060). Both knowledge index rows are
kept, KB-059 before KB-060.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

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

Labels

area/value-objects Value-object contracts and what the aspects generate for them enhancement New feature or request package/analyzers Affects CodoMetis.TypeKit.Analyzers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant