Skip to content

Commit 38aa320

Browse files
Add ref docs for linter rules (microsoft#2668)
Add some generated doc for the linter capability of a library. Each linter rule can also provide a url pointing to the the full documentation where we can get a more detailed set of examples of what is good and what is bad. Example for http linter https://tspwebsitepr.z22.web.core.windows.net/prs/2668/next/standard-library/http/reference/linter.html ![image](https://github.com/microsoft/typespec/assets/1031227/81c2dddc-fa0f-40dd-bad7-9d193f764c55) ![image](https://github.com/microsoft/typespec/assets/1031227/36652cd5-418b-43b9-bd9a-aff046cbe538) --------- Co-authored-by: Mark Cowlishaw <markcowl@microsoft.com>
1 parent 90b7b02 commit 38aa320

File tree

14 files changed

+305
-12
lines changed

14 files changed

+305
-12
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@typespec/compiler",
5+
"comment": "Linter rules can provide a url to the full documentation",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@typespec/compiler"
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@typespec/http",
5+
"comment": "",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@typespec/http"
10+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
title: "Linter usage"
3+
toc_min_heading_level: 2
4+
toc_max_heading_level: 3
5+
---
6+
7+
# Linter
8+
9+
## Usage
10+
11+
Add the following in `tspconfig.yaml`:
12+
13+
```yaml
14+
linter:
15+
extends:
16+
- "@typespec/http/all"
17+
```
18+
19+
## RuleSets
20+
21+
Available ruleSets:
22+
23+
- [`@typespec/http/all`](#@typespec/http/all)
24+
25+
## Rules
26+
27+
| Name | Description |
28+
| ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
29+
| [`@typespec/http/op-reference-container-route`](/standard-library/http/rules/op-reference-container-route.md) | Check for referenced (`op is`) operations which have a @route on one of their containers. |
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
title: "op-reference-container-route"
3+
---
4+
5+
```text title="Id"
6+
@typespec/http/op-reference-container-route
7+
```
8+
9+
Check for referenced (`op is`) operations which have a `@route` on one of their containers.
10+
11+
When referencing an operation with `op is` only the data on the operation itself is carried over anything on parent container is lost.
12+
This result in unexpected behavior where information is lost.
13+
As a best practice the route should be provided on the operation itself.
14+
15+
#### ❌ Incorrect
16+
17+
```tsp
18+
namespace Library {
19+
@route("/pets")
20+
interface Pets {
21+
@route("/read") read(): string;
22+
}
23+
}
24+
25+
@service
26+
namespace Service {
27+
interface PetStore {
28+
readPet is Library.Pets.read;
29+
}
30+
}
31+
```
32+
33+
#### ✅ Correct
34+
35+
```tsp
36+
namespace Library {
37+
interface Pets {
38+
@route("/pets/read") read(): string;
39+
}
40+
}
41+
42+
@service
43+
namespace Service {
44+
interface PetStore {
45+
readPet is Library.Pets.read;
46+
}
47+
}
48+
```

packages/compiler/src/core/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2022,10 +2022,17 @@ export interface LinterDefinition {
20222022
}
20232023

20242024
export interface LinterRuleDefinition<N extends string, DM extends DiagnosticMessages> {
2025+
/** Rule name (without the library name) */
20252026
name: N;
2027+
/** Rule default severity. */
20262028
severity: "warning";
2029+
/** Short description of the rule */
20272030
description: string;
2031+
/** Specifies the URL at which the full documentation can be accessed. */
2032+
url?: string;
2033+
/** Messages that can be reported with the diagnostic. */
20282034
messages: DM;
2035+
/** Creator */
20292036
create(context: LinterRuleContext<DM>): SemanticNodeListener;
20302037
}
20312038

packages/http/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,30 @@ TypeSpec HTTP protocol binding
88
npm install @typespec/http
99
```
1010

11+
## Linter
12+
13+
### Usage
14+
15+
Add the following in `tspconfig.yaml`:
16+
17+
```yaml
18+
linter:
19+
extends:
20+
- "@typespec/http/all"
21+
```
22+
23+
### RuleSets
24+
25+
Available ruleSets:
26+
27+
- [`@typespec/http/all`](#@typespec/http/all)
28+
29+
### Rules
30+
31+
| Name | Description |
32+
| ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
33+
| [`@typespec/http/op-reference-container-route`](https://microsoft.github.io/typespec/standard-library/http/rules/op-reference-container-route) | Check for referenced (`op is`) operations which have a @route on one of their containers. |
34+
1135
## Decorators
1236

1337
### TypeSpec.Http

packages/http/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"author": "Microsoft Corporation",
55
"description": "TypeSpec HTTP protocol binding",
66
"homepage": "https://github.com/microsoft/typespec",
7+
"docusaurusWebsite": "https://microsoft.github.io/typespec",
78
"readme": "https://github.com/microsoft/typespec/blob/main/README.md",
89
"license": "MIT",
910
"repository": {

packages/http/src/rules/op-reference-container-route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { OperationContainer } from "../types.js";
44

55
export const opReferenceContainerRouteRule = createRule({
66
name: "op-reference-container-route",
7+
severity: "warning",
78
description:
89
"Check for referenced (`op is`) operations which have a @route on one of their containers.",
9-
severity: "warning",
10+
url: "https://microsoft.github.io/typespec/standard-library/http/rules/op-reference-container-route",
1011
messages: {
1112
default: paramMessage`Operation ${"opName"} references an operation which has a @route prefix on its namespace or interface: "${"routePrefix"}". This operation will not carry forward the route prefix so the final route may be different than the referenced operation.`,
1213
},

packages/tspd/src/ref-doc/emitters/docusaurus.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { MarkdownRenderer, groupByNamespace } from "./markdown.js";
1919
* Render doc to a markdown using docusaurus addons.
2020
*/
2121
export function renderToDocusaurusMarkdown(refDoc: TypeSpecRefDoc): Record<string, string> {
22-
const renderer = new DocusaurusRenderer();
22+
const renderer = new DocusaurusRenderer(refDoc);
2323
const files: Record<string, string> = {
2424
"index.mdx": renderIndexFile(renderer, refDoc),
2525
};
@@ -43,6 +43,10 @@ export function renderToDocusaurusMarkdown(refDoc: TypeSpecRefDoc): Record<strin
4343
if (emitter) {
4444
files["emitter.md"] = emitter;
4545
}
46+
const linter = renderLinter(renderer, refDoc);
47+
if (linter) {
48+
files["linter.md"] = linter;
49+
}
4650

4751
return files;
4852
}
@@ -231,8 +235,31 @@ function renderEmitter(
231235

232236
return renderMarkdowDoc(content);
233237
}
238+
function renderLinter(
239+
renderer: DocusaurusRenderer,
240+
refDoc: TypeSpecLibraryRefDoc
241+
): string | undefined {
242+
if (refDoc.linter === undefined) {
243+
return undefined;
244+
}
245+
const content: MarkdownDoc = [
246+
"---",
247+
`title: "Linter usage"`,
248+
"toc_min_heading_level: 2",
249+
"toc_max_heading_level: 3",
250+
"---",
251+
renderer.linterUsage(refDoc),
252+
];
253+
254+
return renderMarkdowDoc(content);
255+
}
234256

235257
export class DocusaurusRenderer extends MarkdownRenderer {
258+
#refDoc: TypeSpecLibraryRefDoc;
259+
constructor(refDoc: TypeSpecLibraryRefDoc) {
260+
super();
261+
this.#refDoc = refDoc;
262+
}
236263
headingTitle(item: NamedTypeRefDoc): string {
237264
// Set an explicit anchor id.
238265
return `${inlinecode(item.name)} {#${item.id}}`;
@@ -259,4 +286,14 @@ export class DocusaurusRenderer extends MarkdownRenderer {
259286
])
260287
);
261288
}
289+
290+
linterRuleLink(url: string) {
291+
const homepage = (this.#refDoc.packageJson as any).docusaurusWebsite;
292+
if (homepage && url.includes(homepage)) {
293+
const fromRoot = url.replace(homepage, "");
294+
return `${fromRoot}.md`;
295+
} else {
296+
return url;
297+
}
298+
}
262299
}

