Skip to content
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

No private usage linter rule #1193

Merged
merged 4 commits into from
Aug 5, 2024
Merged
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,7 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-azure-core"
---

Add new linter rule to prevent using items from Private namespace from an external library.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-azure-rulesets"
---

Add new `no-private-usage` linter rule to `data-plane` and `resource-manager` rulesets
1 change: 1 addition & 0 deletions docs/libraries/azure-core/reference/linter.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,4 @@ Available ruleSets:
| `@azure-tools/typespec-azure-core/use-standard-operations` | Operations should be defined using a signature from the Azure.Core namespace. |
| [`@azure-tools/typespec-azure-core/no-string-discriminator`](/libraries/azure-core/rules/no-string-discriminator.md) | Azure services discriminated models should define the discriminated property as an extensible union. |
| `@azure-tools/typespec-azure-core/friendly-name` | Ensures that @friendlyName is used as intended. |
| [`@azure-tools/typespec-azure-core/no-private-usage`](/libraries/azure-core/rules/no-private-usage.md) | Verify that elements inside Private namespace are not referenced. |
32 changes: 32 additions & 0 deletions docs/libraries/azure-core/rules/no-private-usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
title: "no-private-usage"
---

```text title="Full name"
@azure-tools/typespec-azure-core/no-private-usage
```

Verify that a spec is not referencing items from another library using a private namespace.

#### ❌ Incorrect

```ts
@Azure.Core.Foundations.Private.embeddingVector(string)
model Foo {}
```

#### ✅ Ok

Using items from a private namespace within the same library is allowed.

