feat: resolve $dynamicRef generic bindings with per-context classes - #7978
feat: resolve $dynamicRef generic bindings with per-context classes#7978aqeelat wants to merge 3 commits into
Conversation
5a95ec9 to
2dff270
Compare
2dff270 to
7465ee3
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (3)
tests/Kiota.Builder.IntegrationTests/GenerateSample.cs:548
- These assertions only guard against an unsuffixed
TreeTemplatein languages that emit theclasskeyword. For Go, an unsuffixed template would be emitted astype TreeTemplate struct, which isn’t currently checked.
Assert.DoesNotContain("class TreeTemplate\n", allModelText, StringComparison.Ordinal);
Assert.DoesNotContain("class TreeTemplate ", allModelText, StringComparison.Ordinal);
Assert.DoesNotContain("class TreeTemplate:", allModelText, StringComparison.Ordinal);
tests/Kiota.Builder.IntegrationTests/GenerateSample.cs:575
- These assertions only check for
class SearchTemplate..., which won’t catch an unsuffixed Go declaration (type SearchTemplate struct). Adding the Go-specific pattern makes the per-language guarantee more robust.
Assert.DoesNotContain("class SearchTemplate\n", allModelText, StringComparison.Ordinal);
Assert.DoesNotContain("class SearchTemplate ", allModelText, StringComparison.Ordinal);
Assert.DoesNotContain("class SearchTemplate:", allModelText, StringComparison.Ordinal);
tests/Kiota.Builder.IntegrationTests/GenerateSample.cs:603
- Same issue as other template checks:
class EnvelopeTemplate...won’t detect an unsuffixed Go model (type EnvelopeTemplate struct).
Assert.DoesNotContain("class EnvelopeTemplate\n", allModelText, StringComparison.Ordinal);
Assert.DoesNotContain("class EnvelopeTemplate ", allModelText, StringComparison.Ordinal);
Assert.DoesNotContain("class EnvelopeTemplate:", allModelText, StringComparison.Ordinal);
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
tests/Kiota.Builder.Tests/KiotaBuilderDynamicRefTests.cs:16
KiotaBuilderis referenced without importing theKiota.Buildernamespace, so this new test file won’t compile (unless a global using exists, which it doesn’t in this test project). Addusing Kiota.Builder;or fully-qualify the type.
public void ExtractAnchorNameReturnsExpectedValue(string dynamicRef, string expected)
{
Assert.Equal(expected, KiotaBuilder.ExtractAnchorName(dynamicRef));
}
a15bbd1 to
bbc5397
Compare
…classes test(builder): expand dynamic-ref coverage and fix review items
bbc5397 to
354fc0d
Compare
354fc0d to
6772aaf
Compare
cd5a1de to
f9adc5c
Compare
|
The failing tests were flaky. I'm attempting to fix them in #8005 |
f9adc5c to
e84d27e
Compare
e84d27e to
f6232d6
Compare
|
Added CodeDOM foundation for Phase 4 generic type-parameter emission in this commit. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 24 out of 24 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/Kiota.Builder/KiotaBuilder.cs:2124
- When composing a union for multi-candidate $dynamicRef (no active binding), the union type name is derived only from
currentNode.GetClassName(...)and ignoressuffixForInlineSchema/anchor identity. If multiple such unions are produced under the same node (e.g., two properties with different multi-candidate $dynamicRef anchors), they can end up with the sameCodeUnionType.Name. Downstream wrapper generation de-dupes primarily by name, so this can cause the second union to reuse the first wrapper and produce an incorrect property type.
Include suffixForInlineSchema in the union name (consistent with other inline composed-type naming) to keep wrapper names stable and collision-resistant.
var unionType = new CodeUnionType { Name = currentNode.GetClassName(config.StructuredMimeTypes, operation: operation, schema: schema).CleanupSymbolName() };
f6232d6 to
fe23941
Compare
|
Re: the suppressed Copilot finding about union type name collisions when multiple multi-candidate `` anchors share a model — fixed. The union name now prefers |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 24 out of 24 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/Kiota.Builder/KiotaBuilder.cs:2158
TryGetDynamicBindingSuffixcallsContainsDynamicReference(schema)even when there is no active binding suffix on the dynamic-scope stack. SinceContainsDynamicReferenceperforms a deep traversal over schema graphs, this adds avoidable overhead for the common case (no binding context). You can short-circuit by first retrieving the active suffix and only scanning the schema when that suffix is non-null (and reuse it for both fallback returns).
private static string? TryGetDynamicBindingSuffix(IOpenApiSchema schema, OpenApiUrlTreeNode currentNode, OpenApiOperation? operation = default, IOpenApiResponse? response = default, bool isRequestBody = false, string suffixForInlineSchema = "")
{
if (schema.Definitions is null || schema.Definitions.Count == 0)
return ContainsDynamicReference(schema) ? GetActiveDynamicBindingSuffix() : null;
var anchorSuffix = string.Empty;
fe23941 to
de5faf3
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 24 out of 24 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/Kiota.Builder/KiotaBuilder.cs:2132
- In the multi-candidate $dynamicRef fallback (union case),
AddModelDeclarationIfDoesntExist(..., c.Name.CleanupSymbolName(), ...)can generate redundant/incorrect declaration names for component keys that include namespace-like dots (e.g.,v1.User). Consider extracting the terminal identifier segment (same as the refId naming path) before CleanupSymbolName so union member declarations match how referenced schemas are normally named.
{
var ns = GetShortestNamespace(codeNamespace, c.Schema);
var decl = AddModelDeclarationIfDoesntExist(currentNode, operation, c.Schema, c.Name.CleanupSymbolName(), ns);
unionType.AddType(new CodeType { TypeDefinition = decl });
src/Kiota.Builder/KiotaBuilder.cs:2122
- In the multi-candidate $dynamicRef fallback (single-candidate case), using
c.Name.CleanupSymbolName()can produce a declaration name that includes namespace qualifiers (e.g.,v1.User->v1User) instead of matching the normal refId-based naming (namespace...Models.V1+ classUser). This can create redundant/incorrect model names when component keys contain dots or path-like separators.
This issue also appears on line 2129 of the same file.
var c = candidates[0];
var ns = GetShortestNamespace(codeNamespace, c.Schema);
return new CodeType { TypeDefinition = AddModelDeclarationIfDoesntExist(currentNode, operation, c.Schema, c.Name.CleanupSymbolName(), ns) };
src/Kiota.Builder/KiotaBuilder.cs:2215
ContainsDynamicReferenceis used to decide whether to inherit the active binding suffix for schemas without local$defs. It currently walks refs/items/properties/defs/allOf/anyOf/oneOf, but it doesn’t considerAdditionalPropertiesschemas. If a dynamic ref appears underadditionalProperties, the binding suffix won’t be inherited and resolution may regress to the unbound type.
return !string.IsNullOrEmpty(schema.DynamicRef) ||
schema is OpenApiSchemaReference { Target: { } target } && ContainsDynamicReference(target, visited) ||
schema.Items is not null && ContainsDynamicReference(schema.Items, visited) ||
schema.Properties?.Values.Any(x => ContainsDynamicReference(x, visited)) == true ||
schema.Definitions?.Values.Any(x => ContainsDynamicReference(x, visited)) == true ||
de5faf3 to
8e969a1
Compare
|
All three suppressed Copilot findings addressed:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 24 out of 24 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/Kiota.Builder/KiotaBuilder.cs:2134
- Same namespace-collision issue as above in the union construction loop: candidate namespace and class name are derived from
c.Schema+c.Name.Split('.'), which drops the dotted prefix into neither the namespace nor the type name. This can merge distinct component schemas into one declaration when multiple dotted keys share the same tail name.
var ns = GetShortestNamespace(codeNamespace, c.Schema);
var className = c.Name.Split('/').Last().Split('.').Last().CleanupSymbolName();
var decl = AddModelDeclarationIfDoesntExist(currentNode, operation, c.Schema, className, ns);
unionType.AddType(new CodeType { TypeDefinition = decl });
src/Kiota.Builder/KiotaBuilder.cs:2223
- In CreateCollectionModelDeclaration, the dynamic-scope frame pushed for array-root schemas with $defs/$dynamicAnchor sets BindingSuffix to null. This prevents referenced templates inside the array items from inheriting the active binding suffix (via GetActiveDynamicBindingSuffix), so binding-aware specialization can be skipped in cases like “array of template with bound anchors”. Compute and store the binding suffix on the frame when pushing.
var shouldPush = schema.Definitions?.Values.Any(static d => !string.IsNullOrEmpty(d.DynamicAnchor)) == true;
if (shouldPush) _dynamicScope.Value!.Push(new(schema, null));
src/Kiota.Builder/KiotaBuilder.cs:2123
- The multi-candidate $dynamicRef fallback derives the candidate model namespace from
c.Schemavia GetShortestNamespace, but component schemas aren’t references so GetReferenceId() is null. For dotted component keys (e.g.v1.StringModel), this collapses everything into the current namespace and can cause name collisions (e.g.v1.StringModelvsv2.StringModelboth becomeStringModel). Use the component key (or schema ref id when available) to compute both namespace and class name consistently with other referenced-schema paths.
This issue also appears on line 2131 of the same file.
var ns = GetShortestNamespace(codeNamespace, c.Schema);
var className = c.Name.Split('/').Last().Split('.').Last().CleanupSymbolName();
return new CodeType { TypeDefinition = AddModelDeclarationIfDoesntExist(currentNode, operation, c.Schema, className, ns) };
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 24 out of 24 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/Kiota.Builder/KiotaBuilder.cs:2192
TryGetDynamicBindingSuffixonly appendssuffixForInlineSchemafor response contexts whenresponse is not null. For normal 2xx responses,CreateModelDeclarationsis called without theresponseargument (e.g.,GetExecutorMethodReturnTypeat KiotaBuilder.cs:1379-1380), soresponseis null and inline-binding suffixes can degrade to just the route-path portion whenoperation.OperationIdis missing. That can cause distinct inline bindings (e.g., GET vs POST on the same path without operationIds) to incorrectly share the same suffixed template class, defeating the "per-context classes" goal.
suffix += string.IsNullOrEmpty(operation?.OperationId) && !string.IsNullOrEmpty(suffixForInlineSchema) ?
suffixForInlineSchema.CleanupSymbolName().ToFirstCharacterUpperCase() :
RequestBodySuffix;
else if (response is not null)
suffix += string.IsNullOrEmpty(suffixForInlineSchema) ? ResponseSuffix : suffixForInlineSchema.CleanupSymbolName().ToFirstCharacterUpperCase();
Summary
Phase 2 of #7815. Builds on Phase 1 (#7817) which resolved recursive
$dynamicRefvia a thread-local dynamic-scope stack.This PR adds support for the template binding pattern: when the same template schema is referenced from multiple contexts with different
$defs+$dynamicAnchorbindings, each binding context now produces a distinct concrete class instead of silently sharing one class with the first binding's types. This PR emits concrete specializations (e.g.,PaginatedTemplateUser,PaginatedTemplateGroup), not reusable generic type declarations.When no binding context is active but multiple candidate schemas declare a matching
$dynamicAnchor, the$dynamicRefresolves to a composed union type instead of degrading toUntypedNode.Additionally, this PR adds CodeDOM foundation for generic type-parameter emission (Phase 4 Step 1):
CodeTypeParameterelement andTypeParameterscollection onProprietableBlockDeclaration. These are populated as metadata on suffixed template classes but not yet rendered by language writers — generated output is byte-identical with or without this addition. Per-language writer PRs will follow.What changed
KiotaBuilder.cs:DynamicScopeFrame(Schema, BindingSuffix)record replaces bareIOpenApiSchemaon the dynamic-scope stack, carrying the computed suffix alongside each frame.TryGetDynamicBindingSuffix— when a schema carries$defsentries with$dynamicAnchor, produces a suffix from the bound type names (or route segment + operation context for inline bindings). Definitions are sorted by key for deterministic output. Context suffix is computed once, not duplicated per anchor.ContainsDynamicReference+GetActiveDynamicBindingSuffix— when a component has no local$defsbut contains a reachable$dynamicRef, inherits the binding suffix from the active dynamic scope.$defs), then the unwrapped target. Recursive check runs beforeGetExistingDeclarationso a bare class can't bypass the suffix.$dynamicAnchorand composes a union type (or single type for one candidate).AddDynamicBindingTypeParameters— populatesCodeTypeParametermetadata on template classes at both binding sites (CreateModelDeclarationAndTypeandCreateInheritedModelDeclarationCore). NamedT+ anchor (e.g.,TItemType). Not rendered by writers yet.CreateCollectionModelDeclarationconditionally pushes onto the dynamic scope when the array schema carries$defswith$dynamicAnchor.ExtractAnchorNamechanged fromprivatetointernalfor unit testing.CodeDOM (
CodeTypeParameter.cs,ProprietableBlock.cs):CodeTypeParameter : CodeTerminal— minimal marker element carrying a name (e.g.,TItemType).ProprietableBlockDeclarationgainsTypeParameterscollection (ConcurrentDictionary, sorted by name) +AddTypeParameter+IsGeneric. BothClassDeclarationandInterfaceDeclarationinherit this.Test coverage
generic-binding.yaml$refbindings in$defsinline-binding.yaml$ref)recursive-generic-binding.yamlrequest-body-generic-binding.yamlmulti-anchor-generic-binding.yaml$dynamicAnchorslots per templatearray-root-dynamicref.yaml$dynamicRefin array items at response rootmulti-candidate-no-binding.yaml$dynamicRefwith no binding → unionunresolved-dynamicref.yaml$dynamicRef→UntypedNodemulti-inline-binding.yamlmixed-anchor-binding.yaml$ref+ inline anchors, orderinginherited-generic-binding.yamlmulti-error-inline-binding.yamlrequest-response-inline-binding.yamlinherited-component-binding.yamlno-operation-id-inline-binding.yamlnamespaced-binding.yamlv1.Uservsv2.User)Five-language fixtures assert language-specific typed deserialization or composed-type semantics. C#-only fixtures assert builder-level model identity and binding-specific type resolution.
CodeDOM unit tests in
CodeTypeParameterTests.cs(3 tests) +DynamicBindingPopulatesTypeParametersOnTemplateAsyncinKiotaBuilderDynamicRefTests.cs.ExtractAnchorNameunit tests inKiotaBuilderDynamicRefTests.cs.Behavioral change
Referenced schemas whose site declares
$defsentries containing$dynamicAnchor+$refnow get a suffixed class name. Multi-candidate$dynamicRefwithout active binding context now produces a composed union type instead ofUntypedNode. These are the intended fixes but will change generated output for any existing spec that happens to match these patterns.Remaining work (Phase 4)
TypeParametersas generic declarations (PaginatedTemplate<TItemType>) and drop the concrete suffix. Each language will be a separate PR.where T : IParsable) not yet supported — deferred.