Skip to content

Commit 62eb0c5

Browse files
Feature: Built-in linter engine (microsoft#2066)
Fix microsoft#1996 Implementation for the linter engine
1 parent 7824bbb commit 62eb0c5

36 files changed

+1595
-577
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@typespec/compiler",
5+
"comment": "**Feature** New built-in linter system. Typespec libraries are able to define linting rules which can be configured in `tspconfig.yaml`. See documentation for configuring a [linter](https://microsoft.github.io/typespec/introduction/configuration#linter---configuring-linters) and [writing a linter](https://microsoft.github.io/typespec/extending-typespec/linters)",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@typespec/compiler"
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@typespec/lint",
5+
"comment": "**Deprecation** Package is deprecated in favor of built-in linter system",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@typespec/lint"
10+
}

common/config/rush/pnpm-lock.yaml

+459-414
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/extending-typespec/linters.md

+101-84
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,54 @@ title: Linters
99

1010
TypeSpec library can probide a `$onValidate` hook which can be used to validate the typespec program is valid in the eye of your library.
1111

12-
A linter on the other hand might be a validation that is more optional, the program is correct but there could be some improvements. For example requiring documentation on every type. This is not something that is needed to represent the typespec program but without it the end user experience might suffer.
12+
A linter on the other hand might be a validation that is optional, the program is correct but there could be some improvements. For example requiring documentation on every type. This is not something that is needed to represent the typespec program but without it the end user experience might suffer.
13+
Linters need to be explicitly enabled. `$onValidate` will be run automatically if that library is imported.
1314

1415
## Writing a linter
1516

16-
There is no built-in concept of linter into TypeSpec, there is however a library `@typespec/lint` that lets a library define its linting rules and hooks on to the `onValidate`.
17+
See examples in `packages/best-practices`.
1718

18-
### 1. Install the `@typespec/lint` package
19-
20-
```bash
21-
npm install @typespec/lint
22-
```
23-
24-
### 2. Define the rules
19+
### 1. Define a rules
2520

2621
```ts
27-
import { createRule } from "@typespec/lint";
22+
import { createLinterRule } from "@typespec/compiler";
2823
import { reportDiagnostic } from "../lib.js";
2924

30-
export const modelDocRule = createRule({
25+
export const requiredDocRule = createLinterRule({
3126
name: "no-model-doc",
32-
create({ program }) {
27+
severity: "warning",
28+
// Short description of what this linter rule does. To be used for generated summary of a linter.
29+
description: "Enforce documentation on models.",
30+
messages: {
31+
default: `Must be documented.`,
32+
// Different messages can be provided
33+
models: `Models must be documented.`,
34+
35+
// Message can be parameterized
36+
enums: paramMessage`Enum ${"enumName"} must be documented.`,
37+
},
38+
create(context) {
3339
return {
40+
operation: (op) => {
41+
if (!getDoc(context.program, op)) {
42+
context.reportDiagnostic({
43+
target: model,
44+
});
45+
}
46+
},
3447
model: (model) => {
35-
if (!getDoc(program, model)) {
36-
reportDiagnostic(program, {
37-
// Note 1
38-
code: "no-model-doc",
48+
if (!getDoc(context.program, model)) {
49+
context.reportDiagnostic({
50+
messageId: "models",
51+
target: model,
52+
});
53+
}
54+
},
55+
enums: (type) => {
56+
if (!getDoc(context.program, type)) {
57+
context.reportDiagnostic({
58+
messageId: "enums",
59+
format: {enumName: type.name}
3960
target: model,
4061
});
4162
}
@@ -45,98 +66,94 @@ export const modelDocRule = createRule({
4566
});
4667
```
4768

