Skip to content

feat(azure-core): add three Azure REST API Guidelines linter rules - #5055

Open
xirzec wants to merge 3 commits into
Azure:mainfrom
xirzec:xirzec-azure-core-guideline-lint-rules
Open

feat(azure-core): add three Azure REST API Guidelines linter rules#5055
xirzec wants to merge 3 commits into
Azure:mainfrom
xirzec:xirzec-azure-core-guideline-lint-rules

Conversation

@xirzec

@xirzec xirzec commented Jul 25, 2026

Copy link
Copy Markdown
Member

Note: This PR was authored by GitHub Copilot (an AI agent) on behalf of @xirzec.

Summary

This is the first batch of a broader effort to encode mechanical statements from the Azure REST API Guidelines (vNext data-plane) as deterministic linter rules, rather than relying on manual (or LLM-based) review to catch them.

Three new @azure-tools/typespec-azure-core rules, all warning severity (consistent with every other rule in the package — in fact the compiler types LinterRuleDefinitionBase.severity as the literal "warning", so a linter rule cannot be anything else):

Rule Guideline data-plane resource-manager
no-version-in-route versioning-no-version-in-path
use-date-api-version versioning-date-based-versioning
no-dollar-prefixed-query-params collections-query-options-no-dollar-sign

Validation — zero false positives on in-repo specs

Every spec in the repo was compiled against the real rulesets (not a synthetic harness) and checked for diagnostics from the three new rules:

