oxlint-plugin-arch 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +101 -0
- package/package.json +64 -0
- package/src/index.ts +57 -0
- package/src/rules/export-file-prefix.ts +70 -0
- package/src/rules/export-name-pattern.ts +71 -0
- package/src/rules/filename-export-name.ts +86 -0
- package/src/rules/filename-match.ts +47 -0
- package/src/rules/folder-prefix.ts +73 -0
- package/src/rules/no-extra-exports.ts +60 -0
- package/src/rules/no-extra-factory-keys.ts +58 -0
- package/src/rules/no-file-level-helpers.ts +58 -0
- package/src/rules/no-imported-type-alias.ts +83 -0
- package/src/rules/no-inline-schema-elements.ts +70 -0
- package/src/rules/no-local-schema-construction.ts +162 -0
- package/src/rules/no-rederive-schema.ts +66 -0
- package/src/rules/no-restricted-files.ts +40 -0
- package/src/rules/no-restricted-token.ts +71 -0
- package/src/rules/no-runtime-in-types.ts +72 -0
- package/src/rules/no-single-use-scalar-schema.ts +164 -0
- package/src/rules/no-top-level-functions.ts +77 -0
- package/src/rules/no-trivial-functions.ts +56 -0
- package/src/rules/no-unescaped-like.ts +67 -0
- package/src/rules/only-export-components.ts +78 -0
- package/src/rules/require-file-factory.ts +67 -0
- package/src/rules/require-object-params.ts +32 -0
- package/src/rules/require-orpc-output.ts +64 -0
- package/src/rules/require-paired-call.ts +71 -0
- package/src/rules/route-surface.ts +62 -0
- package/src/utils/helpers/ast.ts +101 -0
- package/src/utils/helpers/declarations.ts +203 -0
- package/src/utils/helpers/exports.ts +158 -0
- package/src/utils/helpers/factories.ts +67 -0
- package/src/utils/helpers/index.ts +11 -0
- package/src/utils/helpers/naming.ts +89 -0
- package/src/utils/helpers/options.ts +5 -0
- package/src/utils/helpers/orpc.ts +31 -0
- package/src/utils/helpers/react-components.ts +146 -0
- package/src/utils/helpers/routes.ts +31 -0
- package/src/utils/helpers/sanitizers.ts +33 -0
- package/src/utils/helpers/schemas.ts +82 -0
- package/src/utils/index.ts +1 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { defineRule } from '@oxlint/plugins'
|
|
2
|
+
import {
|
|
3
|
+
factoriesCollectTopLevel,
|
|
4
|
+
factoriesDirectlyReturnedObjects,
|
|
5
|
+
factoriesObjectPropertyName,
|
|
6
|
+
optionsFirst,
|
|
7
|
+
} from '../utils/index.ts'
|
|
8
|
+
|
|
9
|
+
interface NoExtraFactoryKeysOptions {
|
|
10
|
+
keys: string[]
|
|
11
|
+
factoryPattern?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Checks direct object returns from selected top-level factories and rejects keys outside the configured allow-list.
|
|
16
|
+
*
|
|
17
|
+
* Example: With `keys: [run]`, `return { run }` passes while `return { run, preview }` fails.
|
|
18
|
+
*/
|
|
19
|
+
export const noExtraFactoryKeys = defineRule({
|
|
20
|
+
meta: {
|
|
21
|
+
type: 'problem',
|
|
22
|
+
schema: [{
|
|
23
|
+
type: 'object',
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
properties: {
|
|
26
|
+
keys: { type: 'array', items: { type: 'string' } },
|
|
27
|
+
factoryPattern: { type: 'string' },
|
|
28
|
+
},
|
|
29
|
+
required: ['keys'],
|
|
30
|
+
}],
|
|
31
|
+
messages: {
|
|
32
|
+
extraKey: "Factory '{{factory}}' must not return key '{{key}}'; allowed keys: {{allowed}}.",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
createOnce(context) {
|
|
36
|
+
return {
|
|
37
|
+
Program(program) {
|
|
38
|
+
const { keys, factoryPattern = '^make[A-Z]' } = optionsFirst<NoExtraFactoryKeysOptions>(context)
|
|
39
|
+
|
|
40
|
+
for (const factory of factoriesCollectTopLevel(program, new RegExp(factoryPattern))) {
|
|
41
|
+
for (const object of factoriesDirectlyReturnedObjects(factory)) {
|
|
42
|
+
for (const property of object.properties) {
|
|
43
|
+
const key = factoriesObjectPropertyName(property)
|
|
44
|
+
|
|
45
|
+
if (!key || !keys.includes(key)) {
|
|
46
|
+
context.report({
|
|
47
|
+
node: property,
|
|
48
|
+
messageId: 'extraKey',
|
|
49
|
+
data: { factory: factory.name, key: key ?? '<computed or spread>', allowed: keys.join(', ') },
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { defineRule } from '@oxlint/plugins'
|
|
2
|
+
import { declarationsFileLevelFunctionCandidates, optionsFirst, reactComponentsIsLike } from '../utils/index.ts'
|
|
3
|
+
|
|
4
|
+
interface NoFileLevelHelpersOptions {
|
|
5
|
+
allowPattern?: string
|
|
6
|
+
detectComponents?: boolean
|
|
7
|
+
hookPattern?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Rejects top-level functions unless they are recognized components, hooks, or names allowed by configuration.
|
|
12
|
+
*
|
|
13
|
+
* Example: `useDialog` can pass as a hook, while a file-level `buildPayload` helper fails.
|
|
14
|
+
*/
|
|
15
|
+
export const noFileLevelHelpers = defineRule({
|
|
16
|
+
meta: {
|
|
17
|
+
type: 'problem',
|
|
18
|
+
schema: [{
|
|
19
|
+
type: 'object',
|
|
20
|
+
additionalProperties: false,
|
|
21
|
+
properties: {
|
|
22
|
+
allowPattern: { type: 'string' },
|
|
23
|
+
detectComponents: { type: 'boolean' },
|
|
24
|
+
hookPattern: { type: 'string' },
|
|
25
|
+
},
|
|
26
|
+
}],
|
|
27
|
+
messages: {
|
|
28
|
+
helper: 'File-level helper {{name}} must move inside its owning component or hook.',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
createOnce(context) {
|
|
32
|
+
return {
|
|
33
|
+
Program(program) {
|
|
34
|
+
const {
|
|
35
|
+
allowPattern,
|
|
36
|
+
detectComponents = true,
|
|
37
|
+
hookPattern = '^use[A-Z]',
|
|
38
|
+
} = optionsFirst<NoFileLevelHelpersOptions>(context, {})
|
|
39
|
+
const allowed = allowPattern ? new RegExp(allowPattern) : null
|
|
40
|
+
const hooks = new RegExp(hookPattern)
|
|
41
|
+
|
|
42
|
+
for (const candidate of declarationsFileLevelFunctionCandidates(program)) {
|
|
43
|
+
if ((detectComponents && reactComponentsIsLike(program, candidate))
|
|
44
|
+
|| hooks.test(candidate.name)
|
|
45
|
+
|| allowed?.test(candidate.name)) {
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
context.report({
|
|
50
|
+
node: candidate.node,
|
|
51
|
+
messageId: 'helper',
|
|
52
|
+
data: { name: candidate.name },
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { defineRule } from '@oxlint/plugins'
|
|
2
|
+
import type { ESTree } from '@oxlint/plugins'
|
|
3
|
+
|
|
4
|
+
function importedAliasTarget(declaration: ESTree.Declaration | null, imports: Set<string>): string | undefined {
|
|
5
|
+
if (declaration?.type !== 'TSTypeAliasDeclaration'
|
|
6
|
+
|| declaration.typeAnnotation.type !== 'TSTypeReference'
|
|
7
|
+
|| declaration.typeAnnotation.typeName.type !== 'Identifier'
|
|
8
|
+
|| !imports.has(declaration.typeAnnotation.typeName.name)) {
|
|
9
|
+
return undefined
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return declaration.typeAnnotation.typeName.name
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function specifierLocalName(specifier: ESTree.ExportSpecifier): string {
|
|
16
|
+
return specifier.local.type === 'Identifier' ? specifier.local.name : specifier.local.value
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Rejects exported type aliases that merely rename an imported type without adding information.
|
|
21
|
+
*
|
|
22
|
+
* Example: `export type User = UserDto` fails when `UserDto` is imported; intersections and unions pass.
|
|
23
|
+
*/
|
|
24
|
+
export const noImportedTypeAlias = defineRule({
|
|
25
|
+
meta: {
|
|
26
|
+
type: 'problem',
|
|
27
|
+
schema: [],
|
|
28
|
+
messages: {
|
|
29
|
+
alias: "Exported type '{{alias}}' must not re-alias imported type '{{name}}'.",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
createOnce(context) {
|
|
33
|
+
return {
|
|
34
|
+
Program(program) {
|
|
35
|
+
const imports = new Set(program.body.flatMap((statement) => statement.type === 'ImportDeclaration'
|
|
36
|
+
? statement.specifiers.map((specifier) => specifier.local.name)
|
|
37
|
+
: []))
|
|
38
|
+
const aliases = new Map<string, string>()
|
|
39
|
+
|
|
40
|
+
for (const statement of program.body) {
|
|
41
|
+
const declaration = statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration'
|
|
42
|
+
? statement.declaration
|
|
43
|
+
: statement
|
|
44
|
+
const imported = declaration && declaration.type.endsWith('Declaration')
|
|
45
|
+
? importedAliasTarget(declaration as ESTree.Declaration, imports)
|
|
46
|
+
: undefined
|
|
47
|
+
|
|
48
|
+
if (declaration?.type === 'TSTypeAliasDeclaration' && imported) {
|
|
49
|
+
aliases.set(declaration.id.name, imported)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const statement of program.body) {
|
|
54
|
+
if (statement.type !== 'ExportNamedDeclaration') {
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const inline = importedAliasTarget(statement.declaration, imports)
|
|
59
|
+
if (inline && statement.declaration?.type === 'TSTypeAliasDeclaration') {
|
|
60
|
+
context.report({
|
|
61
|
+
node: statement.declaration,
|
|
62
|
+
messageId: 'alias',
|
|
63
|
+
data: { alias: statement.declaration.id.name, name: inline },
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const specifier of statement.specifiers) {
|
|
68
|
+
const local = specifierLocalName(specifier)
|
|
69
|
+
const imported = aliases.get(local)
|
|
70
|
+
|
|
71
|
+
if (imported) {
|
|
72
|
+
context.report({
|
|
73
|
+
node: specifier,
|
|
74
|
+
messageId: 'alias',
|
|
75
|
+
data: { alias: local, name: imported },
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { defineRule } from '@oxlint/plugins'
|
|
2
|
+
import {
|
|
3
|
+
astStaticMemberName,
|
|
4
|
+
optionsFirst,
|
|
5
|
+
schemasElements,
|
|
6
|
+
schemasIsAllowedElement,
|
|
7
|
+
schemasIsCall,
|
|
8
|
+
} from '../utils/index.ts'
|
|
9
|
+
|
|
10
|
+
interface NoInlineSchemaElementsOptions {
|
|
11
|
+
namespaces: string[]
|
|
12
|
+
methods: string[]
|
|
13
|
+
structuralMethods: string[]
|
|
14
|
+
allowZodScalars: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Requires configured schema combinators to reference named schemas instead of defining structural schemas inline.
|
|
19
|
+
*
|
|
20
|
+
* Example: `z.array(userSchema)` passes; `z.array(z.object({ id: z.string() }))` fails.
|
|
21
|
+
*/
|
|
22
|
+
export const noInlineSchemaElements = defineRule({
|
|
23
|
+
meta: {
|
|
24
|
+
type: 'problem',
|
|
25
|
+
schema: [{
|
|
26
|
+
type: 'object',
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
properties: {
|
|
29
|
+
namespaces: { type: 'array', items: { type: 'string' } },
|
|
30
|
+
methods: { type: 'array', items: { type: 'string' } },
|
|
31
|
+
structuralMethods: { type: 'array', items: { type: 'string' } },
|
|
32
|
+
allowZodScalars: { type: 'boolean' },
|
|
33
|
+
},
|
|
34
|
+
}],
|
|
35
|
+
defaultOptions: [{
|
|
36
|
+
namespaces: ['z'],
|
|
37
|
+
methods: ['array', 'union', 'record', 'tuple'],
|
|
38
|
+
structuralMethods: ['array', 'discriminatedUnion', 'intersection', 'lazy', 'object', 'record', 'tuple', 'union'],
|
|
39
|
+
allowZodScalars: true,
|
|
40
|
+
}],
|
|
41
|
+
messages: { inline: 'z.{{method}}() elements must use named schemas.' },
|
|
42
|
+
},
|
|
43
|
+
createOnce(context) {
|
|
44
|
+
return {
|
|
45
|
+
CallExpression(node) {
|
|
46
|
+
if (node.callee.type !== 'MemberExpression') {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const options = optionsFirst<NoInlineSchemaElementsOptions>(context)
|
|
51
|
+
const namespaces = new Set(options.namespaces)
|
|
52
|
+
const method = astStaticMemberName(node.callee)
|
|
53
|
+
|
|
54
|
+
if (!schemasIsCall(node, namespaces) || !method || !options.methods.includes(method)) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const element of schemasElements(node)) {
|
|
59
|
+
if (!schemasIsAllowedElement(element, options.allowZodScalars, namespaces, new Set(options.structuralMethods))) {
|
|
60
|
+
context.report({
|
|
61
|
+
node: element,
|
|
62
|
+
messageId: 'inline',
|
|
63
|
+
data: { method },
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
})
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { defineRule } from '@oxlint/plugins'
|
|
2
|
+
import {
|
|
3
|
+
namingPosixPath,
|
|
4
|
+
optionsFirst,
|
|
5
|
+
schemasIsConstruction,
|
|
6
|
+
schemasIsRuntimeImport,
|
|
7
|
+
schemasIsRuntimeReexport,
|
|
8
|
+
schemasRuntimeImportLocals,
|
|
9
|
+
} from '../utils/index.ts'
|
|
10
|
+
|
|
11
|
+
interface NoLocalSchemaConstructionOptions {
|
|
12
|
+
packages?: string[]
|
|
13
|
+
namespaces?: string[]
|
|
14
|
+
allowIn?: string[]
|
|
15
|
+
allowPathPatterns?: string[]
|
|
16
|
+
message?: string
|
|
17
|
+
runtimeImportMessage?: string
|
|
18
|
+
constructionMessage?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const defaultRuntimeImportMessage = 'Do not import runtime schema constructors from {{source}}; import a named schema instead.'
|
|
22
|
+
const defaultConstructionMessage = 'Do not construct schemas locally; import a named schema instead.'
|
|
23
|
+
|
|
24
|
+
function formatMessage(template: string, data: Record<string, string>): string {
|
|
25
|
+
return Object.entries(data).reduce((result, [key, value]) => result.replaceAll(`{{${key}}}`, value), template)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Rejects runtime schema-library imports and local schema construction, while allowing imported named schemas.
|
|
30
|
+
*
|
|
31
|
+
* Example: `schemaResolver(userSchema)` passes after importing `userSchema`; `import { z } from 'zod'` and `z.object({})` fail.
|
|
32
|
+
*/
|
|
33
|
+
export const noLocalSchemaConstruction = defineRule({
|
|
34
|
+
meta: {
|
|
35
|
+
type: 'problem',
|
|
36
|
+
schema: [{
|
|
37
|
+
type: 'object',
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
properties: {
|
|
40
|
+
packages: { type: 'array', items: { type: 'string' } },
|
|
41
|
+
namespaces: { type: 'array', items: { type: 'string' } },
|
|
42
|
+
allowIn: { type: 'array', items: { type: 'string' } },
|
|
43
|
+
allowPathPatterns: { type: 'array', items: { type: 'string' } },
|
|
44
|
+
message: { type: 'string' },
|
|
45
|
+
runtimeImportMessage: { type: 'string' },
|
|
46
|
+
constructionMessage: { type: 'string' },
|
|
47
|
+
},
|
|
48
|
+
}],
|
|
49
|
+
defaultOptions: [{
|
|
50
|
+
packages: ['zod'],
|
|
51
|
+
namespaces: ['z'],
|
|
52
|
+
}],
|
|
53
|
+
messages: {
|
|
54
|
+
runtimeImport: '{{message}}',
|
|
55
|
+
construction: '{{message}}',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
createOnce(context) {
|
|
59
|
+
let constructors = new Set<string>()
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
before() {
|
|
63
|
+
const {
|
|
64
|
+
namespaces = ['z'],
|
|
65
|
+
allowIn = [],
|
|
66
|
+
allowPathPatterns = [],
|
|
67
|
+
} = optionsFirst<NoLocalSchemaConstructionOptions>(context, {})
|
|
68
|
+
const filename = namingPosixPath(context.filename)
|
|
69
|
+
constructors = new Set(namespaces)
|
|
70
|
+
|
|
71
|
+
if (allowIn.some((suffix) => filename.endsWith(suffix))
|
|
72
|
+
|| allowPathPatterns.some((pattern) => new RegExp(pattern).test(filename))) {
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
ImportDeclaration(node) {
|
|
77
|
+
const {
|
|
78
|
+
packages = ['zod'],
|
|
79
|
+
message,
|
|
80
|
+
runtimeImportMessage,
|
|
81
|
+
} = optionsFirst<NoLocalSchemaConstructionOptions>(context, {})
|
|
82
|
+
|
|
83
|
+
if (!schemasIsRuntimeImport(node, new Set(packages))) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const name of schemasRuntimeImportLocals(node)) {
|
|
88
|
+
constructors.add(name)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
context.report({
|
|
92
|
+
node,
|
|
93
|
+
messageId: 'runtimeImport',
|
|
94
|
+
data: {
|
|
95
|
+
message: formatMessage(
|
|
96
|
+
runtimeImportMessage ?? message ?? defaultRuntimeImportMessage,
|
|
97
|
+
{ source: node.source.value },
|
|
98
|
+
),
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
},
|
|
102
|
+
ExportNamedDeclaration(node) {
|
|
103
|
+
const {
|
|
104
|
+
packages = ['zod'],
|
|
105
|
+
message,
|
|
106
|
+
runtimeImportMessage,
|
|
107
|
+
} = optionsFirst<NoLocalSchemaConstructionOptions>(context, {})
|
|
108
|
+
|
|
109
|
+
if (!schemasIsRuntimeReexport(node, new Set(packages)) || !node.source) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
context.report({
|
|
114
|
+
node,
|
|
115
|
+
messageId: 'runtimeImport',
|
|
116
|
+
data: {
|
|
117
|
+
message: formatMessage(
|
|
118
|
+
runtimeImportMessage ?? message ?? defaultRuntimeImportMessage,
|
|
119
|
+
{ source: node.source.value },
|
|
120
|
+
),
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
},
|
|
124
|
+
ExportAllDeclaration(node) {
|
|
125
|
+
const {
|
|
126
|
+
packages = ['zod'],
|
|
127
|
+
message,
|
|
128
|
+
runtimeImportMessage,
|
|
129
|
+
} = optionsFirst<NoLocalSchemaConstructionOptions>(context, {})
|
|
130
|
+
|
|
131
|
+
if (!schemasIsRuntimeReexport(node, new Set(packages))) {
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
context.report({
|
|
136
|
+
node,
|
|
137
|
+
messageId: 'runtimeImport',
|
|
138
|
+
data: {
|
|
139
|
+
message: formatMessage(
|
|
140
|
+
runtimeImportMessage ?? message ?? defaultRuntimeImportMessage,
|
|
141
|
+
{ source: node.source.value },
|
|
142
|
+
),
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
},
|
|
146
|
+
CallExpression(node) {
|
|
147
|
+
if (!schemasIsConstruction(node, constructors)) {
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const { message, constructionMessage } = optionsFirst<NoLocalSchemaConstructionOptions>(context, {})
|
|
152
|
+
context.report({
|
|
153
|
+
node,
|
|
154
|
+
messageId: 'construction',
|
|
155
|
+
data: {
|
|
156
|
+
message: formatMessage(constructionMessage ?? message ?? defaultConstructionMessage, {}),
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { defineRule } from '@oxlint/plugins'
|
|
2
|
+
import { optionsFirst, schemasTypeOperator } from '../utils/index.ts'
|
|
3
|
+
|
|
4
|
+
interface NoRederiveSchemaOptions {
|
|
5
|
+
from: string[]
|
|
6
|
+
namespaces?: string[]
|
|
7
|
+
operators?: string[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Prevents deriving types from configured imported schemas when the package should export and consume a named type.
|
|
12
|
+
*
|
|
13
|
+
* Example: `z.infer<typeof importedSchema>` fails; deriving from a same-file schema passes.
|
|
14
|
+
*/
|
|
15
|
+
export const noRederiveSchema = defineRule({
|
|
16
|
+
meta: {
|
|
17
|
+
type: 'problem',
|
|
18
|
+
schema: [{
|
|
19
|
+
type: 'object',
|
|
20
|
+
additionalProperties: false,
|
|
21
|
+
properties: {
|
|
22
|
+
from: { type: 'array', items: { type: 'string' } },
|
|
23
|
+
namespaces: { type: 'array', items: { type: 'string' } },
|
|
24
|
+
operators: { type: 'array', items: { type: 'string' } },
|
|
25
|
+
},
|
|
26
|
+
required: ['from'],
|
|
27
|
+
}],
|
|
28
|
+
messages: {
|
|
29
|
+
rederive: 'Do not derive {{operator}} from imported schema {{schema}}; import its DTO type.',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
createOnce(context) {
|
|
33
|
+
let imported = new Set<string>()
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
Program(program) {
|
|
37
|
+
const { from } = optionsFirst<NoRederiveSchemaOptions>(context)
|
|
38
|
+
imported = new Set(program.body.flatMap((statement) => statement.type === 'ImportDeclaration'
|
|
39
|
+
&& from.includes(statement.source.value)
|
|
40
|
+
? statement.specifiers.map((specifier) => specifier.local.name)
|
|
41
|
+
: []))
|
|
42
|
+
},
|
|
43
|
+
TSTypeReference(node) {
|
|
44
|
+
const {
|
|
45
|
+
namespaces = ['z'],
|
|
46
|
+
operators = ['infer', 'input'],
|
|
47
|
+
} = optionsFirst<NoRederiveSchemaOptions>(context)
|
|
48
|
+
const operator = schemasTypeOperator(node, new Set(namespaces), new Set(operators))
|
|
49
|
+
const parameter = node.typeArguments?.params[0]
|
|
50
|
+
|
|
51
|
+
if (!operator
|
|
52
|
+
|| parameter?.type !== 'TSTypeQuery'
|
|
53
|
+
|| parameter.exprName.type !== 'Identifier'
|
|
54
|
+
|| !imported.has(parameter.exprName.name)) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
context.report({
|
|
59
|
+
node,
|
|
60
|
+
messageId: 'rederive',
|
|
61
|
+
data: { operator, schema: parameter.exprName.name },
|
|
62
|
+
})
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineRule } from '@oxlint/plugins'
|
|
2
|
+
import { optionsFirst } from '../utils/index.ts'
|
|
3
|
+
|
|
4
|
+
interface NoRestrictedFilesOptions {
|
|
5
|
+
message: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Reports every file matched by the consumer glob, making forbidden file locations or extensions lintable.
|
|
10
|
+
*
|
|
11
|
+
* Example: Enable it for hook TSX files to require hooks to use `.ts` files.
|
|
12
|
+
*/
|
|
13
|
+
export const noRestrictedFiles = defineRule({
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
schema: [{
|
|
17
|
+
type: 'object',
|
|
18
|
+
additionalProperties: false,
|
|
19
|
+
properties: {
|
|
20
|
+
message: { type: 'string' },
|
|
21
|
+
},
|
|
22
|
+
required: ['message'],
|
|
23
|
+
}],
|
|
24
|
+
messages: {
|
|
25
|
+
restricted: '{{message}}',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
createOnce(context) {
|
|
29
|
+
return {
|
|
30
|
+
Program(program) {
|
|
31
|
+
const { message } = optionsFirst<NoRestrictedFilesOptions>(context)
|
|
32
|
+
context.report({
|
|
33
|
+
node: program,
|
|
34
|
+
messageId: 'restricted',
|
|
35
|
+
data: { message },
|
|
36
|
+
})
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { defineRule } from '@oxlint/plugins'
|
|
2
|
+
import { namingPosixPath, optionsFirst } from '../utils/index.ts'
|
|
3
|
+
import type { ESTree } from '@oxlint/plugins'
|
|
4
|
+
|
|
5
|
+
interface NoRestrictedTokenOptions {
|
|
6
|
+
token: string
|
|
7
|
+
allowIn: string[]
|
|
8
|
+
allowPathPatterns?: string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function tokenIsTypeOnlyImport(node: ESTree.Node): boolean {
|
|
12
|
+
const parent = node.parent
|
|
13
|
+
|
|
14
|
+
if (parent?.type === 'ImportSpecifier') {
|
|
15
|
+
return parent.importKind === 'type'
|
|
16
|
+
|| (parent.parent.type === 'ImportDeclaration' && parent.parent.importKind === 'type')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (parent?.type === 'ImportDefaultSpecifier' || parent?.type === 'ImportNamespaceSpecifier')
|
|
20
|
+
&& parent.parent.type === 'ImportDeclaration'
|
|
21
|
+
&& parent.parent.importKind === 'type'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Restricts one identifier to configured owner paths while leaving path selection to the consumer.
|
|
26
|
+
*
|
|
27
|
+
* Example: `RouterClient` can be allowed in `rpc.client.ts` and rejected everywhere else.
|
|
28
|
+
*/
|
|
29
|
+
export const noRestrictedToken = defineRule({
|
|
30
|
+
meta: {
|
|
31
|
+
type: 'problem',
|
|
32
|
+
schema: [{
|
|
33
|
+
type: 'object',
|
|
34
|
+
additionalProperties: false,
|
|
35
|
+
properties: {
|
|
36
|
+
token: { type: 'string' },
|
|
37
|
+
allowIn: { type: 'array', items: { type: 'string' } },
|
|
38
|
+
allowPathPatterns: { type: 'array', items: { type: 'string' } },
|
|
39
|
+
},
|
|
40
|
+
required: ['token', 'allowIn'],
|
|
41
|
+
}],
|
|
42
|
+
messages: {
|
|
43
|
+
restricted: "'{{token}}' may only appear in configured owner paths.",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
createOnce(context) {
|
|
47
|
+
return {
|
|
48
|
+
before() {
|
|
49
|
+
const options = optionsFirst<NoRestrictedTokenOptions>(context)
|
|
50
|
+
const filename = namingPosixPath(context.filename)
|
|
51
|
+
const allowed = options.allowIn.some((suffix) => filename.endsWith(suffix))
|
|
52
|
+
|| (options.allowPathPatterns ?? []).some((pattern) => new RegExp(pattern).test(filename))
|
|
53
|
+
|
|
54
|
+
if (allowed) {
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
Identifier(node) {
|
|
59
|
+
const { token } = optionsFirst<NoRestrictedTokenOptions>(context)
|
|
60
|
+
|
|
61
|
+
if (node.name === token && !tokenIsTypeOnlyImport(node)) {
|
|
62
|
+
context.report({
|
|
63
|
+
node,
|
|
64
|
+
messageId: 'restricted',
|
|
65
|
+
data: { token },
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { defineRule } from '@oxlint/plugins'
|
|
2
|
+
import {
|
|
3
|
+
declarationsIsRuntime,
|
|
4
|
+
declarationsRuntimeKind,
|
|
5
|
+
declarationsRuntimeName,
|
|
6
|
+
optionsFirst,
|
|
7
|
+
} from '../utils/index.ts'
|
|
8
|
+
|
|
9
|
+
interface NoRuntimeInTypesOptions {
|
|
10
|
+
allow?: ('class' | 'default' | 'enum' | 'function' | 'variable')[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Keeps consumer-selected type modules free of runtime declarations, with optional declaration-category exceptions.
|
|
15
|
+
*
|
|
16
|
+
* Example: An interface passes; `export const value = 1` fails unless variables are allowed.
|
|
17
|
+
*/
|
|
18
|
+
export const noRuntimeInTypes = defineRule({
|
|
19
|
+
meta: {
|
|
20
|
+
type: 'problem',
|
|
21
|
+
schema: [{
|
|
22
|
+
type: 'object',
|
|
23
|
+
additionalProperties: false,
|
|
24
|
+
properties: {
|
|
25
|
+
allow: {
|
|
26
|
+
type: 'array',
|
|
27
|
+
items: { type: 'string', enum: ['class', 'default', 'enum', 'function', 'variable'] },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
}],
|
|
31
|
+
messages: {
|
|
32
|
+
runtimeValue: 'Types files must not contain runtime value {{name}}.',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
createOnce(context) {
|
|
36
|
+
return {
|
|
37
|
+
Program(program) {
|
|
38
|
+
const { allow = [] } = optionsFirst<NoRuntimeInTypesOptions>(context, {})
|
|
39
|
+
|
|
40
|
+
for (const statement of program.body) {
|
|
41
|
+
const declaration = statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration'
|
|
42
|
+
? statement.declaration
|
|
43
|
+
: statement
|
|
44
|
+
|
|
45
|
+
if (!declaration) {
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (declarationsIsRuntime(declaration)) {
|
|
50
|
+
if (!declaration.declare && !allow.includes(declarationsRuntimeKind(declaration))) {
|
|
51
|
+
context.report({
|
|
52
|
+
node: declaration,
|
|
53
|
+
messageId: 'runtimeValue',
|
|
54
|
+
data: { name: declarationsRuntimeName(declaration) },
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!allow.includes('default') && statement.type === 'ExportDefaultDeclaration'
|
|
61
|
+
&& declaration.type !== 'TSInterfaceDeclaration' && declaration.type !== 'TSTypeAliasDeclaration') {
|
|
62
|
+
context.report({
|
|
63
|
+
node: statement,
|
|
64
|
+
messageId: 'runtimeValue',
|
|
65
|
+
data: { name: 'default' },
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
})
|