-
Notifications
You must be signed in to change notification settings - Fork 46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[tcgc] add linter rulesets framework #1208
base: main
Are you sure you want to change the base?
Changes from all commits
e8b5ac6
69bd8f1
befc3c8
667dd62
e35c3e4
b5cce28
57d2f48
1684417
01dd7e4
2de1f09
a46581e
fc765ce
a83e2c2
23e0e77
ae9e97b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
changeKind: feature | ||
packages: | ||
- "@azure-tools/typespec-client-generator-core" | ||
--- | ||
|
||
add linter rulesets to TCGC, for both generic and language-specific linter rules |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
changeKind: feature | ||
packages: | ||
- "@azure-tools/typespec-azure-rulesets" | ||
--- | ||
|
||
add some tcgc rules to the list |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
--- | ||
title: "Linter usage" | ||
toc_min_heading_level: 2 | ||
toc_max_heading_level: 3 | ||
--- | ||
|
||
# Linter | ||
|
||
## Usage | ||
|
||
Add the following in `tspconfig.yaml`: | ||
|
||
```yaml | ||
linter: | ||
extends: | ||
- "@azure-tools/typespec-client-generator-core/all" | ||
``` | ||
|
||
## RuleSets | ||
|
||
Available ruleSets: | ||
|
||
- `@azure-tools/typespec-client-generator-core/all` | ||
- `@azure-tools/typespec-client-generator-core/best-practices:csharp` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does best practice mean here? Does it mean this is optional? If so is there another category which is not optional? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. best practices is what I've called the ruleset for now, open to suggestions! I think the advice from the typespec team has been to use the word "best-practices", @timotheeguerin is this correct? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aren't these rulesets optional and can be opted out? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh I don't think we have a strong opinion on that name, we just have a placeholder I would say name this however makes the most sense. But I would note that I don't think this really should spend too much time on this as for all of azure they MUST use either the I expect those more targeted ruleset might be more useful when we have the non azure version of tcgc(e.g. |
||
|
||
## Rules | ||
|
||
| Name | Description | | ||
| -------------------------------------------------------------------- | ----------------------------------------------------------------------- | | ||
| `@azure-tools/typespec-client-generator-core/require-client-suffix` | Client names should end with 'Client'. | | ||
iscai-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| `@azure-tools/typespec-client-generator-core/property-name-conflict` | Avoid naming conflicts between a property and a model of the same name. | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a required rule or is it a recommendation to avoid property and model having the same name. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. all rules are in a sense optional its up to the user to enable them(directly or via a ruleset). For azure specs we require that the |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# Contributing Linter Rules to the @azure-tools/typespec-client-generator-core Package | ||
|
||
For information about linter rules in typespec in general, view [here][generic-linter] | ||
|
||
In the `@azure-tools/typespec-client-generator-core` library, we have two main types of rules: | ||
|
||
1. Generic rule that applies to all emitted languages | ||
2. Specific rule that applies to a subset of all languages: it can even apply to just one language | ||
|
||
The process for adding a rule starts off the same for both: for language-specific rules, there is an extra step | ||
|
||
1. Write the rule in `typespec-azure/packages/typespec-client-generator-core/src/rules/[rule-name].rule.ts | ||
2. Add reference to the rule in the `rules` array in [`typespec-azure/packages/typespec-client-generator-core/src/linter.ts`][tcgc-linter] | ||
- This will automatically add it to a ruleset called `:all` for `@azure-tools/typespec-client-generator-core` | ||
3. Add the rule to the enable list for [`data-plane.ts`][data-plane-ruleset] and/or [`resource-manager.ts`][resource-manager-ruleset] in the [rulesets][rulesets] package. You can set `enable` to `false` here, if you want to delay enabling | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we really want to have different rules for data plane and mgmt plane? I know we are less strict with mgmt plane, but aren't the recommendations the same for both? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's the current setup of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there is some rules that management plane specific and some that shouldn't apply to mamangement plane. But the majority should apply to both There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have a single place to add a rule that applies to both or do we need to add the rule explicitly to both lists? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see step 3 done in this PR. Will this be added in a follow-up PR? |
||
|
||
**If you are adding a language-specific rule**, you will also need this extra step 4. Add reference to the rule in the `[language]Rules` array in [`typespec-azure/packages/typespec-client-generator-core/src/linter.ts`][tcgc-linter] | ||
|
||
For Azure generations then, all rules, including all language-specific rules, will be run on the specs. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we want to limit this to the Tier 1 languages not necessarily There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's fair, let me update this to say that language-agnostic rules, and tier-1 languages will be run |
||
For unbranded generations, since we've added the rules into specific `best-practices:[language]` rulesets, you can explicitly specify a subset of rules in your `tsp-config.yaml`, i.e. if I only want Python best-practices, I could add this in my `tsp-config.yaml`: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This might create a bad situation where someone can turn off csharp rules but in reality they cannot. I think we might want to consider what flexibility a user would have over tcgc rules when running in azure context. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. every azure service is enforced to have one of the 2 rules set enabled by the spec repo pipeline. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With rulesets, we have to give users the ability to turn them off because they are ultimately warnings. After talking with @timotheeguerin, you can't have error linter rules, only warnings. If there is a case where you explicitly want to error in csharp for a csharp-specific issue, the csharp emitter itself can throw a fatal error with |
||
|
||
```yaml | ||
linter: | ||
extends: | ||
- best-practices: python | ||
``` | ||
|
||
Finally, we recommend that every warning or error you throw in your language emitter has a corresponding warning in TCGC. This is part of our shift-left policy, so tsp authors can catch these potential pitfalls earlier in the process. | ||
|
||
### Links | ||
|
||
[generic-linter]: https://typespec.io/docs/next/extending-typespec/linters "Generic Linter Docs" | ||
[tcgc-linter]: https://github.com/Azure/typespec-azure/blob/main/packages/typespec-client-generator-core/src/linter.ts "Linter TS File" | ||
[rulesets]: https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-rulesets "Rulesets package" | ||
[data-plane-ruleset]: https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-rulesets/src/rulesets/data-plane.ts "Data Plane Ruleset" | ||
[resource-manager-ruleset]: https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts "Resource Manager Ruleset" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
--- | ||
title: "property-name-conflict" | ||
--- | ||
|
||
```text title="Full name" | ||
@azure-tools/typespec-client-generator-core/property-name-conflict | ||
``` | ||
|
||
Verify that there isn't a name conflict between a property in the model, and the name of the model itself | ||
|
||
#### ❌ Incorrect | ||
iscai-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```ts | ||
model Widget { | ||
widget: string; | ||
} | ||
``` | ||
|
||
#### ✅ Ok | ||
|
||
```ts | ||
model Widget { | ||
widgetName: string; | ||
} | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
--- | ||
title: "require-client-suffix" | ||
--- | ||
|
||
```text title="Full name" | ||
@azure-tools/typespec-client-generator-core/require-client-suffix | ||
``` | ||
|
||
Verify that the generated client's name will have the suffix `Client` in its name | ||
|
||
#### ❌ Incorrect | ||
|
||
```ts | ||
// main.tsp | ||
namespace MyService; | ||
``` | ||
|
||
```ts | ||
// client.tsp | ||
namespace MyCustomizations; | ||
|
||
@@client(MyService); | ||
``` | ||
|
||
#### ✅ Recommended | ||
|
||
If you would not like to make any changes to the generated client besides its name, you can rely on the [`@clientName`][client-name] decorator to rename the main namespace of the service. | ||
|
||
```ts | ||
@@clientName(MyService, "MyClient"); | ||
``` | ||
|
||
#### ✅ Ok | ||
|
||
If you are completely recreating a client namespace in your `client.tsp`, you can rely on that namespace to end in `Client` | ||
|
||
```ts | ||
// client.tsp | ||
@client({service: MyService}) | ||
namespace MyClient { | ||
iscai-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
``` | ||
|
||
### Links | ||
|
||
[client-name]: https://azure.github.io/typespec-azure/docs/libraries/typespec-client-generator-core/reference/decorators#@Azure.ClientGenerator.Core.clientName "@clientName Decorator" |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,25 +3,13 @@ import { createTypeSpecLibrary, paramMessage } from "@typespec/compiler"; | |
export const $lib = createTypeSpecLibrary({ | ||
name: "@azure-tools/typespec-client-generator-core", | ||
diagnostics: { | ||
"client-name": { | ||
severity: "warning", | ||
messages: { | ||
default: paramMessage`Client name "${"name"}" must end with Client. Use @client({name: "...Client"})`, | ||
}, | ||
}, | ||
"client-service": { | ||
severity: "warning", | ||
messages: { | ||
default: paramMessage`Client "${"name"}" is not inside a service namespace. Use @client({service: MyServiceNS})`, | ||
}, | ||
}, | ||
"unknown-client-format": { | ||
severity: "error", | ||
messages: { | ||
default: paramMessage`Client format "${"format"}" is unknown. Known values are "${"knownValues"}"`, | ||
}, | ||
}, | ||
"incorrect-client-format": { | ||
"invalid-client-format": { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this actually used? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's used when we're checking the input of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see it used in main anymore, could it have been removed |
||
severity: "error", | ||
messages: { | ||
default: paramMessage`Format "${"format"}" can only apply to "${"expectedTargetTypes"}"`, | ||
|
@@ -33,22 +21,7 @@ export const $lib = createTypeSpecLibrary({ | |
default: "Cannot have a union containing only null types.", | ||
}, | ||
}, | ||
"union-unsupported": { | ||
severity: "error", | ||
messages: { | ||
default: | ||
"Unions cannot be emitted by our language generators unless all options are literals of the same type.", | ||
null: "Unions containing multiple model types cannot be emitted unless the union is between one model type and 'null'.", | ||
}, | ||
}, | ||
"use-enum-instead": { | ||
severity: "warning", | ||
messages: { | ||
default: | ||
"Use enum instead of union of string or number literals. Falling back to the literal type.", | ||
}, | ||
}, | ||
access: { | ||
"invalid-access": { | ||
severity: "error", | ||
messages: { | ||
default: `Access decorator value must be "public" or "internal".`, | ||
|
@@ -60,13 +33,6 @@ export const $lib = createTypeSpecLibrary({ | |
default: `Usage decorator value must be 2 ("input") or 4 ("output").`, | ||
}, | ||
}, | ||
"invalid-encode": { | ||
severity: "error", | ||
messages: { | ||
default: "Invalid encoding", | ||
wrongType: paramMessage`Encoding '${"encoding"}' cannot be used on type '${"type"}'`, | ||
}, | ||
}, | ||
"conflicting-multipart-model-usage": { | ||
severity: "error", | ||
messages: { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { defineLinter } from "@typespec/compiler"; | ||
import { propertyNameConflictRule } from "./rules/property-name-conflict.rule.js"; | ||
import { requireClientSuffixRule } from "./rules/require-client-suffix.rule.js"; | ||
|
||
const rules = [requireClientSuffixRule, propertyNameConflictRule]; | ||
|
||
const csharpRules = [propertyNameConflictRule]; | ||
|
||
export const $linter = defineLinter({ | ||
rules, | ||
ruleSets: { | ||
"best-practices:csharp": { | ||
enable: { | ||
...Object.fromEntries( | ||
csharpRules.map((rule) => [ | ||
`@azure-tools/typespec-client-generator-core/${rule.name}`, | ||
true, | ||
]), | ||
), | ||
}, | ||
}, | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { ModelProperty, createRule, paramMessage } from "@typespec/compiler"; | ||
|
||
export const propertyNameConflictRule = createRule({ | ||
name: "property-name-conflict", | ||
description: "Avoid naming conflicts between a property and a model of the same name.", | ||
severity: "warning", | ||
url: "https://azure.github.io/typespec-azure/docs/libraries/typespec-client-generator-core/rules/property-name-conflict", | ||
messages: { | ||
default: paramMessage`Property '${"propertyName"}' having the same name as its enclosing model will cause problems with C# code generation. Consider renaming the property directly or using the @clientName("newName", "csharp") decorator to rename the property for C#.`, | ||
}, | ||
create(context) { | ||
return { | ||
modelProperty: (property: ModelProperty) => { | ||
const modelName = property.model?.name.toLocaleLowerCase(); | ||
const propertyName = property.name.toLocaleLowerCase(); | ||
if (propertyName === modelName) { | ||
context.reportDiagnostic({ | ||
format: { propertyName: property.name }, | ||
target: property, | ||
}); | ||
} | ||
}, | ||
}; | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { createRule, Interface, Namespace, paramMessage } from "@typespec/compiler"; | ||
import { createTCGCContext, getClient } from "../decorators.js"; | ||
|
||
export const requireClientSuffixRule = createRule({ | ||
name: "require-client-suffix", | ||
description: "Client names should end with 'Client'.", | ||
severity: "warning", | ||
url: "https://azure.github.io/typespec-azure/docs/libraries/typespec-client-generator-core/rules/require-client-suffix", | ||
messages: { | ||
default: paramMessage`Client name "${"name"}" must end with Client. Use @client({name: "...Client"}`, | ||
}, | ||
create(context) { | ||
const tcgcContext = createTCGCContext( | ||
context.program, | ||
"@azure-tools/typespec-client-generator-core", | ||
); | ||
return { | ||
namespace: (namespace: Namespace) => { | ||
const sdkClient = getClient(tcgcContext, namespace); | ||
if (sdkClient && !sdkClient.name.endsWith("Client")) { | ||
context.reportDiagnostic({ | ||
target: namespace, | ||
format: { | ||
name: sdkClient.name, | ||
}, | ||
}); | ||
} | ||
}, | ||
interface: (interfaceType: Interface) => { | ||
const sdkClient = getClient(tcgcContext, interfaceType); | ||
if (sdkClient && !sdkClient.name.endsWith("Client")) { | ||
context.reportDiagnostic({ | ||
target: interfaceType, | ||
format: { | ||
name: sdkClient.name, | ||
}, | ||
}); | ||
} | ||
}, | ||
}; | ||
}, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure
all
would ever be useful when considering non tier 1 languages as well as required rules vs best practices? Should we be a bit more granular on whatall
means?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all
isn't a specificly-defined ruleset, it's the standard name for all of the rules existing in a package