```ts
namespace MyService;

@MyService.Private.myPrivateDecorator
model Foo {}


namespace Private {
extern dec myPrivateDecorator(target);
}
```
1 change: 1 addition & 0 deletions packages/typespec-azure-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Available ruleSets:
| `@azure-tools/typespec-azure-core/use-standard-operations` | Operations should be defined using a signature from the Azure.Core namespace. |
| [`@azure-tools/typespec-azure-core/no-string-discriminator`](https://azure.github.io/typespec-azure/docs/libraries/azure-core/rules/no-string-discriminator) | Azure services discriminated models should define the discriminated property as an extensible union. |
| `@azure-tools/typespec-azure-core/friendly-name` | Ensures that @friendlyName is used as intended. |
| [`@azure-tools/typespec-azure-core/no-private-usage`](https://azure.github.io/typespec-azure/docs/libraries/azure-core/rules/no-private-usage) | Verify that elements inside Private namespace are not referenced. |

## Decorators

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 @@ -15,6 +15,7 @@ import { noGenericNumericRule } from "./rules/no-generic-numeric.js";
import { noNullableRule } from "./rules/no-nullable.js";
import { noOffsetDateTimeRule } from "./rules/no-offsetdatetime.js";
import { operationIdRule } from "./rules/no-operation-id.js";
import { noPrivateUsage } from "./rules/no-private-usage.js";
import { noResponseBodyRule } from "./rules/no-response-body.js";
import { noRpcPathParamsRule } from "./rules/no-rpc-path-params.js";
import { noStringDiscriminatorRule } from "./rules/no-string-discriminator.js";
Expand Down Expand Up @@ -71,6 +72,7 @@ const rules = [
useStandardOperations,
noStringDiscriminatorRule,
friendlyNameRule,
noPrivateUsage,
];

export const $linter = defineLinter({
Expand Down
94 changes: 94 additions & 0 deletions packages/typespec-azure-core/src/rules/no-private-usage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {
createRule,
DecoratedType,
DiagnosticTarget,
getLocationContext,
getTypeName,
Namespace,
paramMessage,
Type,
} from "@typespec/compiler";

export const noPrivateUsage = createRule({
name: "no-private-usage",
description: "Verify that elements inside Private namespace are not referenced.",
severity: "warning",
url: "https://azure.github.io/typespec-azure/docs/libraries/azure-core/rules/no-private-usage",
messages: {
default: paramMessage`Referencing elements inside Private namespace "${"ns"}" is not allowed.`,
},
create(context) {
function checkReference(origin: Type, type: Type, target: DiagnosticTarget) {
if (getLocationContext(context.program, origin).type !== "project") {
return;
}
if (getLocationContext(context.program, type).type === "project") {
return;
}
if (isInPrivateNamespace(type)) {
context.reportDiagnostic({
target,
format: { ns: getTypeName(type.namespace) },
});
}
}

function checkDecorators(type: Type & DecoratedType) {
if (getLocationContext(context.program, type).type !== "project") {
return;
}
for (const decorator of type.decorators) {
if (
decorator.definition &&
isInPrivateNamespace(decorator.definition) &&
getLocationContext(context.program, decorator.definition).type !== "project"
) {
context.reportDiagnostic({
target: decorator.node ?? type,
format: { ns: getTypeName(decorator.definition.namespace) },
});
}
}
}
return {
model: (model) => {
checkDecorators(model);
model.baseModel && checkReference(model, model.baseModel, model);
},
modelProperty: (prop) => {
checkDecorators(prop);
checkReference(prop, prop.type, prop);
},
unionVariant: (variant) => {
checkDecorators(variant);
checkReference(variant, variant.type, variant);
},
operation: (type) => {
checkDecorators(type);
},
interface: (type) => {
checkDecorators(type);
},
enum: (type) => {
checkDecorators(type);
},
union: (type) => {
checkDecorators(type);
},
};
},
});

function isInPrivateNamespace(type: Type): type is Type & { namespace: Namespace } {
if (!("namespace" in type)) {
return false;
}
let current = type;
while (current.namespace) {
if (current.namespace?.name === "Private") {
return true;
}
current = current.namespace;
}
return false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {
BasicTestRunner,
LinterRuleTester,
createLinterRuleTester,
} from "@typespec/compiler/testing";
import { beforeEach, it } from "vitest";
import { noPrivateUsage } from "../../src/rules/no-private-usage.js";
import { createAzureCoreTestRunner } from "../test-host.js";

let runner: BasicTestRunner;
let tester: LinterRuleTester;

beforeEach(async () => {
runner = await createAzureCoreTestRunner({ omitServiceNamespace: true });
tester = createLinterRuleTester(runner, noPrivateUsage, "@azure-tools/typespec-azure-core");
});

it("emits a warning diagnostic if using type from Azure.Core.Foundations.Private", async () => {
await tester
.expect(
`
@useDependency(Azure.Core.Versions.v1_0_Preview_2)
namespace MyService {
model Foo {
bar: Azure.Core.Foundations.Private.ArmResourceIdentifierConfigOptions
}
}
`
)
.toEmitDiagnostics([
{
code: "@azure-tools/typespec-azure-core/no-private-usage",
message:
'Referencing elements inside Private namespace "Azure.Core.Foundations.Private" is not allowed.',
},
]);
});

it("emits a warning diagnostic if using decorators from Azure.Core.Foundations.Private", async () => {
await tester
.expect(
`
@useDependency(Azure.Core.Versions.v1_0_Preview_2)
namespace MyService {
@Azure.Core.Foundations.Private.embeddingVector(string)
model Foo {}
}
`
)
.toEmitDiagnostics([
{
code: "@azure-tools/typespec-azure-core/no-private-usage",
message:
'Referencing elements inside Private namespace "Azure.Core.Foundations.Private" is not allowed.',
},
]);
});

it("ok using item from Private namespace in the project", async () => {
await tester
.expect(
`
namespace MyService {
model Foo {
bar: MyLib.Private.Bar;
}
}

namespace MyLib.Private {
model Bar {}
}
`
)
.toBeValid();
});
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default {
"@azure-tools/typespec-azure-core/use-standard-names": true,
"@azure-tools/typespec-azure-core/use-standard-operations": true,
"@azure-tools/typespec-azure-core/no-string-discriminator": true,
"@azure-tools/typespec-azure-core/no-private-usage": true,
"@azure-tools/typespec-azure-core/friendly-name": true,

// Azure core rules enabled via an optional rulesets
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default {
"@azure-tools/typespec-azure-core/use-standard-names": true,
"@azure-tools/typespec-azure-core/use-standard-operations": true,
"@azure-tools/typespec-azure-core/no-string-discriminator": true,
"@azure-tools/typespec-azure-core/no-private-usage": true,
"@azure-tools/typespec-azure-core/friendly-name": true,

// Azure core not enabled - Arm has its own conflicting rule
Expand Down
Loading