48-
Note 1: `code` refers to they key of a diagnostic defined when you `createTypeSpecLibrary` [See](./basics.md#4-create-libts)
69+
#### Don'ts
4970

50-
### Register the rules
51-
52-
<!-- cspell:disable-next-line -->
53-
54-
`$lib` refer to the value of `createTypeSpecLibrary` [See](./basics.md#4-create-libts)
71+
- ❌ Do not call `program.reportDiagnostic` or your library `reportDiagnostic` helper directly in a linter rule
5572

5673
```ts
57-
import { $lib } from "../lib.js";
58-
import { modelDocRule } from "./rules/model-doc.js";
59-
60-
// Get the instance of the linter for your library
61-
const linter = getLinter($lib);
62-
63-
linter.registerRule(modelDocRule);
64-
```
65-
66-
Or multiple rules at once
67-
68-
```ts
69-
linter.registerRules([modelDocRule, interfaceDocRule]);
70-
```
71-
72-
When registering a rule, its name will be prefixed by the library named defined in `$lib`.
73-
74-
### Enable the rules
75-
76-
Rules are by default just registered but not enabled. This allows a library to provide a set of linting rules for other libraries to use or a user to enable.
74+
// ❌ Bad
75+
program.reportDiagnostic({
76+
code: "other-code",
77+
target,
78+
});
79+
// ❌ Bad
80+
reportDiagnostic(program, {
81+
code: "other-code",
82+
target,
83+
});
7784

78-
```ts
79-
// Note: The rule ID here needs to be the fully qualified rule name prefixed with the
80-
// `<libraryname>/` prefix and it cannot be a rule provided by another library.
81-
linter.enableRule("my-library/no-model-doc");
85+
// ✅ Good
86+
context.reportDiagnostic({
87+
target,
88+
});
8289
```
8390

84-
Alternatively rules can be automatically enabled when registered.
85-
86-
```ts
87-
// With single registration
88-
linter.registerRule(modelDocRule, { autoEnable: true });
89-
90-
// With multi registration
91-
linter.registerRules([modelDocRule, interfaceDocRule], { autoEnable: true });
92-
```
91+
### Register the rules
9392

94-
### Register the linter hook
93+
<!-- cspell:disable-next-line -->
9594

96-
The lint library still depends on `$onValidate` to run. For that each library providing a linter should call `linter.lintOnValidate(program);` to ensure that the linter will be run.
95+
When defining your `$lib` with `createTypeSpecLibrary`([See](./basics.md#4-create-libts)) an additional entry for `linter` can be provided
9796

9897
```ts
99-
export function $onValidate(program: Program) {
100-
// Optional if you want to automatically enable your rules.
101-
linter.autoEnableRules();
102-
103-
// Alternatively enable rules explicitly.
104-
// Note: The rule IDs here needs to be the fully qualified rule names with the
105-
// `<libraryname>/` prefix and they cannot be rules provided by other libraries.
106-
linter.enableRules(["<library-name>/<rule-name-1>", "<library-name>/<rule-name-2>]);
107-
108-
linter.lintOnValidate(program);
109-
}
98+
// Import the rule defined previously
99+
import { requiredDocRule } from "./rules/required-doc.rule.js";
100+
101+
export const $lib = createTypeSpecLibrary({
102+
name: "@typespec/my-linter",
103+
diagnostics: {},
104+
linter: {
105+
// Include all the rules your linter is defining here.
106+
rules: [requiredDocRule],
107+
108+
// Optionally a linter can provide a set of rulesets
109+
ruleSets: {
110+
recommended: {
111+
// (optional) A ruleset takes a map of rules to explicitly enable
112+
enable: { [`@typespec/my-linter:${requiredDocRule.name}`]: true },
113+
114+
// (optional) A rule set can extend another rule set
115+
extends: ["@typespec/best-practices:recommended"],
116+
117+
// (optional) A rule set can disable a rule enabled in a ruleset it extended.
118+
disable: {
119+
"`@typespec/best-practices:no-a": "This doesn't apply in this ruleset.",
120+
},
121+
},
122+
},
123+
},
124+
});
110125
```
111126

112-
This will not run the linter right here, it will just add a new callback to the onValidate list giving time for all linters to register their rules.
127+
When referencing a rule or ruleset(in `enable`, `extends`, `disable`) the rule or rule set id must be used which in this format: `<libraryName>:<ruleName>`
113128

114129
## Testing a linter
115130

116131
To test linter rule an rule tester is provided letting you test a specific rule without enabling the others.
117132

118-
First you'll want to create an instance of the rule tester using `createRuleTester` passing it the rule that is being tested.
133+
First you'll want to create an instance of the rule tester using `createLinterRuleTester` passing it the rule that is being tested.
119134
You can then provide different test checking the rule pass or fails.
120135

121136
```ts
122-
import { createRuleTester } from "@typespec/lint/testing";
123-
import { noFooModelRule } from "./no-foo-model.js";
137+
import { createLinterRuleTester, RuleTester, createTestRunner } from "@typespec/compiler/testing";
138+
import { requiredDocRule } from "./rules/required-doc.rule.js";
124139