Corpus Ruleset New-rule diagnostics
packages/samples/specs/data-plane/* (5 specs) data-plane 0
packages/samples/specs/resource-manager/* (6 specs) resource-manager 0
packages/azure-http-specs/specs/azure/core/* data-plane 0
packages/azure-http-specs/specs/azure/resource-manager/* (10 specs) resource-manager 0

Firing was separately verified end-to-end against a deliberately-violating spec, so the zeroes above are "no violations present", not "rule silently disabled".

Other checks:

  • 38 tests across the three rules (valid cases, invalid cases, boundary conditions, dedup regressions, and a case asserting Azure.Core / Azure.ResourceManager library types are exempt).
  • Full typespec-azure-core suite green.
  • typespec-azure-rulesets suite (incl. validate-rules-defined): 4/4.
  • pnpm lint, pnpm format, pnpm cspell, regen-docs all clean.

Why these aren't duplicates of casing-style — wire names vs. TypeSpec identifiers

Worth calling out explicitly, because it looks like overlap at first glance.

casing-style inspects TypeSpec identifiers only — model.name, property.name, operation.name, and namespace/interface names. It never looks at the string arguments of @query(...), @header(...), or @route(...).

That means this is completely invisible to the existing rule:

op list(@query("$filter") filter?: string): Widget[];

The TypeSpec identifier is filter — perfectly camelCase, no complaint. The wire name the customer actually types into a URL is $filter. no-dollar-prefixed-query-params reads HttpProperty.options.name (the resolved wire name), which is a capability the linter did not previously have. Same story for no-version-in-route, which reads the resolved HttpOperation.path rather than the raw @route string, so version segments contributed by a namespace- or interface-level @route are also caught.

This is a new axis of checking and is where I expect most of the remaining mechanical guidelines to live (header casing, query-param casing, JSON field casing).

Ruleset matrix and reasoning

Guidelines.md vNext is the data-plane contract. ARM has its own, separately-governed contract, so a data-plane guideline is not automatically an ARM guideline. Two of the three rules are therefore deliberately disabled in resource-manager.ts:

no-dollar-prefixed-query-params → disabled for ARM. ARM ships the OData spelling by design, in this very repo:

// packages/typespec-azure-resource-manager/lib/parameters.tsp
model ArmTopParameter {
  @query("$top") top?: int64;
}
model ArmFilterParameter {
  @query("$filter") filter?: string;
}
model ArmSkipParameter {
  @query("$skip") skip?: int64;
}
model SkipTokenParameter {
  @query("$skiptoken") skipToken: string;
}

Enabling the rule for ARM would flag every ARM service that uses the library's own standard parameters. (The rule already exempts the library declarations themselves via isExcludedCoreType, but a service spreading ArmTopParameter into its own operation would still be flagged — which is exactly the unactionable diagnostic we want to avoid.)

use-date-api-version → disabled for ARM. ARM already has arm-resource-invalid-version-format, whose pattern is /^[\d]{4}-[\d]{2}-[\d]{2}(-[\w]+(\.\d+)?)?$/ — it permits arbitrary suffixes such as -alpha.1 and -beta.2. The data-plane guideline permits only -preview. These are in genuine conflict, and ARM's existing rule wins on ARM.

no-version-in-route → enabled for ARM. This is a deliberate decision, not an oversight. Reasoning:

  • ARM's versioning contract is also api-version-query-parameter based; a literal /v1/ path segment is wrong under both contracts, so this is a shared constraint rather than a data-plane-only one.
  • ARM paths are structurally fixed (/subscriptions/{id}/resourceGroups/{rg}/providers/{RP}/{type}/{name}); there is no legitimate place for a version segment.
  • Verified empirically: no v<digits> segment appears in any ARM library template (packages/typespec-azure-resource-manager/lib/**), any ARM sample, or any ARM spec in azure-http-specs — all confirmed by compilation, not just grep.
  • Verified there is no competing ARM rule. The closest, arm-resource-path-segment-invalid-chars, only constrains the character set and casing of a segment; it would happily accept v2.

Residual risk: I can only verify against specs in this repo. If a real ARM RP in azure-rest-api-specs carries a nested resource-type segment that happens to match v<digits>, int:azure-specs will surface it, and scoping the rule to data-plane only is a one-line change to resource-manager.ts. Happy to flip it pre-emptively if reviewers prefer.

Rule details

no-version-in-route

Reports operations whose resolved HTTP path contains a literal API-version segment.

// ❌
@route("/v1/widgets") @get op list(): Widget[];

// ✅
@route("/widgets") @get op list(): Widget[];

Intentionally narrow to keep false positives at zero: it matches only v<digits>[.<digits>]* (case-insensitive), and skips {...} path-parameter segments so a parameter that happens to be named v1 is not flagged. It does not flag date-shaped path segments, even though the guideline arguably covers them — that felt like it would need more real-world evidence before turning on.

use-date-api-version

Validates the values of the enum referenced by @versioned.

// ❌
enum Versions { v1, `2021-6-4`, `2021-06-04-beta` }

// ✅
enum Versions { `2021-06-04-preview`, `2021-10-01` }

A shape regex alone is insufficient — 2021-13-45 and 2023-02-29 both match \d{4}-\d{2}-\d{2}. The rule does a real calendar round-trip via Date.UTC and emits a distinct invalidDate message so the diagnostic explains why the value is rejected rather than just restating the pattern.

no-dollar-prefixed-query-params

Reports $-prefixed wire names for the seven standard collection query options (filter, orderby, skip, top, maxpagesize, select, expand), and suggests the unprefixed name in the message.

// ❌
op list(@query("$filter") filter?: string, @query `$top` top?: int32): Widget[];

// ✅
op list(@query filter?: string, @query top?: int32): Widget[];

Handles both the explicit-@query("...") form and the backtick-identifier form (where the wire name comes from the property name). Non-standard $-prefixed names (e.g. $custom) are intentionally left alone — the guideline is about the OData collection options specifically.

Update: External Integration results and duplicate-reporting fix

The int:azure-specs check surfaced violations in existing specs. All are true positives — external-standard compatibility (OCI Distribution Spec for ACR's /v2/, Apache Atlas for Purview's /atlas/v2/), deliberate OData (Batch, Search), and already-shipped api-versions (Key Vault 7.5/7.6). Renaming any of them would be a breaking change to released SDKs, so they need suppressions rather than fixes. A companion suppression PR against azure-rest-api-specs typespec-next is required before this merges; it has not been created yet, pending reviewer input on its shape.

Investigating that also surfaced a genuine bug in two of the rules: they reported the same declaration many times. Both are fixed in this PR.

rule before after
no-version-in-route 343 ~254
no-dollar-prefixed-query-params 295 81
use-date-api-version 41 41
total 679 ~376
  • no-dollar-prefixed-query-params now resolves each property back through sourceProperty and reports each declaration once, instead of once per consuming operation (Batch: 180 diagnostics → 36 declarations, all in one file).
  • no-version-in-route no longer double-reports is-aliases that inherit their route (Purview: 178 → 89).

Neither change relaxes detection — distinct declarations sharing a name are still reported separately, an alias that declares its own versioned @route is still reported, and an alias of an excluded library operation is still reported. 5 regression tests added.

📋 Full analysis — severity semantics, per-cluster fixability, precedent, and the typespec-next vs main suppression flow — is in this comment, which also asks two specific questions for reviewers.

Notes for reviewers

  • Each rule has a companion .md doc with ✅/❌ examples, an ## Impact section, and a ## Suppression section explaining when suppression is legitimate and (for the two ARM-disabled rules) why they don't apply to ARM.
  • linter.ts's diff is larger than three lines because pnpm create:linter-rule re-sorts the entire import block.
  • Chronus changeset included (feature).
  • More mechanical rules are planned as follow-ups — most promising next are header wire-name casing, query-parameter wire-name casing, and JSON field-name casing, all of which build on the same wire-name inspection introduced here.

Prototype encoding vNext data-plane guidelines as deterministic lint rules:

- `no-version-in-route` (versioning-no-version-in-path): rejects literal
  version segments such as `/v1/` or `/v2` in resolved operation paths.
- `api-version-date-format` (versioning-date-based-versioning): requires the
  values of the `@versioned` enum to be a real `YYYY-MM-DD` date with an
  optional `-preview` suffix.
- `no-dollar-prefixed-query-params` (collections-query-options-no-dollar-sign):
  rejects OData-style `\$`-prefixed wire names for the seven standard
  collection query options.

All three are enabled in the data-plane ruleset. `api-version-date-format` is
disabled for resource-manager, which has its own conflicting version format rule
that permits suffixes other than `-preview`.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 07c7eb54-a85a-494c-b2bc-4dd967523a02
@xirzec xirzec added the int:azure-specs Run integration tests against azure-rest-api-specs label Jul 25, 2026
@microsoft-github-policy-service microsoft-github-policy-service Bot added lib:azure-core Issues for @azure-tools/typespec-azure-core library meta:website TypeSpec.io updates linter Issues related to linter rules labels Jul 25, 2026
@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

All changed packages have been documented.

  • @azure-tools/typespec-azure-core
  • @azure-tools/typespec-azure-rulesets
Show changes

@azure-tools/typespec-azure-core - feature ✏️

Add no-version-in-route, use-date-api-version, and no-dollar-prefixed-query-params linter rules enforcing the Azure REST API Guidelines for path versioning, date-based api-version values, and standard collection query option names.

@azure-tools/typespec-azure-rulesets - feature ✏️

Add no-version-in-route, use-date-api-version, and no-dollar-prefixed-query-params linter rules enforcing the Azure REST API Guidelines for path versioning, date-based api-version values, and standard collection query option names.

@pkg-pr-new

pkg-pr-new Bot commented Jul 25, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@azure-tools/typespec-azure-core@5055
npm i https://pkg.pr.new/@azure-tools/typespec-azure-rulesets@5055

commit: 5e49093

@azure-sdk-automation

Copy link
Copy Markdown
Contributor

You can try these changes here

🛝 Playground 🌐 Website

Both rules could report a single offending declaration many times.

no-dollar-prefixed-query-params walked each operation's resolved query
parameters, so a shared parameter model spread into N operations produced N
diagnostics for one declaration. It now resolves each property back through
sourceProperty to the declaration it was copied from, reports there, and
reports each declaration only once. Against azure-rest-api-specs this takes
the rule from 295 diagnostics to 81; Batch alone goes from 180 to 36, all in
one file.

no-version-in-route reported both an operation and every is alias of it,
because an alias inherits the route. It now skips an alias whose version
segment comes from its source operation, so the diagnostic lands on the
declaration that owns the route. An alias that declares its own versioned
@route is still reported, and an alias of an excluded (library) operation
is still reported since the source is never visited. This takes Purview from
178 diagnostics to 89.

Neither change relaxes what the rules detect: distinct declarations that
happen to share a name are still reported separately.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 07c7eb54-a85a-494c-b2bc-4dd967523a02
@xirzec

xirzec commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

🤖 Written by GitHub Copilot (an AI agent) on behalf of @xirzec.

int:azure-specs results. All the diagnostics are true positives, and none are realistically fixable: ACR's /v2/ is mandated by the OCI Distribution Spec, Purview's /atlas/v2/ is Apache Atlas compat, Batch/Search $filter is deliberate OData, and the Key Vault 7.5/7.6 versions are already shipped. Renaming any of them breaks released SDKs, so these need suppressions rather than fixes.

Investigating also turned up a real bug in two of the rules — they reported the same declaration once per consuming operation (Batch: 180 diagnostics for 36 declarations) and double-reported is-aliases (Purview: 178 for 89). Both fixed in this PR; 679 → ~376, with no loss of detection.

Suppressions go to azure-rest-api-specs typespec-next (it pins the next dist-tag), following #4880 / Azure/azure-rest-api-specs#44804. Not created yet — two questions first:

  1. Per-project linter.disable (~18 tspconfigs) or per-declaration #suppress (~465 lines / 44 files)? I lean toward the former since these deviations are service-wide and permanent, but #suppress matches prior art.
  2. Prepare main suppressions now, or wait until the release is adopted there?

Happy to expand on any of it — severity semantics, the per-cluster breakdown, or why "only lint new APIs" isn't expressible in the linter model.

Comment thread packages/typespec-azure-core/src/rules/use-date-api-version.md

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are those new rules? is there any conflict with the ongoing lintdiff migration work or is this part of it?
@catalinaperalta @markcowl

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is from me asking Copilot to look at any gaps from our REST guidelines. Happy to avoid duplicating any work that Cat and Mark have in flight.

Align the rule id with the repo's linter naming convention (use the
`use-<preferred-thing>` prefix for rules that point to a preferred
TypeSpec pattern), per reviewer feedback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 07c7eb54-a85a-494c-b2bc-4dd967523a02

@markcowl markcowl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think any of these rules meet the bar for linting rules. These some more like authoring guidance.

@@ -0,0 +1,57 @@
The standard collection query options (`filter`, `orderby`, `skip`, `top`, `maxpagesize`, `select`, and `expand`) come from the OData standard, where they are spelled with a `$` prefix. Azure services use the same names, but without the `$`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not true for management plane - this should be narrowly focused on data plane services.

/**
* Matches a path segment that looks like an API version, e.g. `v1`, `V2`, `v1.0`, `v2.1.3`.
*/
const versionSegmentPattern = /^v\d+(\.\d+)*$/i;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am skeptical of this rule. We are unsure if any path like this represents an api-version and not something else. I think we should reserve these rules for real, impactful violations that are likely

@@ -0,0 +1,60 @@
import { createRule, fileRef, listServices, paramMessage } from "@typespec/compiler";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is duplicative of the rule that already exists in ARM. However, unlike in ARM, this is not always true for data plane APIs, which is why this wasn't added for data planes. I would suggest not adding this rule, and if you do, moving the ARM rule into azure-core instead.

https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-resource-manager/src/rules/arm-resource-invalid-version-format.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

int:azure-specs Run integration tests against azure-rest-api-specs lib:azure-core Issues for @azure-tools/typespec-azure-core library linter Issues related to linter rules meta:website TypeSpec.io updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants