Skip to content

Linters

Linter versus onValidate

A TypeSpec library can provide an $onValidate hook, which can be used to validate whether the TypeSpec program is valid according to your libraryโ€™s rules.

On the other hand, a linter might provide optional validation. The program could be correct, but there might be room for improvements. For instance, a linter might require documentation on every type. While this isnโ€™t necessary to represent the TypeSpec program, it could enhance the end user experience. Linters need to be explicitly enabled, whereas $onValidate will run automatically if that library is imported.

Creating a linter

You can find examples in packages/best-practices.

1. Define rules

import { createLinterRule } from "@typespec/compiler";
import { reportDiagnostic } from "../lib.js";
export const requiredDocRule = createLinterRule({
name: "no-model-doc",
severity: "warning",
// Short description of what this linter rule does. To be used for generated summary of a linter.
description: "Enforce documentation on models.",
messages: {
default: `Must be documented.`,
// Different messages can be provided
models: `Models must be documented.`,
// Message can be parameterized
enums: paramMessage`Enum ${"enumName"} must be documented.`,
},
create(context) {
return {
operation: (op) => {
if (!getDoc(context.program, op)) {
context.reportDiagnostic({
target: model,
});
}
},
model: (model) => {
if (!getDoc(context.program, model)) {
context.reportDiagnostic({
messageId: "models",
target: model,
});
}
},
enums: (type) => {
if (!getDoc(context.program, type)) {
context.reportDiagnostic({
messageId: "enums",
format: {enumName: type.name}
target: model,
});
}
},
};
},
});

Provide a codefix

See codefixes for more details on how codefixes work in the TypeSpec ecosystem.

In the same way you can provide a codefix on any reported diagnostic, you can pass codefixes to the reportDiagnostic function.

context.reportDiagnostic({
messageId: "models",
target: model,
codefixes: [
defineCodeFix({
id: "add-model-suffix",
label: "Add 'Model' suffix to model name",
fix: (program) => {
program.update(model, {
name: `${model.name}Model`,
});
},
}),
],
});

Things to avoid

  • โŒ Do not call program.reportDiagnostic or your library reportDiagnostic helper directly in a linter rule
// โŒ Bad
program.reportDiagnostic({
code: "other-code",
target,
});
// โŒ Bad
reportDiagnostic(program, {
code: "other-code",
target,
});
// โœ… Good
context.reportDiagnostic({
target,
});

Register the rules

Export a $linter variable from your library entrypoint:

index.ts
export { $linter } from "./linter.js";
linter.ts
import { defineLinter } from "@typespec/compiler";
// Import the rule defined previously
import { requiredDocRule } from "./rules/required-doc.rule.js";
export const $linter = defineLinter({
// Include all the rules your linter is defining here.
rules: [requiredDocRule],
// Optionally a linter can provide a set of rulesets
ruleSets: {
recommended: {
// (optional) A ruleset takes a map of rules to explicitly enable
enable: { [`@typespec/my-linter/${requiredDocRule.name}`]: true },
// (optional) A rule set can extend another rule set
extends: ["@typespec/best-practices/recommended"],
// (optional) A rule set can disable a rule enabled in a ruleset it extended.
disable: {
"`@typespec/best-practices/no-a": "This doesn't apply in this ruleset.",
},
},
},
});

When referencing a rule or ruleset (in enable, extends, disable), you must use the rule or rule set id, which is in this format: <libraryName>/<ruleName>.

Testing a linter

To test a linter rule, a rule tester is provided, allowing you to test a specific rule without enabling the others.

First, youโ€™ll want to create an instance of the rule tester using createLinterRuleTester, passing it the rule that is being tested. You can then provide different tests to check whether the rule passes or fails.

import { RuleTester, createLinterRuleTester, createTestRunner } from "@typespec/compiler/testing";
import { requiredDocRule } from "./rules/required-doc.rule.js";
describe("required-doc rule", () => {
let ruleTester: RuleTester;
beforeEach(() => {
const runner = createTestRunner();
ruleTester = createLinterRuleTester(runner, requiredDocRule, "@typespec/my-linter");
});
it("emit diagnostics when using model named foo", async () => {
await ruleTester.expect(`model Foo {}`).toEmitDiagnostics({
code: "@typespec/my-linter/no-foo-model",
message: "Cannot name a model with 'Foo'",
});
});
it("should be valid to use other names", async () => {
await ruleTester.expect(`model Bar {}`).toBeValid();
});
});

Testing linter with codefixes

The linter rule tester provides an API to easily test a codefix. This is a different approach from the standalone codefix tester, which is more targeted at testing codefixes in isolation.

This can be done by calling applyCodeFix with the fix id. It will expect a single diagnostic to be emitted with a codefix with the given id. Then, call toEqual with the expected code after the codefix is applied.

await tester
.expect(
`
model Foo {}
`,
)
.applyCodeFix("add-model-suffix").toEqual(`
model FooModel {}
`);