Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-azure-core"
- "@azure-tools/typespec-azure-rulesets"
---

Add the `enum-instead-of-boolean` lint rule that recommends descriptive extensible enums instead of boolean API shapes when semantic values matter.
1 change: 1 addition & 0 deletions packages/typespec-azure-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Available ruleSets:
| [`@azure-tools/typespec-azure-core/byos`](https://azure.github.io/typespec-azure/docs/libraries/azure-core/rules/byos) | Use the BYOS pattern recommended for Azure Services. |
| [`@azure-tools/typespec-azure-core/casing-style`](https://azure.github.io/typespec-azure/docs/libraries/azure-core/rules/casing-style) | Ensure proper casing style. |
| [`@azure-tools/typespec-azure-core/composition-over-inheritance`](https://azure.github.io/typespec-azure/docs/libraries/azure-core/rules/composition-over-inheritance) | Check that if a model is used in an operation and has derived models that it has a discriminator or recommend to use composition via spread or `is`. |
| [`@azure-tools/typespec-azure-core/use-enum-instead-of-boolean`](https://azure.github.io/typespec-azure/docs/libraries/azure-core/rules/use-enum-instead-of-boolean) | Boolean properties should use descriptive extensible enums when semantic values matter. |
| [`@azure-tools/typespec-azure-core/known-encoding`](https://azure.github.io/typespec-azure/docs/libraries/azure-core/rules/known-encoding) | Check for supported encodings. |
| [`@azure-tools/typespec-azure-core/long-running-polling-operation-required`](https://azure.github.io/typespec-azure/docs/libraries/azure-core/rules/long-running-polling-operation-required) | Long-running operations should have a linked polling operation. |
| [`@azure-tools/typespec-azure-core/no-case-mismatch`](https://azure.github.io/typespec-azure/docs/libraries/azure-core/rules/no-case-mismatch) | Validate that no two types have the same name with different casing. |
Expand Down
2 changes: 2 additions & 0 deletions packages/typespec-azure-core/src/linter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { requireVersionedRule } from "./rules/require-versioned.js";
import { responseSchemaMultiStatusCodeRule } from "./rules/response-schema-multi-status-code.js";
import { rpcOperationRequestBodyRule } from "./rules/rpc-operation-request-body.js";
import { spreadDiscriminatedModelRule } from "./rules/spread-discriminated-model.js";
import { useEnumInsteadOfBooleanRule } from "./rules/use-enum-instead-of-boolean.js";
import { useStandardNames } from "./rules/use-standard-names.js";
import { useStandardOperations } from "./rules/use-standard-operations.js";

Expand All @@ -49,6 +50,7 @@ const rules = [
byosRule,
casingRule,
compositionOverInheritanceRule,
useEnumInsteadOfBooleanRule,
knownEncodingRule,
longRunningOperationsRequirePollingOperation,
noCaseMismatchRule,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
title: "use-enum-instead-of-boolean"
---

```text title="Full name"
@azure-tools/typespec-azure-core/use-enum-instead-of-boolean
```

Boolean values can be hard for API users to understand when the property or payload represents a
domain state, option, or mode. Prefer a descriptive extensible enum modeled as a union so future
values can be added without a breaking change.

## Impact

- **Area:** SDK, API

Boolean shapes can make generated clients less readable and can force future breaking changes if the
API later needs more than two values.

## LintDiff Equivalent

This rule corresponds to the LintDiff rule
[EnumInsteadOfBoolean](https://github.com/Azure/azure-openapi-validator/blob/main/docs/enum-instead-of-boolean.md).

#### Incorrect

```tsp
model Widget {
enabled: boolean;
}
```

```tsp
@get
op isWidgetEnabled(): boolean;
```

#### Correct

```tsp
union WidgetState {
Enabled: "Enabled",
Disabled: "Disabled",
string,
}

model Widget {
state: WidgetState;
}
```

```tsp
@get
op getWidgetState(): WidgetState;
```

## Suppression

Suppress this rule only when the value is inherently boolean and is unlikely to grow additional
states, such as a simple yes/no capability.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { Type } from "@typespec/compiler";
import { createRule, fileRef } from "@typespec/compiler";
import { getHttpOperation } from "@typespec/http";

export const useEnumInsteadOfBooleanRule = createRule({
name: "use-enum-instead-of-boolean",
docs: fileRef.fromPackageRoot("src/rules/use-enum-instead-of-boolean.md"),
description:
"Boolean properties should use descriptive extensible enums when semantic values matter.",
severity: "warning",
url: "https://azure.github.io/typespec-azure/docs/libraries/azure-core/rules/use-enum-instead-of-boolean",
messages: {
default:
"Consider using an extensible enum instead of a boolean property so the API shape is more descriptive.",
},
create(context) {
return {
modelProperty: (property) => {
if (!isBooleanScalar(property)) {
return;
}

context.reportDiagnostic({
target: property,
});
},
operation: (operation) => {
const [httpOperation] = getHttpOperation(context.program, operation);

for (const response of httpOperation.responses) {
if (isBooleanScalar(response.type)) {
context.reportDiagnostic({
target: operation,
});
continue;
}

for (const content of response.responses) {
if (content.body === undefined || !isBooleanScalar(content.body.type)) {
continue;
}

context.reportDiagnostic({
target: content.body.property ?? operation,
});
}
}
},
};
},
});

function isBooleanScalar(type: Type): boolean {
return type.kind === "ModelProperty"
? isBooleanScalar(type.type)
: type.kind === "Scalar" && type.name === "boolean";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Tester } from "#test/test-host.js";
import { type LinterRuleTester, createLinterRuleTester } from "@typespec/compiler/testing";
import { beforeEach, describe, it } from "vitest";
import { useEnumInsteadOfBooleanRule } from "../../src/rules/use-enum-instead-of-boolean.js";

let tester: LinterRuleTester;

beforeEach(async () => {
const runner = await Tester.createInstance();
tester = createLinterRuleTester(
runner,
useEnumInsteadOfBooleanRule,
"@azure-tools/typespec-azure-core",
);
});

describe("boolean shapes should use descriptive extensible enums", () => {
it("emits warning for boolean model properties", async () => {
await tester
.expect(
`
model Widget {
enabled: boolean;
}
`,
)
.toEmitDiagnostics({
code: "@azure-tools/typespec-azure-core/use-enum-instead-of-boolean",
message:
"Consider using an extensible enum instead of a boolean property so the API shape is more descriptive.",
});
});

it("emits warning for boolean path parameters", async () => {
await tester
.expect(
`
@route("/widgets/{enabled}")
@get
op getWidget(@path enabled: boolean): string;
`,
)
.toEmitDiagnostics({
code: "@azure-tools/typespec-azure-core/use-enum-instead-of-boolean",
});
});

it("emits warning for boolean request bodies", async () => {
await tester
.expect(
`
@post
op checkWidget(@body body: boolean): string;
`,
)
.toEmitDiagnostics({
code: "@azure-tools/typespec-azure-core/use-enum-instead-of-boolean",
});
});

it("emits warning for boolean response bodies", async () => {
await tester
.expect(
`
@get
op isWidgetEnabled(): boolean;
`,
)
.toEmitDiagnostics({
code: "@azure-tools/typespec-azure-core/use-enum-instead-of-boolean",
});
});

it("allows comparable non-boolean shapes", async () => {
await tester
.expect(
`
union WidgetState {
Enabled: "Enabled",
Disabled: "Disabled",
string,
}

model Widget {
state: WidgetState;
}

@post
op checkWidget(@body body: WidgetState): WidgetState;
`,
)
.toBeValid();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default {
"@azure-tools/typespec-azure-core/byos": true,
"@azure-tools/typespec-azure-core/casing-style": true,
"@azure-tools/typespec-azure-core/composition-over-inheritance": true,
"@azure-tools/typespec-azure-core/use-enum-instead-of-boolean": true,
"@azure-tools/typespec-azure-core/use-extensible-enum": true,
"@azure-tools/typespec-azure-core/known-encoding": true,
"@azure-tools/typespec-azure-core/long-running-polling-operation-required": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default {
"@azure-tools/typespec-azure-core/byos": true,
"@azure-tools/typespec-azure-core/casing-style": true,
"@azure-tools/typespec-azure-core/composition-over-inheritance": true,
"@azure-tools/typespec-azure-core/use-enum-instead-of-boolean": true,
"@azure-tools/typespec-azure-core/use-extensible-enum": true,
"@azure-tools/typespec-azure-core/known-encoding": true,
"@azure-tools/typespec-azure-core/long-running-polling-operation-required": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Available ruleSets:
| [`@azure-tools/typespec-azure-core/byos`](../rules/byos.md) | Use the BYOS pattern recommended for Azure Services. |
| [`@azure-tools/typespec-azure-core/casing-style`](../rules/casing-style.md) | Ensure proper casing style. |
| [`@azure-tools/typespec-azure-core/composition-over-inheritance`](../rules/composition-over-inheritance.md) | Check that if a model is used in an operation and has derived models that it has a discriminator or recommend to use composition via spread or `is`. |
| [`@azure-tools/typespec-azure-core/use-enum-instead-of-boolean`](../rules/use-enum-instead-of-boolean.md) | Boolean properties should use descriptive extensible enums when semantic values matter. |
| [`@azure-tools/typespec-azure-core/known-encoding`](../rules/known-encoding.md) | Check for supported encodings. |
| [`@azure-tools/typespec-azure-core/long-running-polling-operation-required`](../rules/long-running-polling-operation-required.md) | Long-running operations should have a linked polling operation. |
| [`@azure-tools/typespec-azure-core/no-case-mismatch`](../rules/no-case-mismatch.md) | Validate that no two types have the same name with different casing. |
Expand Down
Loading