277 lines
8.5 KiB
TypeScript
277 lines
8.5 KiB
TypeScript
import rule from "@effect/oxc/oxlint/rules/no-opaque-instance-fields"
|
|
import { describe, expect, it } from "vitest"
|
|
import { createTestContext, runRule } from "./utils.ts"
|
|
|
|
function runRuleWithNodes(nodes: Array<{ visitor: string; node: unknown }>) {
|
|
const { context, errors } = createTestContext()
|
|
const visitors = rule.create(context as never)
|
|
for (const { visitor, node } of nodes) {
|
|
const handler = visitors[visitor as keyof typeof visitors]
|
|
if (handler) {
|
|
;(handler as (node: unknown) => void)(node)
|
|
}
|
|
}
|
|
return errors
|
|
}
|
|
|
|
describe("no-opaque-instance-fields", () => {
|
|
const createOpaqueClass = (
|
|
callee: { type: string; name?: string; object?: unknown; property?: unknown },
|
|
members: Array<{ type: string; static: boolean }>
|
|
) => ({
|
|
type: "ClassDeclaration",
|
|
superClass: {
|
|
type: "CallExpression",
|
|
callee: {
|
|
type: "CallExpression",
|
|
callee
|
|
}
|
|
},
|
|
body: {
|
|
body: members
|
|
}
|
|
})
|
|
|
|
const createSchemaOpaqueClass = (members: Array<{ type: string; static: boolean }>) =>
|
|
createOpaqueClass({
|
|
type: "MemberExpression",
|
|
object: { type: "Identifier", name: "Schema" },
|
|
property: { type: "Identifier", name: "Opaque" }
|
|
}, members)
|
|
|
|
const createSchemaImport = () => ({
|
|
type: "ImportDeclaration",
|
|
source: { type: "Literal", value: "effect" },
|
|
specifiers: [{
|
|
type: "ImportSpecifier",
|
|
imported: { type: "Identifier", name: "Schema" },
|
|
local: { type: "Identifier", name: "Schema" },
|
|
importKind: "value"
|
|
}],
|
|
importKind: "value"
|
|
})
|
|
|
|
const createOpaqueImport = (localName = "Opaque") => ({
|
|
type: "ImportDeclaration",
|
|
source: { type: "Literal", value: "effect/Schema" },
|
|
specifiers: [{
|
|
type: "ImportSpecifier",
|
|
imported: { type: "Identifier", name: "Opaque" },
|
|
local: { type: "Identifier", name: localName },
|
|
importKind: "value"
|
|
}],
|
|
importKind: "value"
|
|
})
|
|
|
|
const createSchemaNamespaceImport = (localName = "S") => ({
|
|
type: "ImportDeclaration",
|
|
source: { type: "Literal", value: "effect/Schema" },
|
|
specifiers: [{
|
|
type: "ImportNamespaceSpecifier",
|
|
local: { type: "Identifier", name: localName }
|
|
}],
|
|
importKind: "value"
|
|
})
|
|
|
|
const createOtherOpaqueImport = (localName = "Opaque") => ({
|
|
type: "ImportDeclaration",
|
|
source: { type: "Literal", value: "other" },
|
|
specifiers: [{
|
|
type: "ImportSpecifier",
|
|
imported: { type: "Identifier", name: "Opaque" },
|
|
local: { type: "Identifier", name: localName },
|
|
importKind: "value"
|
|
}],
|
|
importKind: "value"
|
|
})
|
|
|
|
const createOtherNamespaceImport = (localName = "S") => ({
|
|
type: "ImportDeclaration",
|
|
source: { type: "Literal", value: "other" },
|
|
specifiers: [{
|
|
type: "ImportNamespaceSpecifier",
|
|
local: { type: "Identifier", name: localName }
|
|
}],
|
|
importKind: "value"
|
|
})
|
|
|
|
const createRegularClass = (members: Array<{ type: string; static: boolean }>) => ({
|
|
type: "ClassDeclaration",
|
|
superClass: {
|
|
type: "Identifier",
|
|
name: "SomeClass"
|
|
},
|
|
body: {
|
|
body: members
|
|
}
|
|
})
|
|
|
|
it("should not report for class without instance fields", () => {
|
|
const node = createSchemaOpaqueClass([])
|
|
const errors = runRuleWithNodes([
|
|
{ visitor: "ImportDeclaration", node: createSchemaImport() },
|
|
{ visitor: "ClassDeclaration", node }
|
|
])
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
|
|
it("should not report for static fields in Schema.Opaque class", () => {
|
|
const node = createSchemaOpaqueClass([
|
|
{ type: "PropertyDefinition", static: true }
|
|
])
|
|
const errors = runRuleWithNodes([
|
|
{ visitor: "ImportDeclaration", node: createSchemaImport() },
|
|
{ visitor: "ClassDeclaration", node }
|
|
])
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
|
|
it("should report for instance fields in Schema.Opaque class", () => {
|
|
const node = createSchemaOpaqueClass([
|
|
{ type: "PropertyDefinition", static: false }
|
|
])
|
|
const errors = runRuleWithNodes([
|
|
{ visitor: "ImportDeclaration", node: createSchemaImport() },
|
|
{ visitor: "ClassDeclaration", node }
|
|
])
|
|
expect(errors).toHaveLength(1)
|
|
expect(errors[0].message).toBe("Classes extending Schema.Opaque must not have instance members")
|
|
})
|
|
|
|
it("should not report when Schema.Opaque has no Schema import", () => {
|
|
const node = createSchemaOpaqueClass([
|
|
{ type: "PropertyDefinition", static: false }
|
|
])
|
|
const errors = runRule(rule, "ClassDeclaration", node)
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
|
|
it("should report for instance fields in Opaque class", () => {
|
|
const node = createOpaqueClass(
|
|
{ type: "Identifier", name: "Opaque" },
|
|
[{ type: "PropertyDefinition", static: false }]
|
|
)
|
|
const errors = runRuleWithNodes([
|
|
{ visitor: "ImportDeclaration", node: createOpaqueImport() },
|
|
{ visitor: "ClassDeclaration", node }
|
|
])
|
|
expect(errors).toHaveLength(1)
|
|
})
|
|
|
|
it("should report for instance fields in aliased Opaque class", () => {
|
|
const node = createOpaqueClass(
|
|
{
|
|
type: "MemberExpression",
|
|
object: { type: "Identifier", name: "S" },
|
|
property: { type: "Identifier", name: "Opaque" }
|
|
},
|
|
[{ type: "PropertyDefinition", static: false }]
|
|
)
|
|
const errors = runRuleWithNodes([
|
|
{ visitor: "ImportDeclaration", node: createSchemaNamespaceImport() },
|
|
{ visitor: "ClassDeclaration", node }
|
|
])
|
|
expect(errors).toHaveLength(1)
|
|
})
|
|
|
|
it("should not report for Opaque import from other module", () => {
|
|
const node = createOpaqueClass(
|
|
{ type: "Identifier", name: "Opaque" },
|
|
[{ type: "PropertyDefinition", static: false }]
|
|
)
|
|
const errors = runRuleWithNodes([
|
|
{ visitor: "ImportDeclaration", node: createOtherOpaqueImport() },
|
|
{ visitor: "ClassDeclaration", node }
|
|
])
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
|
|
it("should not report for namespace Opaque from other module", () => {
|
|
const node = createOpaqueClass(
|
|
{
|
|
type: "MemberExpression",
|
|
object: { type: "Identifier", name: "S" },
|
|
property: { type: "Identifier", name: "Opaque" }
|
|
},
|
|
[{ type: "PropertyDefinition", static: false }]
|
|
)
|
|
const errors = runRuleWithNodes([
|
|
{ visitor: "ImportDeclaration", node: createOtherNamespaceImport() },
|
|
{ visitor: "ClassDeclaration", node }
|
|
])
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
|
|
it("should report for multiple instance fields", () => {
|
|
const node = createSchemaOpaqueClass([
|
|
{ type: "PropertyDefinition", static: false },
|
|
{ type: "PropertyDefinition", static: true },
|
|
{ type: "PropertyDefinition", static: false }
|
|
])
|
|
const errors = runRuleWithNodes([
|
|
{ visitor: "ImportDeclaration", node: createSchemaImport() },
|
|
{ visitor: "ClassDeclaration", node }
|
|
])
|
|
expect(errors).toHaveLength(2)
|
|
})
|
|
|
|
it("should report for instance methods", () => {
|
|
const node = createSchemaOpaqueClass([
|
|
{ type: "MethodDefinition", static: false }
|
|
])
|
|
const errors = runRuleWithNodes([
|
|
{ visitor: "ImportDeclaration", node: createSchemaImport() },
|
|
{ visitor: "ClassDeclaration", node }
|
|
])
|
|
expect(errors).toHaveLength(1)
|
|
})
|
|
|
|
it("should not report for non-Schema.Opaque classes", () => {
|
|
const node = createRegularClass([
|
|
{ type: "PropertyDefinition", static: false }
|
|
])
|
|
const errors = runRule(rule, "ClassDeclaration", node)
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
|
|
it("should work with ClassExpression", () => {
|
|
const node = {
|
|
...createSchemaOpaqueClass([
|
|
{ type: "PropertyDefinition", static: false }
|
|
]),
|
|
type: "ClassExpression"
|
|
}
|
|
const errors = runRuleWithNodes([
|
|
{ visitor: "ImportDeclaration", node: createSchemaImport() },
|
|
{ visitor: "ClassExpression", node }
|
|
])
|
|
expect(errors).toHaveLength(1)
|
|
})
|
|
|
|
it("should not report for class without superClass", () => {
|
|
const node = {
|
|
type: "ClassDeclaration",
|
|
body: {
|
|
body: [{ type: "PropertyDefinition", static: false }]
|
|
}
|
|
}
|
|
const errors = runRule(rule, "ClassDeclaration", node)
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
|
|
it("should not report for class with non-CallExpression superClass", () => {
|
|
const node = {
|
|
type: "ClassDeclaration",
|
|
superClass: {
|
|
type: "Identifier",
|
|
name: "BaseClass"
|
|
},
|
|
body: {
|
|
body: [{ type: "PropertyDefinition", static: false }]
|
|
}
|
|
}
|
|
const errors = runRule(rule, "ClassDeclaration", node)
|
|
expect(errors).toHaveLength(0)
|
|
})
|
|
})
|