| name | create-linter-rule |
| description | Create a new TypeSpec linter rule, lint diagnostic, or design guideline checker with a TDD approach, including implementation, tests, documentation, ruleset registration, and changeset. Use this skill when asked to create, add, implement, or write a new linter rule, lint warning, validation rule, or design guideline enforcement for TypeSpec Azure libraries.
|
| allowed-tools | shell |
Create a TypeSpec Linter Rule
Follow these steps in order to create a complete, high-quality linter rule.
Step 1: IDENTIFY
Determine the rule metadata before writing any code:
- Package:
typespec-azure-core (data-plane rules), typespec-azure-resource-manager (ARM rules), or typespec-client-generator-core (client SDK generation rules)
- Rule name: Must follow these naming conventions:
- Use kebab-case (lowercase letters, numbers, hyphens)
- DO NOT include the package name in the rule ID (the package is already part of the fully-qualified diagnostic code)
- Use
no-<thing> when the rule bans a construct or usage (e.g., no-nullable, no-enum, no-format)
- Use
use-<preferred-thing> when the rule points users to a standard/preferred TypeSpec pattern (e.g., use-standard-operations, use-extensible-enum)
- Keep names short and concise — prefer
no-enum over no-enum-type-usage
- Severity: All linter rules are warnings; no severity choice is needed
- Target ruleset(s):
data-plane, resource-manager, or both
- Description: One-line explanation of what the rule enforces
Choose the package by scope:
- Decorators from
typespec-client-generator-core, or rules only about client SDK generation → typespec-client-generator-core
- Rules specific to ARM APIs →
typespec-azure-resource-manager
- Rules that apply to both data-plane and ARM, or only data-plane →
typespec-azure-core
Step 2: SCAFFOLD
Generate all required files using the repo's scaffolding tool:
pnpm create:linter-rule < rule-name > --package < azure-core | azure-resource-manager | client-generator-core > --description "<description>"
This creates:
packages/<pkg>/src/rules/<rule-name>.ts — rule skeleton
packages/<pkg>/test/rules/<rule-name>.test.ts — test skeleton
packages/<pkg>/src/rules/<rule-name>.md — docs skeleton (referenced from the rule via docs: fileRef.fromPackageRoot(...))
- Updates
packages/<pkg>/src/linter.ts — registers the rule
Step 3: WRITE FAILING TESTS FIRST (TDD)
Edit packages/<pkg>/test/rules/<rule-name>.test.ts:
- Write tests for valid code that should produce no diagnostics (
.toBeValid())
- Write tests for invalid code that should produce specific diagnostics (
.toEmitDiagnostics())
- Create equivalence classes for the input and write tests covering at least one instance of each class:
- Group inputs by how the rule handles them (e.g., for a rule targeting
ModelProperty):
- Simply defined properties
- Properties defined using
spread or is
- Properties inherited from a base class
- Add boundary conditions specific to the rule logic (e.g., for a name-prefix rule):
- Properties with the forbidden prefix
- Properties with the prefix text in the middle or end of the name
- Properties with names shorter than the prefix
- Always include at least one test verifying that library types in
Azure.Core and Azure.ResourceManager are not subject to the rule
Test API reference:
await tester.expect(`model Foo {}`).toBeValid();
await tester.expect(`model foo {}`).toEmitDiagnostics([
{
code: "@azure-tools/typespec-<pkg>/<rule-name>",
severity: "warning",
message: "Expected message text",
},
]);
await tester
.expect(`enum Color { red }`)
.applyCodeFix("fix-id")
.toEqual(`union Color { string, red: "red" }`);
Verify tests fail before implementing:
pnpm --filter "@azure-tools/typespec-<pkg>..." build
pnpm --filter "@azure-tools/typespec-<pkg>..." test
Step 4: IMPLEMENT THE RULE
Edit packages/<pkg>/src/rules/<rule-name>.ts:
- Implement visitor logic in the
create(context) function
- Return a
SemanticNodeListener with the appropriate hooks:
model — for model-level checks
modelProperty — for property-level checks
operation — for operation-level checks
enum — for enum checks
namespace — for namespace-level checks
interface — for interface checks
union / unionVariant — for union checks
- Use
context.reportDiagnostic({ target }) to report violations
- Use
paramMessage from @typespec/compiler for interpolated messages
- Add
codefixes array to reportDiagnostic() if a fix is possible
Step 5: VERIFY TESTS PASS
pnpm --filter "@azure-tools/typespec-<pkg>..." build
pnpm --filter "@azure-tools/typespec-<pkg>..." test
All tests should pass. If not, fix the implementation until they do.
Step 6: REGISTER IN RULESETS
Add the rule to the appropriate ruleset(s):
- Data-plane rules: Edit
packages/typespec-azure-rulesets/src/rulesets/data-plane.ts
- ARM rules: Edit
packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts
TCGC rules generally go in both rulesets. Rules in typespec-azure-core that apply to both ARM and data-plane specs also go in both rulesets. Only rules that are truly ARM-specific go exclusively in resource-manager.ts; otherwise, add the rule to both resource-manager.ts and data-plane.ts as appropriate.
Add an entry: "@azure-tools/typespec-<pkg>/<rule-name>": true,
Every rule MUST be explicitly listed (enabled or disabled). The validate-rules-defined.test.ts test will fail otherwise.
Verify:
pnpm --filter "@azure-tools/typespec-azure-rulesets..." build
pnpm --filter "@azure-tools/typespec-azure-rulesets..." test
Step 7: WRITE DOCUMENTATION AND REGENERATE DOCS
Edit packages/<pkg>/src/rules/<rule-name>.md:
- Replace all placeholder text
- Write a clear description of what the rule checks and why
- Provide realistic ❌ Incorrect and ✅ Correct examples using actual TypeSpec patterns
- Add a required
## Impact section: a bulleted **Area:** line (e.g. API, SDK, Emitters) followed by a short paragraph describing what breaks or degrades when the rule is violated
- Add a required
## Suppression section: state whether suppression is acceptable and why, and point to the correct fix. Suppressions use the #suppress "<rule-id>" "<justification-string>" directive placed on the line above the target — NOT a // suppress comment
- Add a
## LintDiff Equivalent section when an equivalent LintDiff (azure-openapi-validator) rule exists, deep-linking to the specific rule id in the OpenAPI authoring automated guidelines. Omit only when no equivalent exists
- This file holds only the extended documentation body — the page title, rule id, and
short description are generated from the rule definition.
tspd renders the page at
website/src/content/docs/docs/libraries/<pkg>/rules/<rule-name>.md
- For ARM rules, also update the alphabetized rule table in
website/src/content/docs/docs/howtos/ARM/arm-rules.md with the documentation URL,
LintDiff equivalent, and impact. This table is authored manually and is not updated
by regen-docs.
Then regenerate the library's reference docs (updates the rule listing):
pnpm --filter "@azure-tools/typespec-<pkg>..." build
pnpm --filter "@azure-tools/typespec-<pkg>" regen-docs
Step 8: CREATE CHANGESET
pnpm change add
When prompted:
- Select change kind:
feature (new rule) or fix (bugfix to existing rule)
- Select affected package:
@azure-tools/typespec-<pkg>
- Write a concise description: "Add
<rule-name> linter rule that "
Step 9: FINAL VALIDATION
Run the general pre-PR validation to ensure everything is ready:
pnpm validate:pr
This checks: branch is up to date, build passes, tests pass, lint passes, format is clean, spelling is clean, regen-docs is clean, changeset exists, and diff only contains expected files.
If any check fails, fix the issue and re-run. Use pnpm validate:pr --fix to auto-fix formatting and lint issues.
Step 10: EXTERNAL INTEGRATION CHECK
New linter rules MUST NOT break existing Azure service specs. After pushing your PR:
- Apply the
int:azure-specs label to the PR to trigger the External Integration check: gh pr edit --add-label "int:azure-specs"
- If the agent cannot apply the label automatically, tell the user to apply the
int:azure-specs label manually in the GitHub UI
- After applying the label, the External Integration workflow will start. Monitor it via
gh run list --workflow=external-integration.yml
- This workflow packages your changes and runs TypeSpec validation against all specs in
Azure/azure-rest-api-specs
- Wait for the check to pass before requesting review
If the check fails, your rule produces diagnostics on existing specs. To resolve:
- Apply an API-neutral fix to the spec (preferred): If the fix doesn't change API behavior, submit a PR to
Azure/azure-rest-api-specs on the main branch.
- Suppress the rule: If the spec cannot be fixed without changing API behavior, add a
#suppress "<rule-id>" "<justification-string>" directive on the line above the target (NOT a // suppress comment). Suppressions always go to the main branch.
- Fix on typespec-next: If the fix requires unreleased TypeSpec APIs or behavior, submit to the typespec-next branch (uses nightly builds).
- Link your spec fix PR: Always link the spec fix PR in your linter rule PR description.
The External Integration workflow:
- Builds and packs all typespec-azure packages from your PR
- Checks out
Azure/azure-rest-api-specs (main branch)
- Patches in your packaged changes
- Runs
tsp-integration azure-specs --stage validate
- Checks for unexpected git changes
Important Notes
- Import extensions: Always use
.js extensions in imports (e.g., from "./rules/my-rule.js")
- Rule URL: Must match
https://azure.github.io/typespec-azure/docs/libraries/<pkg>/rules/<rule-name>
- Test coverage: Aim for ≥ 95% branch coverage of the rule file
- Existing patterns: Look at similar existing rules in
packages/<pkg>/src/rules/ for reference
- ARM rules can import from azure-core:
import { ... } from "@azure-tools/typespec-azure-core"