packages/tspd/src/ref-doc/emitters/markdown.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import { resolvePath } from "@typespec/compiler";
22
import { readFile } from "fs/promises";
3+
import { stringify } from "yaml";
34
import {
45
DecoratorRefDoc,
56
EmitterOptionRefDoc,
67
EnumRefDoc,
78
ExampleRefDoc,
89
InterfaceRefDoc,
10+
LinterRuleRefDoc,
911
ModelRefDoc,
1012
NamedTypeRefDoc,
1113
NamespaceRefDoc,
1214
OperationRefDoc,
15+
ReferencableElement,
1316
ScalarRefDoc,
1417
TemplateParameterRefDoc,
1518
TypeSpecRefDoc,
@@ -20,6 +23,7 @@ import {
2023
MarkdownDoc,
2124
codeblock,
2225
inlinecode,
26+
link,
2327
renderMarkdowDoc,
2428
section,
2529
table,
@@ -56,6 +60,10 @@ export async function renderReadme(refDoc: TypeSpecRefDoc, projectRoot: string)
5660
content.push(renderer.emitterUsage(refDoc));
5761
}
5862

63+
if (refDoc.linter) {
64+
content.push(renderer.linterUsage(refDoc));
65+
}
66+
5967
if (refDoc.namespaces.some((x) => x.decorators.length > 0)) {
6068
content.push(section("Decorators", renderer.decoratorsSection(refDoc, { includeToc: true })));
6169
}
@@ -85,7 +93,7 @@ export class MarkdownRenderer {
8593
return inlinecode(item.name);
8694
}
8795

88-
anchorId(item: NamedTypeRefDoc): string {
96+
anchorId(item: ReferencableElement): string {
8997
return `${item.name.toLowerCase().replace(/ /g, "-")}`;
9098
}
9199

@@ -233,7 +241,7 @@ export class MarkdownRenderer {
233241
});
234242
}
235243

