From f0bf9be5b6ae566b4c00c03966ef530499b65b0d Mon Sep 17 00:00:00 2001 From: kiwigitops Date: Mon, 1 Jun 2026 17:41:05 -0400 Subject: [PATCH] fix: target tag metadata extension diagnostics Signed-off-by: kiwigitops --- ...adata-extension-target-2026-6-1-14-45-0.md | 8 ++++ packages/openapi/src/decorators.ts | 4 +- packages/openapi/src/helpers.ts | 40 +++++++++++++++++-- packages/openapi/test/decorators.test.ts | 14 +++++++ 4 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 .chronus/changes/fix-openapi-tagmetadata-extension-target-2026-6-1-14-45-0.md diff --git a/.chronus/changes/fix-openapi-tagmetadata-extension-target-2026-6-1-14-45-0.md b/.chronus/changes/fix-openapi-tagmetadata-extension-target-2026-6-1-14-45-0.md new file mode 100644 index 00000000000..ebe8f5f025b --- /dev/null +++ b/.chronus/changes/fix-openapi-tagmetadata-extension-target-2026-6-1-14-45-0.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@typespec/openapi" +--- + +Fix tagMetadata extension diagnostic targets diff --git a/packages/openapi/src/decorators.ts b/packages/openapi/src/decorators.ts index a1cd496cf90..f2bdd4e257c 100644 --- a/packages/openapi/src/decorators.ts +++ b/packages/openapi/src/decorators.ts @@ -400,7 +400,7 @@ export const tagMetadataDecorator: TagMetadataDecorator = ( if ( !validateAdditionalInfoModel( context.program, - context.getArgumentTarget(0)!, + context.getArgumentTarget(1)!, resolvedMetadata, "TypeSpec.OpenAPI.TagMetadata", ) @@ -413,7 +413,7 @@ export const tagMetadataDecorator: TagMetadataDecorator = ( if ( !validateIsUri( context.program, - context.getArgumentTarget(0)!, + context.getArgumentTarget(1)!, resolvedMetadata.externalDocs.url, "externalDocs.url", ) diff --git a/packages/openapi/src/helpers.ts b/packages/openapi/src/helpers.ts index e9a2f7e95ca..cb79b73abd9 100644 --- a/packages/openapi/src/helpers.ts +++ b/packages/openapi/src/helpers.ts @@ -16,6 +16,11 @@ import { Type, TypeNameOptions, } from "@typespec/compiler"; +import { + SyntaxKind, + type ObjectLiteralNode, + type ObjectLiteralPropertyNode, +} from "@typespec/compiler/ast"; import { getOperationId } from "./decorators.js"; import { createDiagnostic, reportDiagnostic } from "./lib.js"; import { ExtensionKey } from "./types.js"; @@ -253,17 +258,30 @@ function checkNoAdditionalProperties( jsonObject: any, target: DiagnosticTarget, source: Model, +): Diagnostic[] { + const targetNode = getObjectLiteralNode(target); + return checkNoAdditionalPropertiesInternal(jsonObject, target, source, targetNode); +} + +function checkNoAdditionalPropertiesInternal( + jsonObject: any, + target: DiagnosticTarget, + source: Model, + targetNode: ObjectLiteralNode | undefined, ): Diagnostic[] { const diagnostics: Diagnostic[] = []; for (const name of Object.keys(jsonObject)) { const sourceProperty = getProperty(source, name); + const propertyNode = getObjectLiteralProperty(targetNode, name); if (sourceProperty) { if (sourceProperty.type.kind === "Model") { - const nestedDiagnostics = checkNoAdditionalProperties( + const nestedTarget = getObjectLiteralNode(propertyNode?.value); + const nestedDiagnostics = checkNoAdditionalPropertiesInternal( jsonObject[name], - target, + propertyNode?.value ?? target, sourceProperty.type, + nestedTarget, ); diagnostics.push(...nestedDiagnostics); } @@ -272,7 +290,7 @@ function checkNoAdditionalProperties( createDiagnostic({ code: "invalid-extension-key", format: { value: name }, - target, + target: propertyNode?.id ?? target, }), ); } @@ -280,3 +298,19 @@ function checkNoAdditionalProperties( return diagnostics; } + +function getObjectLiteralNode(target: DiagnosticTarget | undefined): ObjectLiteralNode | undefined { + return target !== undefined && "kind" in target && target.kind === SyntaxKind.ObjectLiteral + ? target + : undefined; +} + +function getObjectLiteralProperty( + node: ObjectLiteralNode | undefined, + name: string, +): ObjectLiteralPropertyNode | undefined { + return node?.properties.find( + (property): property is ObjectLiteralPropertyNode => + property.kind === SyntaxKind.ObjectLiteralProperty && property.id.sv === name, + ); +} diff --git a/packages/openapi/test/decorators.test.ts b/packages/openapi/test/decorators.test.ts index f2bddca4779..fbe5aef9d34 100644 --- a/packages/openapi/test/decorators.test.ts +++ b/packages/openapi/test/decorators.test.ts @@ -366,6 +366,20 @@ describe("openapi: decorators", () => { }); describe("emit diagnostics when passing extension key not starting with `x-` in metadata", () => { + it("reports the diagnostic on the invalid metadata property", async () => { + const [{ pos }, diagnostics] = await Tester.compileAndDiagnose(` + @service + @tagMetadata("tagName", #{ /*custom*/custom: "Bar" }) + namespace PetStore{}; + `); + + expectDiagnostics(diagnostics, { + code: "@typespec/openapi/invalid-extension-key", + message: `OpenAPI extension must start with 'x-' but was 'custom'`, + pos: pos.custom.pos, + }); + }); + it.each([ ["root", `#{ foo:"Bar" }`], ["externalDocs", `#{ externalDocs: #{ url: "https://example.com", foo:"Bar"} }`],