Skip to content

Commit

Permalink
No private usage linter rule (#1193)
Browse files Browse the repository at this point in the history
fix [#977](#977)
Add new linter rule to prevent using items from Private namespace from
an external library.
  • Loading branch information
timotheeguerin committed Aug 5, 2024
1 parent c0ff005 commit 1726967
Show file tree
Hide file tree
Showing 10 changed files with 221 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .chronus/changes/no-private-linter-rule-2024-6-17-15-10-51.md
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.
7 changes: 7 additions & 0 deletions .chronus/changes/no-private-linter-rule-2024-6-17-15-11-17.md
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;
}
75 changes: 75 additions & 0 deletions packages/typespec-azure-core/test/rules/no-private-usage.test.ts
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

0 comments on commit 1726967

Please sign in to comment.