Why
Two of the three PC0029 false negatives in #544 had one root cause that has nothing to do with receivers: the table was declared without a keys section, and PC0029 decided key membership from ITableTypeSymbol.Keys. That collection never contains the compiler-synthesized primary key. PC0029 got an inline fix in #544; this issue asks to audit the rest of the rule set and to document the SDK behaviour once.
SDK facts (decompiled Microsoft.Dynamics.Nav.CodeAnalysis, identical in net8.0 and net10.0)
TableTypeSymbol.GetPrimaryKey() (Symbols/TableTypeSymbol.cs) returns the first declared key or, when the table declares none, a SynthesizedKeySymbol over the lowest-Id non-synthesized field whose type is a valid key type. Not literally "the first field": a field with an invalid key type is skipped.
SynthesizedKeySymbol has IsSynthesized == true, Location == null, DeclaringSyntaxReference == null, no properties, and Fields populated with that one field.
ITableTypeSymbol.PrimaryKey returns it. ITableTypeSymbol.Keys is declaration-only (SourceTableTypeSymbol.GetKeys collects SymbolKind.Key members) and is empty for such a table. The same holds for tables loaded from a referenced .app: SerializableSymbolModelConverter.ConvertKeys serializes declared key members only.
- Consequence: any key-membership or index logic built on
Keys is blind to tables without a keys section, in the current app and in every dependency.
What to do
- Grep every analyzer and code fix for
.Keys, IKeySymbol, KeyListSyntax, KeySyntax and key-related properties (Clustered, Unique, IncludedFields, SumIndexFields, MaintainSiftIndex) and decide per rule whether the synthesized primary key must participate. Read PrimaryKey where it must; the synthesized key is recognisable by IsSynthesized and a null Location when a rule needs to treat it differently (it has no syntax to report on).
- Add a fixture whose table has no
keys section to every rule that reads keys (.claude/rules/testing.md rule 6 now requires it), including rules that already read PrimaryKey, so the behaviour is pinned rather than assumed.
- Consider a small Common helper that yields
PrimaryKey followed by the declared Keys without duplicates, if the audit finds more than one consumer.
Background and reviewer rows: .claude/rules/receiver-forms.md (section "Table shape: the implicit primary key"), REVIEW.md house rules.
Upstream note (material for a Microsoft issue)
Microsoft's own cops share the gap: Rule210SuboptimalIndex (Microsoft.Dynamics.Nav.CodeCop.Design) collects table.Keys only, Rule222SIFTIndexShouldNotBeUsedForPrimaryAndUniqueKey bails out when Keys.IsDefaultOrEmpty, and TableFieldsAnalyzer reads Keys as well, so a table without a keys section is invisible to all three. Rule0242PartialRecordsDetectJitLoads reads PrimaryKey.Fields and is correct. This could be reported to Microsoft as a CodeCop gap.
Related: #544 (PC0029 fix), and #545 (receiver-form audit).
Why
Two of the three PC0029 false negatives in #544 had one root cause that has nothing to do with receivers: the table was declared without a
keyssection, and PC0029 decided key membership fromITableTypeSymbol.Keys. That collection never contains the compiler-synthesized primary key. PC0029 got an inline fix in #544; this issue asks to audit the rest of the rule set and to document the SDK behaviour once.SDK facts (decompiled
Microsoft.Dynamics.Nav.CodeAnalysis, identical in net8.0 and net10.0)TableTypeSymbol.GetPrimaryKey()(Symbols/TableTypeSymbol.cs) returns the first declared key or, when the table declares none, aSynthesizedKeySymbolover the lowest-Id non-synthesized field whose type is a valid key type. Not literally "the first field": a field with an invalid key type is skipped.SynthesizedKeySymbolhasIsSynthesized == true,Location == null,DeclaringSyntaxReference == null, no properties, andFieldspopulated with that one field.ITableTypeSymbol.PrimaryKeyreturns it.ITableTypeSymbol.Keysis declaration-only (SourceTableTypeSymbol.GetKeyscollectsSymbolKind.Keymembers) and is empty for such a table. The same holds for tables loaded from a referenced.app:SerializableSymbolModelConverter.ConvertKeysserializes declared key members only.Keysis blind to tables without akeyssection, in the current app and in every dependency.What to do
.Keys,IKeySymbol,KeyListSyntax,KeySyntaxand key-related properties (Clustered,Unique,IncludedFields,SumIndexFields,MaintainSiftIndex) and decide per rule whether the synthesized primary key must participate. ReadPrimaryKeywhere it must; the synthesized key is recognisable byIsSynthesizedand a nullLocationwhen a rule needs to treat it differently (it has no syntax to report on).keyssection to every rule that reads keys (.claude/rules/testing.mdrule 6 now requires it), including rules that already readPrimaryKey, so the behaviour is pinned rather than assumed.PrimaryKeyfollowed by the declaredKeyswithout duplicates, if the audit finds more than one consumer.Background and reviewer rows:
.claude/rules/receiver-forms.md(section "Table shape: the implicit primary key"),REVIEW.mdhouse rules.Upstream note (material for a Microsoft issue)
Microsoft's own cops share the gap:
Rule210SuboptimalIndex(Microsoft.Dynamics.Nav.CodeCop.Design) collectstable.Keysonly,Rule222SIFTIndexShouldNotBeUsedForPrimaryAndUniqueKeybails out whenKeys.IsDefaultOrEmpty, andTableFieldsAnalyzerreadsKeysas well, so a table without akeyssection is invisible to all three.Rule0242PartialRecordsDetectJitLoadsreadsPrimaryKey.Fieldsand is correct. This could be reported to Microsoft as a CodeCop gap.Related: #544 (PC0029 fix), and #545 (receiver-form audit).