diff --git a/.chronus/changes/fix-formatter-heritage-keyword-break-2026-6-18.md b/.chronus/changes/fix-formatter-heritage-keyword-break-2026-6-18.md new file mode 100644 index 00000000000..d9e5429e605 --- /dev/null +++ b/.chronus/changes/fix-formatter-heritage-keyword-break-2026-6-18.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/compiler" +--- + +Keep the `is`/`extends` keyword on the declaration line when the base is a template reference with multiple arguments. The template argument list now controls the line breaking instead of the keyword being pushed onto its own indented line. diff --git a/.chronus/changes/fix-formatter-heritage-keyword-break-java-2026-6-18.md b/.chronus/changes/fix-formatter-heritage-keyword-break-java-2026-6-18.md new file mode 100644 index 00000000000..378311e042e --- /dev/null +++ b/.chronus/changes/fix-formatter-heritage-keyword-break-java-2026-6-18.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@typespec/http-client-java" +--- + +Reformat a test spec affected by the compiler formatter change keeping `is`/`extends` inline for multi-argument template references (whitespace only, no semantic change). diff --git a/packages/compiler/src/formatter/print/printer.ts b/packages/compiler/src/formatter/print/printer.ts index adf307c6c6a..497155a71ce 100644 --- a/packages/compiler/src/formatter/print/printer.ts +++ b/packages/compiler/src/formatter/print/printer.ts @@ -1043,6 +1043,31 @@ export function printArrayLiteral( ]); } +// When the base is a multi-argument template, its argument list breaks on its +// own, so keep `is`/`extends` inline instead of breaking the keyword too (#11009). +function printHeritageClause( + path: AstPath, + print: PrettierChildPrint, + keyword: string, + propertyName: keyof T, +): Doc { + const ref = path.node[propertyName] as Node | undefined; + if (!ref) { + return ""; + } + const printed = [`${keyword} `, path.call(print, propertyName as any)]; + if (isMultiArgTemplateReference(ref)) { + return [" ", printed]; + } + return group(indent([ifBreak(line, " "), printed])); +} + +function isMultiArgTemplateReference(node: Node): boolean { + return ( + node.kind === SyntaxKind.TypeReference && (node as TypeReferenceNode).arguments.length >= 2 + ); +} + export function printModelStatement( path: AstPath, options: TypeSpecPrettierOptions, @@ -1050,10 +1075,8 @@ export function printModelStatement( ) { const node = path.node; const id = path.call(print, "id"); - const heritage = node.extends - ? [ifBreak(line, " "), "extends ", path.call(print, "extends")] - : ""; - const isBase = node.is ? [ifBreak(line, " "), "is ", path.call(print, "is")] : ""; + const heritage = printHeritageClause(path, print, "extends", "extends"); + const isBase = printHeritageClause(path, print, "is", "is"); const generic = printTemplateParameters(path, options, print, "templateParameters"); const nodeHasComments = hasComments(node, CommentCheckFlags.Dangling); const shouldPrintBody = nodeHasComments || !(node.properties.length === 0 && node.is); @@ -1064,7 +1087,8 @@ export function printModelStatement( "model ", id, generic, - group(indent(["", heritage, isBase])), + heritage, + isBase, body, ]; } @@ -1240,9 +1264,7 @@ function printScalarStatement( const id = path.call(print, "id"); const template = printTemplateParameters(path, options, print, "templateParameters"); - const heritage = node.extends - ? [ifBreak(line, " "), "extends ", path.call(print, "extends")] - : ""; + const heritage = printHeritageClause(path, print, "extends", "extends"); const nodeHasComments = hasComments(node, CommentCheckFlags.Dangling); const shouldPrintBody = nodeHasComments || !(node.members.length === 0); @@ -1253,7 +1275,7 @@ function printScalarStatement( "scalar ", id, template, - group(indent(["", heritage])), + heritage, members, ]; } diff --git a/packages/compiler/test/formatter/formatter.test.ts b/packages/compiler/test/formatter/formatter.test.ts index c4ac8e5900e..7595e57b6d2 100644 --- a/packages/compiler/test/formatter/formatter.test.ts +++ b/packages/compiler/test/formatter/formatter.test.ts @@ -207,6 +207,21 @@ model Foo extends SuperExtremeAndVeryVeryVeryVeryVeryVeryLongModelThatWillBeTo expected: ` model Foo extends SuperExtremeAndVeryVeryVeryVeryVeryVeryLongModelThatWillBeTooLong {} +`, + }); + }); + + it("keeps extends inline and breaks the template arguments when too long", async () => { + await assertFormat({ + code: ` +model Foo extends Base {} +`, + expected: ` +model Foo extends Base< + FirstArgumentTypeName, + SecondArgumentTypeName, + ThirdArgumentTypeName +> {} `, }); }); @@ -255,6 +270,21 @@ model Foo is SuperExtremeAndVeryVeryVeryVeryVeryVeryLongLongLongModelThatWillB expected: ` model Foo is SuperExtremeAndVeryVeryVeryVeryVeryVeryLongLongLongModelThatWillBeTooLong; +`, + }); + }); + + it("keeps is inline and breaks the template arguments when too long", async () => { + await assertFormat({ + code: ` +model Foo is Base; +`, + expected: ` +model Foo is Base< + FirstArgumentTypeName, + SecondArgumentTypeName, + ThirdArgumentTypeName +>; `, }); }); @@ -809,6 +839,21 @@ scalar Foo extends string; }); }); + it("keeps extends inline and breaks the template arguments when too long", async () => { + await assertFormat({ + code: ` +scalar Foo extends Base; +`, + expected: ` +scalar Foo extends Base< + FirstArgumentTypeName, + SecondArgumentTypeName, + ThirdArgumentTypeName +>; +`, + }); + }); + it("format with template parameters", async () => { await assertFormat({ code: ` diff --git a/packages/http-client-java/generator/http-client-generator-test/tsp/arm-customization.tsp b/packages/http-client-java/generator/http-client-generator-test/tsp/arm-customization.tsp index b964ba2e376..fe67b54a891 100644 --- a/packages/http-client-java/generator/http-client-generator-test/tsp/arm-customization.tsp +++ b/packages/http-client-java/generator/http-client-generator-test/tsp/arm-customization.tsp @@ -23,8 +23,10 @@ enum Versions { v2023_12_01_preview: "2023-12-01-preview", } -model Vault - is Azure.ResourceManager.Legacy.TrackedResourceWithOptionalLocation { +model Vault is Azure.ResourceManager.Legacy.TrackedResourceWithOptionalLocation< + VaultProperties, + false +> { ...ResourceNameParameter< Resource = Vault, KeyName = "vaultName", diff --git a/website/src/content/docs/release-notes/typespec-1-11-0.mdx b/website/src/content/docs/release-notes/typespec-1-11-0.mdx index 23ed7633339..a28beb27fda 100644 --- a/website/src/content/docs/release-notes/typespec-1-11-0.mdx +++ b/website/src/content/docs/release-notes/typespec-1-11-0.mdx @@ -31,12 +31,11 @@ model Example { description: string; } -model CreateAndReadExample - is FilterVisibility< - Example, - #{ all: #[Lifecycle.Create, Lifecycle.Read] }, - "CreateAndRead{name}" - >; +model CreateAndReadExample is FilterVisibility< + Example, + #{ all: #[Lifecycle.Create, Lifecycle.Read] }, + "CreateAndRead{name}" +>; ``` :::caution