236-
toc(items: NamedTypeRefDoc[], filename?: string) {
244+
toc(items: ReferencableElement[], filename?: string) {
237245
return items.map(
238246
(item) => ` - [${inlinecode(item.name)}](${filename ?? ""}#${this.anchorId(item)})`
239247
);
@@ -270,4 +278,37 @@ export class MarkdownRenderer {
270278
}
271279
return section("Emitter options", content);
272280
}
281+
282+
linterUsage(refDoc: TypeSpecRefDoc) {
283+
if (refDoc.linter === undefined) {
284+
return [];
285+
}
286+
const setupExample = stringify({
287+
linter: refDoc.linter.ruleSets
288+
? { extends: [refDoc.linter.ruleSets[0].name] }
289+
: { rules: {} },
290+
});
291+
return section("Linter", [
292+
section("Usage", ["Add the following in `tspconfig.yaml`:", codeblock(setupExample, "yaml")]),
293+
refDoc.linter.ruleSets
294+
? section("RuleSets", ["Available ruleSets:", this.toc(refDoc.linter.ruleSets)])
295+
: [],
296+
section("Rules", this.linterRuleToc(refDoc.linter.rules)),
297+
]);
298+
}
299+
300+
linterRuleToc(rules: LinterRuleRefDoc[]) {
301+
return table([
302+
["Name", "Description"],
303+
...rules.map((rule) => {
304+
const name = inlinecode(rule.name);
305+
const nameCell = rule.rule.url ? link(name, this.linterRuleLink(rule.rule.url)) : name;
306+
return [nameCell, rule.rule.description];
307+
}),
308+
]);
309+
}
310+
311+
linterRuleLink(url: string) {
312+
return url;
313+
}
273314
}

0 commit comments

Comments
 (0)