Merge commit '36c66dd290d3ce6eb1ccd310d0c658d4a32bb8eb' as 'repos/effect'
This commit is contained in:
75
repos/effect/packages/tools/oxc/test/jsdocs.test.ts
Normal file
75
repos/effect/packages/tools/oxc/test/jsdocs.test.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import rule from "@effect/oxc/oxlint/rules/jsdocs"
|
||||
import * as crypto from "node:crypto"
|
||||
import * as fs from "node:fs"
|
||||
import * as os from "node:os"
|
||||
import * as path from "node:path"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { createTestContext } from "./utils.ts"
|
||||
|
||||
function hash(source: string): string {
|
||||
return crypto.createHash("sha256").update(source).digest("hex")
|
||||
}
|
||||
|
||||
function run(source: string, cwd: string, filename: string, model = ".data/jsdocs.json") {
|
||||
const { context, errors } = createTestContext({ sourceCode: source, cwd, filename, ruleOptions: [{ model }] })
|
||||
const visitors = rule.create(context as never)
|
||||
visitors.Program?.({ type: "Program", range: [0, source.length] } as never)
|
||||
return errors
|
||||
}
|
||||
|
||||
describe("jsdocs", () => {
|
||||
it("skips when the model is missing", () => {
|
||||
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-rule-"))
|
||||
const errors = run("export const a = 1\n", cwd, path.join(cwd, "src/Foo.ts"))
|
||||
expect(errors).toEqual([])
|
||||
})
|
||||
|
||||
it("reports invalid model files", () => {
|
||||
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-rule-"))
|
||||
fs.mkdirSync(path.join(cwd, ".data"), { recursive: true })
|
||||
fs.writeFileSync(path.join(cwd, ".data/jsdocs.json"), "{")
|
||||
const errors = run("export const a = 1\n", cwd, path.join(cwd, "src/Foo.ts"))
|
||||
expect(errors.map((error) => error.message)).toEqual([expect.stringContaining("Invalid jsdocs model")])
|
||||
})
|
||||
|
||||
it("reports stale model files", () => {
|
||||
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-rule-"))
|
||||
fs.mkdirSync(path.join(cwd, ".data"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(cwd, ".data/jsdocs.json"),
|
||||
JSON.stringify({
|
||||
version: 2,
|
||||
generatedBy: "@effect/jsdocs",
|
||||
generatedAt: "now",
|
||||
files: [{ file: "src/Foo.ts", hash: "old", diagnostics: [], declarations: [], namespaces: [] }],
|
||||
apis: []
|
||||
})
|
||||
)
|
||||
const errors = run("export const a = 1\n", cwd, path.join(cwd, "src/Foo.ts"))
|
||||
expect(errors.map((error) => error.message)).toEqual(["JSDoc model is stale for this file; run `pnpm jsdocs`"])
|
||||
})
|
||||
|
||||
it("reports model diagnostics", () => {
|
||||
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-rule-"))
|
||||
const source = "export const a = 1\n"
|
||||
fs.mkdirSync(path.join(cwd, ".data"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(cwd, ".data/jsdocs.json"),
|
||||
JSON.stringify({
|
||||
version: 2,
|
||||
generatedBy: "@effect/jsdocs",
|
||||
generatedAt: "now",
|
||||
apis: [],
|
||||
files: [{
|
||||
file: "src/Foo.ts",
|
||||
hash: hash(source),
|
||||
diagnostics: [{ code: "missing-jsdoc", message: "Public JSDoc is required", range: [0, 6] }],
|
||||
declarations: [],
|
||||
namespaces: []
|
||||
}]
|
||||
})
|
||||
)
|
||||
const errors = run(source, cwd, path.join(cwd, "src/Foo.ts"))
|
||||
expect(errors.map((error) => error.message)).toEqual(["Public JSDoc is required"])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,182 @@
|
||||
import rule from "@effect/oxc/oxlint/rules/no-import-from-barrel-package"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { runRule } from "./utils.ts"
|
||||
|
||||
describe("no-import-from-barrel-package", () => {
|
||||
const testOptions = {
|
||||
filename: "/test/file.ts",
|
||||
cwd: "/test",
|
||||
ruleOptions: [{ checkPatterns: ["^effect$"] }]
|
||||
}
|
||||
|
||||
const createImportDeclaration = (
|
||||
source: string,
|
||||
specifiers: Array<unknown>,
|
||||
importKind?: "type" | "value"
|
||||
) => {
|
||||
const base = {
|
||||
type: "ImportDeclaration" as const,
|
||||
source: { value: source },
|
||||
specifiers,
|
||||
range: [0, 50] as [number, number]
|
||||
}
|
||||
return importKind !== undefined ? { ...base, importKind } : base
|
||||
}
|
||||
|
||||
const createNamedSpecifier = (
|
||||
name: string,
|
||||
local?: string,
|
||||
importKind?: "type" | "value"
|
||||
) => {
|
||||
const base = {
|
||||
type: "ImportSpecifier" as const,
|
||||
imported: { type: "Identifier" as const, name },
|
||||
local: { name: local ?? name }
|
||||
}
|
||||
return importKind !== undefined ? { ...base, importKind } : base
|
||||
}
|
||||
|
||||
it("should not report for imports from non-barrel packages", () => {
|
||||
const node = createImportDeclaration("effect/Effect", [
|
||||
createNamedSpecifier("Effect")
|
||||
])
|
||||
const errors = runRule(rule, "ImportDeclaration", node, testOptions)
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should report for named imports from effect barrel", () => {
|
||||
const node = createImportDeclaration("effect", [
|
||||
createNamedSpecifier("Effect")
|
||||
])
|
||||
const errors = runRule(rule, "ImportDeclaration", node, testOptions)
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0].message).toBe(
|
||||
`Use import * as Effect from "effect/Effect" instead`
|
||||
)
|
||||
})
|
||||
|
||||
it("should report for multiple named imports from effect barrel", () => {
|
||||
const node = createImportDeclaration("effect", [
|
||||
createNamedSpecifier("Effect"),
|
||||
createNamedSpecifier("Option"),
|
||||
createNamedSpecifier("Either")
|
||||
])
|
||||
const errors = runRule(rule, "ImportDeclaration", node, testOptions)
|
||||
expect(errors).toHaveLength(3)
|
||||
})
|
||||
|
||||
it("should not report for type-only import declarations", () => {
|
||||
const node = createImportDeclaration(
|
||||
"effect",
|
||||
[createNamedSpecifier("Effect")],
|
||||
"type"
|
||||
)
|
||||
const errors = runRule(rule, "ImportDeclaration", node, testOptions)
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should not report for type imports within named specifiers", () => {
|
||||
const node = createImportDeclaration("effect", [
|
||||
createNamedSpecifier("Effect", "Effect", "type")
|
||||
])
|
||||
const errors = runRule(rule, "ImportDeclaration", node, testOptions)
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle aliased imports", () => {
|
||||
const node = createImportDeclaration("effect", [
|
||||
createNamedSpecifier("Effect", "Eff")
|
||||
])
|
||||
const errors = runRule(rule, "ImportDeclaration", node, testOptions)
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0].message).toBe(
|
||||
`Use import * as Eff from "effect/Effect" instead`
|
||||
)
|
||||
})
|
||||
|
||||
it("should report for namespace imports from barrel", () => {
|
||||
const node = createImportDeclaration("effect", [
|
||||
{
|
||||
type: "ImportNamespaceSpecifier",
|
||||
local: { name: "Effect" }
|
||||
}
|
||||
])
|
||||
const errors = runRule(rule, "ImportDeclaration", node, testOptions)
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0].message).toBe(
|
||||
`Do not use namespace import from barrel file "effect", import from specific modules instead`
|
||||
)
|
||||
})
|
||||
|
||||
it("should not report for namespace imports from module paths", () => {
|
||||
const node = createImportDeclaration("effect/Effect", [
|
||||
{
|
||||
type: "ImportNamespaceSpecifier",
|
||||
local: { name: "Effect" }
|
||||
}
|
||||
])
|
||||
const errors = runRule(rule, "ImportDeclaration", node, testOptions)
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should not report for default imports", () => {
|
||||
const node = createImportDeclaration("effect", [
|
||||
{
|
||||
type: "ImportDefaultSpecifier",
|
||||
local: { name: "Effect" }
|
||||
}
|
||||
])
|
||||
const errors = runRule(rule, "ImportDeclaration", node, testOptions)
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
describe("configuration", () => {
|
||||
it("should match regex patterns", () => {
|
||||
const node = createImportDeclaration("@myorg/utils", [
|
||||
createNamedSpecifier("helper")
|
||||
])
|
||||
// No patterns configured - doesn't match
|
||||
const defaultErrors = runRule(rule, "ImportDeclaration", node, testOptions)
|
||||
expect(defaultErrors).toHaveLength(0)
|
||||
|
||||
// Pattern matches @myorg/*
|
||||
const customErrors = runRule(rule, "ImportDeclaration", node, {
|
||||
...testOptions,
|
||||
ruleOptions: [{ checkPatterns: ["^@myorg/"] }]
|
||||
})
|
||||
expect(customErrors).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("should match exact package with pattern", () => {
|
||||
const node = createImportDeclaration("lodash", [
|
||||
createNamedSpecifier("map")
|
||||
])
|
||||
// No patterns - doesn't match
|
||||
const defaultErrors = runRule(rule, "ImportDeclaration", node, testOptions)
|
||||
expect(defaultErrors).toHaveLength(0)
|
||||
|
||||
// Exact match pattern
|
||||
const customErrors = runRule(rule, "ImportDeclaration", node, {
|
||||
...testOptions,
|
||||
ruleOptions: [{ checkPatterns: ["^lodash$"] }]
|
||||
})
|
||||
expect(customErrors).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("should disable relative index checking when configured", () => {
|
||||
const node = createImportDeclaration("./index.ts", [
|
||||
createNamedSpecifier("foo")
|
||||
])
|
||||
// Default checks relative imports
|
||||
const defaultErrors = runRule(rule, "ImportDeclaration", node, testOptions)
|
||||
expect(defaultErrors).toHaveLength(1)
|
||||
|
||||
// Disabled relative checking
|
||||
const customErrors = runRule(rule, "ImportDeclaration", node, {
|
||||
...testOptions,
|
||||
ruleOptions: [{ checkRelativeIndexImports: false }]
|
||||
})
|
||||
expect(customErrors).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,120 @@
|
||||
import rule from "@effect/oxc/oxlint/rules/no-js-extension-imports"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { runRule } from "./utils.ts"
|
||||
|
||||
describe("no-js-extension-imports", () => {
|
||||
const createImportDeclaration = (source: string) => ({
|
||||
type: "ImportDeclaration",
|
||||
source: { value: source, range: [8, 8 + source.length + 2] as [number, number] },
|
||||
range: [0, 50] as [number, number]
|
||||
})
|
||||
|
||||
const createExportAllDeclaration = (source: string) => ({
|
||||
type: "ExportAllDeclaration",
|
||||
source: { value: source, range: [14, 14 + source.length + 2] as [number, number] },
|
||||
range: [0, 50] as [number, number]
|
||||
})
|
||||
|
||||
const createExportNamedDeclaration = (source: string) => ({
|
||||
type: "ExportNamedDeclaration",
|
||||
source: { value: source, range: [9, 9 + source.length + 2] as [number, number] },
|
||||
range: [0, 50] as [number, number]
|
||||
})
|
||||
|
||||
describe("ImportDeclaration", () => {
|
||||
it("should report .js extension in relative imports", () => {
|
||||
const node = createImportDeclaration("./foo.js")
|
||||
const errors = runRule(rule, "ImportDeclaration", node)
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0].message).toBe(`Use ".ts" extension instead of ".js" for relative imports`)
|
||||
})
|
||||
|
||||
it("should report .jsx extension in relative imports", () => {
|
||||
const node = createImportDeclaration("./component.jsx")
|
||||
const errors = runRule(rule, "ImportDeclaration", node)
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0].message).toBe(`Use ".tsx" extension instead of ".jsx" for relative imports`)
|
||||
})
|
||||
|
||||
it("should report .mjs extension in relative imports", () => {
|
||||
const node = createImportDeclaration("../utils.mjs")
|
||||
const errors = runRule(rule, "ImportDeclaration", node)
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0].message).toBe(`Use ".mts" extension instead of ".mjs" for relative imports`)
|
||||
})
|
||||
|
||||
it("should report .cjs extension in relative imports", () => {
|
||||
const node = createImportDeclaration("./config.cjs")
|
||||
const errors = runRule(rule, "ImportDeclaration", node)
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0].message).toBe(`Use ".cts" extension instead of ".cjs" for relative imports`)
|
||||
})
|
||||
|
||||
it("should not report .ts extension in relative imports", () => {
|
||||
const node = createImportDeclaration("./foo.ts")
|
||||
const errors = runRule(rule, "ImportDeclaration", node)
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should not report .tsx extension in relative imports", () => {
|
||||
const node = createImportDeclaration("./component.tsx")
|
||||
const errors = runRule(rule, "ImportDeclaration", node)
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should not report for package imports with .js extension", () => {
|
||||
const node = createImportDeclaration("some-package/utils.js")
|
||||
const errors = runRule(rule, "ImportDeclaration", node)
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should not report for bare package imports", () => {
|
||||
const node = createImportDeclaration("effect")
|
||||
const errors = runRule(rule, "ImportDeclaration", node)
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle deeply nested relative imports", () => {
|
||||
const node = createImportDeclaration("../../lib/utils.js")
|
||||
const errors = runRule(rule, "ImportDeclaration", node)
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0].message).toBe(`Use ".ts" extension instead of ".js" for relative imports`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("ExportAllDeclaration", () => {
|
||||
it("should report .js extension in relative re-exports", () => {
|
||||
const node = createExportAllDeclaration("./foo.js")
|
||||
const errors = runRule(rule, "ExportAllDeclaration", node)
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0].message).toBe(`Use ".ts" extension instead of ".js" for relative imports`)
|
||||
})
|
||||
|
||||
it("should not report .ts extension in relative re-exports", () => {
|
||||
const node = createExportAllDeclaration("./foo.ts")
|
||||
const errors = runRule(rule, "ExportAllDeclaration", node)
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("ExportNamedDeclaration", () => {
|
||||
it("should report .js extension in relative named exports", () => {
|
||||
const node = createExportNamedDeclaration("./foo.js")
|
||||
const errors = runRule(rule, "ExportNamedDeclaration", node)
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0].message).toBe(`Use ".ts" extension instead of ".js" for relative imports`)
|
||||
})
|
||||
|
||||
it("should not report .ts extension in relative named exports", () => {
|
||||
const node = createExportNamedDeclaration("./foo.ts")
|
||||
const errors = runRule(rule, "ExportNamedDeclaration", node)
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle export without source", () => {
|
||||
const node = { type: "ExportNamedDeclaration", source: null }
|
||||
const errors = runRule(rule, "ExportNamedDeclaration", node)
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,276 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
129
repos/effect/packages/tools/oxc/test/no-unused-internal.test.ts
Normal file
129
repos/effect/packages/tools/oxc/test/no-unused-internal.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import rule from "@effect/oxc/oxlint/rules/no-unused-internal"
|
||||
import * as fs from "node:fs"
|
||||
import * as os from "node:os"
|
||||
import * as path from "node:path"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { createTestContext } from "./utils.ts"
|
||||
|
||||
function writeFixture(files: Record<string, string>): string {
|
||||
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "no-unused-internal-rule-"))
|
||||
for (const [file, source] of Object.entries(files)) {
|
||||
const fileName = path.join(cwd, file)
|
||||
fs.mkdirSync(path.dirname(fileName), { recursive: true })
|
||||
fs.writeFileSync(fileName, source)
|
||||
}
|
||||
return cwd
|
||||
}
|
||||
|
||||
function run(cwd: string, file: string) {
|
||||
const filename = path.join(cwd, file)
|
||||
const source = fs.readFileSync(filename, "utf8")
|
||||
const { context, errors } = createTestContext({ sourceCode: source, cwd, filename })
|
||||
const visitors = rule.create(context as never)
|
||||
visitors.Program?.({ type: "Program", range: [0, source.length] } as never)
|
||||
return errors.map((error) => error.message)
|
||||
}
|
||||
|
||||
describe("no-unused-internal", () => {
|
||||
it("reports unused top-level @internal exports", () => {
|
||||
const cwd = writeFixture({
|
||||
"packages/foo/src/Internal.ts": `/** @internal */
|
||||
export const unused = 1
|
||||
`
|
||||
})
|
||||
|
||||
expect(run(cwd, "packages/foo/src/Internal.ts")).toEqual([
|
||||
`@internal export "unused" is not used by production sources`
|
||||
])
|
||||
})
|
||||
|
||||
it("counts same-file references as usage", () => {
|
||||
const cwd = writeFixture({
|
||||
"packages/foo/src/Internal.ts": `/** @internal */
|
||||
export const used = 1
|
||||
|
||||
export const value = used
|
||||
`
|
||||
})
|
||||
|
||||
expect(run(cwd, "packages/foo/src/Internal.ts")).toEqual([])
|
||||
})
|
||||
|
||||
it("counts type-only references as usage", () => {
|
||||
const cwd = writeFixture({
|
||||
"packages/foo/src/Internal.ts": `/** @internal */
|
||||
export interface Internal {
|
||||
readonly value: string
|
||||
}
|
||||
`,
|
||||
"packages/foo/src/Consumer.ts": `import type { Internal } from "./Internal.ts"
|
||||
|
||||
type Local = Internal
|
||||
`
|
||||
})
|
||||
|
||||
expect(run(cwd, "packages/foo/src/Internal.ts")).toEqual([])
|
||||
})
|
||||
|
||||
it("reports @internal exports used in public exported type signatures", () => {
|
||||
const cwd = writeFixture({
|
||||
"packages/foo/src/Internal.ts": `/** @internal */
|
||||
export interface Internal {
|
||||
readonly value: string
|
||||
}
|
||||
`,
|
||||
"packages/foo/src/Public.ts": `import type { Internal } from "./Internal.ts"
|
||||
|
||||
export interface Public {
|
||||
readonly value: Internal
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
expect(run(cwd, "packages/foo/src/Public.ts")).toEqual([
|
||||
`Do not reference @internal export "Internal" in a public exported type signature`
|
||||
])
|
||||
expect(run(cwd, "packages/foo/src/Internal.ts")).toEqual([])
|
||||
})
|
||||
|
||||
it("ignores @internal class member type signatures", () => {
|
||||
const cwd = writeFixture({
|
||||
"packages/foo/src/Internal.ts": `/** @internal */
|
||||
export interface Internal {
|
||||
readonly value: string
|
||||
}
|
||||
`,
|
||||
"packages/foo/src/Public.ts": `import type { Internal } from "./Internal.ts"
|
||||
|
||||
export class Public {
|
||||
/** @internal */
|
||||
getInternal(): Internal {
|
||||
throw new Error("not implemented")
|
||||
}
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
expect(run(cwd, "packages/foo/src/Public.ts")).toEqual([])
|
||||
expect(run(cwd, "packages/foo/src/Internal.ts")).toEqual([])
|
||||
})
|
||||
|
||||
it("ignores public type signatures in internal directories", () => {
|
||||
const cwd = writeFixture({
|
||||
"packages/foo/src/Internal.ts": `/** @internal */
|
||||
export interface Internal {
|
||||
readonly value: string
|
||||
}
|
||||
`,
|
||||
"packages/foo/src/internal/Consumer.ts": `import type { Internal } from "../Internal.ts"
|
||||
|
||||
export interface PublicInsideInternalDirectory {
|
||||
readonly value: Internal
|
||||
}
|
||||
`
|
||||
})
|
||||
|
||||
expect(run(cwd, "packages/foo/src/internal/Consumer.ts")).toEqual([])
|
||||
expect(run(cwd, "packages/foo/src/Internal.ts")).toEqual([])
|
||||
})
|
||||
})
|
||||
61
repos/effect/packages/tools/oxc/test/utils.ts
Normal file
61
repos/effect/packages/tools/oxc/test/utils.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { CreateRule, Visitor } from "oxlint"
|
||||
|
||||
export interface ReportedError {
|
||||
node: unknown
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface TestContextOptions {
|
||||
sourceCode?: string
|
||||
filename?: string
|
||||
cwd?: string
|
||||
ruleOptions?: Array<unknown>
|
||||
}
|
||||
|
||||
export const createTestContext = (options: TestContextOptions = {}) => {
|
||||
const {
|
||||
sourceCode = "",
|
||||
filename = "/test/file.ts",
|
||||
cwd = "/test",
|
||||
ruleOptions = []
|
||||
} = options
|
||||
|
||||
const errors: Array<ReportedError> = []
|
||||
const context = {
|
||||
id: "test/rule",
|
||||
filename,
|
||||
physicalFilename: filename,
|
||||
cwd,
|
||||
options: ruleOptions,
|
||||
getFilename: () => filename,
|
||||
getCwd: () => cwd,
|
||||
report(reportOptions: ReportedError) {
|
||||
errors.push(reportOptions)
|
||||
},
|
||||
sourceCode: {
|
||||
text: sourceCode,
|
||||
getText(node?: { range?: [number, number] } | null) {
|
||||
if (node?.range) {
|
||||
return sourceCode.slice(node.range[0], node.range[1])
|
||||
}
|
||||
return sourceCode
|
||||
}
|
||||
}
|
||||
}
|
||||
return { errors, context }
|
||||
}
|
||||
|
||||
export const runRule = (
|
||||
rule: CreateRule,
|
||||
visitor: keyof Visitor,
|
||||
node: unknown,
|
||||
options: TestContextOptions = {}
|
||||
): Array<ReportedError> => {
|
||||
const { context, errors } = createTestContext(options)
|
||||
const visitors = rule.create(context as never)
|
||||
const handler = visitors[visitor]
|
||||
if (handler) {
|
||||
;(handler as (node: unknown) => void)(node)
|
||||
}
|
||||
return errors
|
||||
}
|
||||
Reference in New Issue
Block a user