125-
let ruleTester: RuleTester;
140+
describe("required-doc rule", () => {
141+
let ruleTester: RuleTester;
126142

127-
beforeEach(() => {
128-
const runner = createTestRunner();
129-
ruleTester = createRuleTester(runner, noFooModelRule);
130-
});
143+
beforeEach(() => {
144+
const runner = createTestRunner();
145+
ruleTester = createLinterRuleTester(runner, requiredDocRule, "@typespec/my-linter");
146+
});
131147

132-
it("emit diagnostics when using model named foo", () => {
133-
ruleTester.expect(`model Foo {}`).toEmitDiagnostic({
134-
code: "my-library/no-foo-model",
135-
message: "Cannot name a model with 'Foo'",
148+
it("emit diagnostics when using model named foo", async () => {
149+
await ruleTester.expect(`model Foo {}`).toEmitDiagnostics({
150+
code: "@typespec/my-linter:no-foo-model",
151+
message: "Cannot name a model with 'Foo'",
152+
});
136153
});
137-
});
138154

139-
it("should be valid to use other names", () => {
140-
ruleTester.expect(`model Bar {}`).toBeValid();
155+
it("should be valid to use other names", async () => {
156+
await ruleTester.expect(`model Bar {}`).toBeValid();
157+
});
141158
});
142159
```

docs/introduction/configuration/configuration.md

+24
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ model TypeSpecProjectSchema {
3636
imports?: string;
3737
emit?: string[];
3838
options?: Record<unknown>;
39+
linter?: LinterConfig;
40+
}
41+
42+
model LinterConfig {
43+
extends?: RuleRef[];
44+
enable?: Record<RuleRef, boolean>;
45+
disable?: Record<RuleRef, string>;
3946
}
4047
```
4148

@@ -182,6 +189,7 @@ options:
182189
| `imports` | `--import` | Additional imports to include |
183190
| `emit` | `--emit` | Emitter configuration |
184191
| `options` | `--option` or `--options` | Emitter configuration |
192+
| `linter` | | Linter configuration |
185193

186194
### `output-dir` - Configure the default output dir
187195

@@ -301,6 +309,22 @@ Default: `{output-dir}/{emitter-name}`
301309

302310
See [output directory configuration for mode details](#output-directory-configuration)
303311

312+
### `linter` - Configuring linters
313+
314+
Configure which linter rules should be enabled in this repository. Referencing to a rule or ruleset must be using their id which is in this format `<libraryName>:<ruleName>`
315+
316+
```yaml
317+
linter:
318+
extends: # Extend `recommended` ruleset from @typespec/best-practices library
319+
- "@typespec/best-practices:recommended"
320+
321+
enable: # Explicitly enable some rules
322+
"@typespec/best-practices:no-x": true
323+
324+
disable: # Disable some rules defined in one of the ruleset extended.
325+
"@typespec/best-practices:no-y": "This rule cannot be applied in this project because X"
326+
```
327+
304328
## Emitter control cli flags
305329
306330
### `--no-emit`

docs/introduction/configuration/tracing.md

+11-6
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,17 @@ Using:
5858

5959
This is a list of the trace area used in the compiler
6060

61-
| Area | Description |
62-
| --------------------------- | -------------------------------------------------------------------- |
63-
| `compiler.options` | Log the resolved compiler options |
64-
| `import-resolution.library` | Information related to the resolution of import libraries |
65-
| `projection.log` | Debug information logged by the `log()` function used in projections |
66-
| `bind.js` | Information when binding JS files |
61+
| Area | Description |
62+
| ------------------------------ | -------------------------------------------------------------------- |
63+
| `compiler.options` | Log the resolved compiler options |
64+
| `import-resolution.library` | Information related to the resolution of import libraries |
65+
| `projection.log` | Debug information logged by the `log()` function used in projections |
66+
| `bind.js` | Information when binding JS files |
67+
| `linter.register-library` | Information that a library rules will be loaded |
68+
| `linter.register-library.rule` | Information about a rule that is being registered |
69+
| `linter.extend-rule-set.start` | Information about a ruleset it is about to extend |
70+
| `linter.extend-rule-set.end` | Information about rules enabled after extending a ruleset |
71+
| `linter.lint` | Start the lint process and show information of all the rules enabled |
6772

6873
## Tracing in TypeSpec library
6974

packages/best-practices/.c8rc.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"reporter": ["cobertura", "json", "text"]
3+
}

packages/best-practices/.eslintrc.cjs

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
require("@typespec/eslint-config-typespec/patch/modern-module-resolution");
2+
3+
module.exports = {
4+
plugins: ["@typespec/eslint-plugin"],
5+
extends: ["@typespec/eslint-config-typespec", "plugin:@typespec/eslint-plugin/recommended"],
6+
parserOptions: { tsconfigRootDir: __dirname },
7+
};

packages/best-practices/.mocharc.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
timeout: 5000
2+
require: source-map-support/register
3+
spec: "dist/test/**/*.test.js"
4+
ignore: "dist/test/manual/**/*.js"
5+
6+
# Config for https://www.npmjs.com/package/mocha-multi-reporters
7+
reporterOptions: "configFile=mocha.reporter.config.json"

packages/best-practices/README.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# TypeSpec Best Practices
2+
3+
**WORK IN PROGRESS This is not ready for consumption**
4+
5+
Set of linter rules to enforce TypeSpec best practices
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"reporterEnabled": "spec, mocha-junit-reporter"
3+
}

0 commit comments

Comments
 (0)