id | title |
---|---|
linters |
Linters |
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.
You can find examples in packages/best-practices
.
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,
});
}
},
};
},
});
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`,
});
},
}),
],
});
- ❌ Do not call
program.reportDiagnostic
or your libraryreportDiagnostic
helper directly in a linter rule
// ❌ Bad
program.reportDiagnostic({
code: "other-code",
target,
});
// ❌ Bad
reportDiagnostic(program, {
code: "other-code",
target,
});
// ✅ Good
context.reportDiagnostic({
target,
});
Export a $linter
variable from your library entrypoint:
export { $linter } from "./linter.js";
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>
.
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();
});
});
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.
:::note When using multi-line strings (with ```) in TypeScript, there is no de-indenting done, so you will need to make sure the input and expected result are aligned to the left. :::
await tester
.expect(
`
model Foo {}
`
)
.applyCodeFix("add-model-suffix").toEqual(`
model FooModel {}
`);