From b017467f2b02b107ae9afdbd54829f126ca43a74 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:11:44 +0000 Subject: [PATCH 1/4] Initial plan From 7672ee5abdae03a8a0aa3f6f0e4ddaa086810138 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:24:47 +0000 Subject: [PATCH 2/4] feat: add warning when @list is used with no paging navigation information" Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- ...ing-for-list-no-paging-2026-7-30-12-0-0.md | 20 ++++++ packages/compiler/src/core/messages.ts | 6 ++ packages/compiler/src/lib/paging.ts | 17 +++++ .../compiler/test/decorators/paging.test.ts | 63 ++++++++++++++++++- 4 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 .chronus/changes/copilot-add-warning-for-list-no-paging-2026-7-30-12-0-0.md diff --git a/.chronus/changes/copilot-add-warning-for-list-no-paging-2026-7-30-12-0-0.md b/.chronus/changes/copilot-add-warning-for-list-no-paging-2026-7-30-12-0-0.md new file mode 100644 index 00000000000..a27c6170c2a --- /dev/null +++ b/.chronus/changes/copilot-add-warning-for-list-no-paging-2026-7-30-12-0-0.md @@ -0,0 +1,20 @@ +--- +changeKind: feature +packages: + - "@typespec/compiler" +--- + +Add warning when `@list` is used with no paging navigation information. Operations decorated with `@list` now require at least one of `@nextLink`, `@pageIndex`, or `@continuationToken`. + +```typespec +// This will now emit a warning: no paging navigation information +@list op list(): { + @pageItems items: string[]; +}; + +// Correct: has nextLink +@list op list(): { + @pageItems items: string[]; + @nextLink next: string; +}; +``` diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 5e5f832eca3..c4c37f74bc2 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -1013,6 +1013,12 @@ const diagnostics = { default: paramMessage`Paged operation '${"operationName"}' return type must have a property annotated with @pageItems.`, }, }, + "list-no-paging-navigation": { + severity: "warning", + messages: { + default: paramMessage`Paged operation '${"operationName"}' has no paging navigation. Add a @nextLink, @pageIndex, or @continuationToken property to enable pagination.`, + }, + }, /** * Service */ diff --git a/packages/compiler/src/lib/paging.ts b/packages/compiler/src/lib/paging.ts index bcca28def3c..73c3d839ef6 100644 --- a/packages/compiler/src/lib/paging.ts +++ b/packages/compiler/src/lib/paging.ts @@ -280,6 +280,23 @@ export function getPagingOperation( ); return diags.wrap(undefined); } + + const hasNavigationInfo = + result.output.nextLink !== undefined || + result.input.pageIndex !== undefined || + result.input.continuationToken !== undefined || + result.output.continuationToken !== undefined; + + if (!hasNavigationInfo) { + diags.add( + createDiagnostic({ + code: "list-no-paging-navigation", + format: { operationName: op.name }, + target: op, + }), + ); + } + return diags.wrap(result); } diff --git a/packages/compiler/test/decorators/paging.test.ts b/packages/compiler/test/decorators/paging.test.ts index b296859401a..785068308ed 100644 --- a/packages/compiler/test/decorators/paging.test.ts +++ b/packages/compiler/test/decorators/paging.test.ts @@ -33,11 +33,64 @@ it("emit error if missing pageItems property", async () => { }); }); +it("emit warning when @list has no paging navigation information", async () => { + const diagnostics = await Tester.diagnose(` + @list op list(): { + @pageItems items: string[]; + }; + `); + + expectDiagnostics(diagnostics, { + code: "list-no-paging-navigation", + severity: "warning", + message: `Paged operation 'list' has no paging navigation. Add a @nextLink, @pageIndex, or @continuationToken property to enable pagination.`, + }); +}); + +it("no warning when @list has @nextLink", async () => { + const diagnostics = await Tester.diagnose(` + @list op list(): { + @pageItems items: string[]; + @nextLink next: string; + }; + `); + expectDiagnosticEmpty(diagnostics); +}); + +it("no warning when @list has @pageIndex", async () => { + const diagnostics = await Tester.diagnose(` + @list op list(@pageIndex page: int32): { + @pageItems items: string[]; + }; + `); + expectDiagnosticEmpty(diagnostics); +}); + +it("no warning when @list has @continuationToken in output", async () => { + const diagnostics = await Tester.diagnose(` + @list op list(): { + @pageItems items: string[]; + @continuationToken token: string; + }; + `); + expectDiagnosticEmpty(diagnostics); +}); + +it("no warning when @list has @continuationToken in input", async () => { + const diagnostics = await Tester.diagnose(` + @list op list(@continuationToken token?: string): { + @pageItems items: string[]; + }; + `); + expectDiagnosticEmpty(diagnostics); +}); + it("identifies inherited paging properties", async () => { const diagnostics = await Tester.diagnose(` model ListTestResult { @pageItems values: string[]; + @nextLink next: string; } model ExtendedListTestResult extends ListTestResult {} @@ -71,7 +124,7 @@ describe("emit conflict diagnostic if multiple properties are annotated with the @list op list( @${name} prop1: ${type}; @${name} prop2: ${type}; - ): { @pageItems items: string[] }; + ): { @pageItems items: string[]; @nextLink next: string; }; `); expectDiagnostics(diagnostics, [ @@ -99,6 +152,7 @@ describe("emit conflict diagnostic if multiple properties are annotated with the @${name} next: ${type}; @${name} nextToo: ${type}; ${name !== "pageItems" ? "@pageItems items: string[];" : ""} + ${!["nextLink", "continuationToken"].includes(name) ? "@nextLink nav: string;" : ""} }; `); @@ -125,7 +179,7 @@ describe("collect paging properties", () => { const { list, prop, program } = await Tester.compile(t.code` @list op ${t.op("list")}( @${name} ${t.modelProperty("prop")}: ${type}; - ): { @pageItems items: string[] }; + ): { @pageItems items: string[]; @nextLink next: string; }; `); const paging = ignoreDiagnostics(getPagingOperation(program, list)); @@ -145,6 +199,7 @@ describe("collect paging properties", () => { @list op ${t.op("list")}(): { @${name} ${t.modelProperty("prop")}: ${type}; ${name !== "pageItems" ? "@pageItems items: string[];" : ""} + ${name !== "nextLink" && name !== "continuationToken" ? "@nextLink nav: string;" : ""} }; `); @@ -164,7 +219,7 @@ describe("collect nested paging properties", () => { const { list, prop, program } = await Tester.compile(t.code` @list op ${t.op("list")}( @${name} ${t.modelProperty("prop")}: ${type}; - ): { @pageItems items: string[] }; + ): { @pageItems items: string[]; @nextLink next: string; }; `); const paging = ignoreDiagnostics(getPagingOperation(program, list)); @@ -183,6 +238,7 @@ describe("collect nested paging properties", () => { @list op ${t.op("list")}(): { results : { @pageItems items: string[]; }; pagination: { @${name} ${t.modelProperty("prop")}: ${type} }; + ${name !== "nextLink" && name !== "continuationToken" ? "@nextLink nav: string;" : ""} }; `); @@ -199,6 +255,7 @@ describe("collect nested paging properties", () => { const { list, program } = await Tester.compile(t.code` @list op ${t.op("list")}(): { results : { @pageItems items: string[]; }; + @nextLink next: string; }; `); From d0238bb9717fde2dcb5218732b10a43491f04c90 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:43:32 +0000 Subject: [PATCH 3/4] fix: suppress list-no-paging-navigation in spec scenarios without navigation, add @offset to nav check, fix Page model Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/compiler/src/lib/paging.ts | 1 + packages/http-specs/smoke/todoapp/main.tsp | 4 ++++ packages/http-specs/specs/payload/pageable/main.tsp | 2 ++ 3 files changed, 7 insertions(+) diff --git a/packages/compiler/src/lib/paging.ts b/packages/compiler/src/lib/paging.ts index 73c3d839ef6..dd6e8c8dc62 100644 --- a/packages/compiler/src/lib/paging.ts +++ b/packages/compiler/src/lib/paging.ts @@ -284,6 +284,7 @@ export function getPagingOperation( const hasNavigationInfo = result.output.nextLink !== undefined || result.input.pageIndex !== undefined || + result.input.offset !== undefined || result.input.continuationToken !== undefined || result.output.continuationToken !== undefined; diff --git a/packages/http-specs/smoke/todoapp/main.tsp b/packages/http-specs/smoke/todoapp/main.tsp index 3e1769f80c5..a48b4ae1ce5 100644 --- a/packages/http-specs/smoke/todoapp/main.tsp +++ b/packages/http-specs/smoke/todoapp/main.tsp @@ -240,6 +240,10 @@ namespace TodoItems { //@friendlyName("{name}List", T) model Page { @pageItems items: T[]; + + /** A link to the next page, if it exists */ + @nextLink + nextLink?: url; } @list op list(...PaginationControls): WithStandardErrors; diff --git a/packages/http-specs/specs/payload/pageable/main.tsp b/packages/http-specs/specs/payload/pageable/main.tsp index db89029037c..52f834f3f1b 100644 --- a/packages/http-specs/specs/payload/pageable/main.tsp +++ b/packages/http-specs/specs/payload/pageable/main.tsp @@ -500,6 +500,7 @@ namespace ServerDrivenPagination { @route("/pagesize") namespace PageSize { + #suppress "list-no-paging-navigation" "Intentional: single-page list with no navigation for testing purposes" @scenario @scenarioDoc(""" Test case for simple pagination without nextlink or continuationToken. @@ -525,6 +526,7 @@ namespace PageSize { pets: Pet[]; }; + #suppress "list-no-paging-navigation" "Intentional: page-size-only list with no navigation for testing purposes" @scenario @scenarioDoc(""" Test case for pagination with a regular @pageSize parameter. From 94d9f6cf11303eaa5c63b3be51eba114e5bb9b74 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:49:34 +0000 Subject: [PATCH 4/4] fix: add @nextLink to Page model and update snapshot in samples/todoApp The `Page` generic model in packages/samples/specs/todoApp/main.tsp was missing a navigation property, causing the new `list-no-paging-navigation` warning to fire on the `Attachments.list` operation. Added `@nextLink nextLink?: url` to the model, and updated the corresponding OpenAPI snapshot to include the new `nextLink` field in the `Attachments_list` response schema. Co-authored-by: timotheeguerin <1031227+timotheeguerin@users.noreply.github.com> --- packages/samples/specs/todoApp/main.tsp | 4 ++++ .../test/output/todoApp/@typespec/openapi3/openapi.yaml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/samples/specs/todoApp/main.tsp b/packages/samples/specs/todoApp/main.tsp index 7aeabdf16e7..384fec4efe8 100644 --- a/packages/samples/specs/todoApp/main.tsp +++ b/packages/samples/specs/todoApp/main.tsp @@ -241,6 +241,10 @@ namespace TodoItems { //@friendlyName("{name}List", T) model Page { @pageItems items: T[]; + + /** A link to the next page, if it exists */ + @nextLink + nextLink?: url; } @list op list(...PaginationControls): WithStandardErrors; diff --git a/packages/samples/test/output/todoApp/@typespec/openapi3/openapi.yaml b/packages/samples/test/output/todoApp/@typespec/openapi3/openapi.yaml index 6e67d77c79c..636f6455f5c 100644 --- a/packages/samples/test/output/todoApp/@typespec/openapi3/openapi.yaml +++ b/packages/samples/test/output/todoApp/@typespec/openapi3/openapi.yaml @@ -183,6 +183,10 @@ paths: type: array items: $ref: '#/components/schemas/TodoAttachment' + nextLink: + type: string + format: uri + description: A link to the next page, if it exists '404': description: The server cannot find the requested resource. content: