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,31 @@
|
|
|
1
|
+
import { astStaticMemberName } from './ast.ts'
|
|
2
|
+
import type { ESTree } from '@oxlint/plugins'
|
|
3
|
+
|
|
4
|
+
export function orpcFindChainCall(expression: ESTree.Expression, method: string): ESTree.CallExpression | undefined {
|
|
5
|
+
if (expression.type === 'CallExpression') {
|
|
6
|
+
if (expression.callee.type === 'MemberExpression' && astStaticMemberName(expression.callee) === method) {
|
|
7
|
+
return expression
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (expression.callee.type === 'MemberExpression') {
|
|
11
|
+
return orpcFindChainCall(expression.callee.object, method)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (expression.type === 'MemberExpression') {
|
|
16
|
+
return orpcFindChainCall(expression.object, method)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (expression.type === 'ChainExpression') {
|
|
20
|
+
return orpcFindChainCall(expression.expression, method)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function orpcIsNamedOrComposedArgument(argument: ESTree.Argument | undefined, composers: Set<string>): boolean {
|
|
25
|
+
if (argument?.type === 'Identifier') {
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return argument?.type === 'CallExpression' && argument.callee.type === 'Identifier'
|
|
30
|
+
&& composers.has(argument.callee.name) && argument.arguments[0]?.type === 'Identifier'
|
|
31
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {
|
|
2
|
+
astCallName,
|
|
3
|
+
astContainsJsx,
|
|
4
|
+
astDirectReturnExpressions,
|
|
5
|
+
astNearestFunction,
|
|
6
|
+
astVisit,
|
|
7
|
+
} from './ast.ts'
|
|
8
|
+
import { exportsCollect } from './exports.ts'
|
|
9
|
+
import type { AstRuntimeFunction } from './ast.ts'
|
|
10
|
+
import type { ESTree } from '@oxlint/plugins'
|
|
11
|
+
|
|
12
|
+
export interface ReactComponentsCandidate {
|
|
13
|
+
name: string
|
|
14
|
+
node: ESTree.Node
|
|
15
|
+
declaration?: ESTree.Declaration
|
|
16
|
+
initializer?: ESTree.Expression | null
|
|
17
|
+
typeOnly?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function reactComponentsExportCandidates(program: ESTree.Program): ReactComponentsCandidate[] {
|
|
21
|
+
return exportsCollect(program).map((binding) => ({
|
|
22
|
+
...binding,
|
|
23
|
+
name: binding.localName ?? binding.exportedName,
|
|
24
|
+
}))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function importedPropsWithChildren(program: ESTree.Program): Set<string> {
|
|
28
|
+
return new Set(program.body.flatMap((statement) => statement.type === 'ImportDeclaration'
|
|
29
|
+
&& statement.source.value === 'react'
|
|
30
|
+
? statement.specifiers.flatMap((specifier) => specifier.type === 'ImportSpecifier'
|
|
31
|
+
&& specifier.imported.type === 'Identifier'
|
|
32
|
+
&& specifier.imported.name === 'PropsWithChildren'
|
|
33
|
+
? [specifier.local.name]
|
|
34
|
+
: [])
|
|
35
|
+
: []))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function containsNamedTypeReference(node: unknown, names: Set<string>): boolean {
|
|
39
|
+
let found = false
|
|
40
|
+
astVisit(node, [], (candidate) => {
|
|
41
|
+
if (candidate.type === 'TSTypeReference'
|
|
42
|
+
&& candidate.typeName.type === 'Identifier'
|
|
43
|
+
&& names.has(candidate.typeName.name)) {
|
|
44
|
+
found = true
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return found
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function returnsReactChildren(program: ESTree.Program, node: AstRuntimeFunction, expressions: ESTree.Expression[]): boolean {
|
|
52
|
+
const returnedNames = new Set(expressions.flatMap((expression) => expression.type === 'Identifier' ? [expression.name] : []))
|
|
53
|
+
const propsWithChildren = importedPropsWithChildren(program)
|
|
54
|
+
|
|
55
|
+
return node.params.some((param) => {
|
|
56
|
+
if (param.type !== 'ObjectPattern') {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const returnsChild = param.properties.some((property) => property.type === 'Property'
|
|
61
|
+
&& property.value.type === 'Identifier'
|
|
62
|
+
&& returnedNames.has(property.value.name))
|
|
63
|
+
return returnsChild && containsNamedTypeReference(param.typeAnnotation, propsWithChildren)
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function reactComponentsFunctionReturnsOutput(program: ESTree.Program, node: AstRuntimeFunction): boolean {
|
|
68
|
+
const expressions = astDirectReturnExpressions(node)
|
|
69
|
+
return expressions.some((expression) => astContainsJsx(expression) || (expression.type === 'Literal' && expression.value === null))
|
|
70
|
+
|| returnsReactChildren(program, node, expressions)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function functionReturnsLocalComponent(program: ESTree.Program, node: ESTree.Function): boolean {
|
|
74
|
+
const componentNames = new Set<string>()
|
|
75
|
+
astVisit(node.body, [node], (candidate, ancestors) => {
|
|
76
|
+
if (candidate.type === 'FunctionDeclaration'
|
|
77
|
+
&& candidate !== node
|
|
78
|
+
&& candidate.id?.name
|
|
79
|
+
&& /^[A-Z]/.test(candidate.id.name)
|
|
80
|
+
&& astNearestFunction(ancestors) === node
|
|
81
|
+
&& reactComponentsFunctionReturnsOutput(program, candidate)) {
|
|
82
|
+
componentNames.add(candidate.id.name)
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
return astDirectReturnExpressions(node)
|
|
87
|
+
.some((expression) => expression.type === 'Identifier' && componentNames.has(expression.name))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function componentFactoryNames(program: ESTree.Program): Set<string> {
|
|
91
|
+
return new Set(program.body.flatMap((statement) => statement.type === 'FunctionDeclaration'
|
|
92
|
+
&& statement.id?.name
|
|
93
|
+
&& functionReturnsLocalComponent(program, statement)
|
|
94
|
+
? [statement.id.name]
|
|
95
|
+
: []))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isComponentInitializer(program: ESTree.Program, initializer: ESTree.Expression | null | undefined): boolean {
|
|
99
|
+
if (!initializer) {
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (initializer.type === 'ArrowFunctionExpression' || initializer.type === 'FunctionExpression') {
|
|
104
|
+
return reactComponentsFunctionReturnsOutput(program, initializer)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (initializer.type !== 'CallExpression') {
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const name = astCallName(initializer)
|
|
112
|
+
|
|
113
|
+
if (name === 'forwardRef' || name === 'memo') {
|
|
114
|
+
return initializer.arguments.some((argument) => argument.type !== 'SpreadElement' && isComponentInitializer(program, argument))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (name === 'lazy') {
|
|
118
|
+
return initializer.arguments.some((argument) => argument.type === 'ArrowFunctionExpression' || argument.type === 'FunctionExpression')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return Boolean(name?.startsWith('create') && componentFactoryNames(program).has(name))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function reactComponentsIsLike(program: ESTree.Program, candidate: ReactComponentsCandidate): boolean {
|
|
125
|
+
if (!/^[A-Z]/.test(candidate.name)) {
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const declaration = candidate.declaration ?? candidate.node
|
|
130
|
+
|
|
131
|
+
if (declaration.type === 'FunctionDeclaration') {
|
|
132
|
+
return reactComponentsFunctionReturnsOutput(program, declaration)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return declaration.type === 'VariableDeclaration'
|
|
136
|
+
&& isComponentInitializer(program, candidate.initializer)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function reactComponentsPascalFromBasename(basename: string): string {
|
|
140
|
+
return basename
|
|
141
|
+
.replace(/\.(tsx?|jsx?)$/, '')
|
|
142
|
+
.split(/[-.]/)
|
|
143
|
+
.filter(Boolean)
|
|
144
|
+
.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
|
|
145
|
+
.join('')
|
|
146
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { astCallName, astVisit } from './ast.ts'
|
|
2
|
+
import type { Context, ESTree } from '@oxlint/plugins'
|
|
3
|
+
|
|
4
|
+
export function routesInspectFunction(
|
|
5
|
+
context: Context,
|
|
6
|
+
item: { name: string, node: ESTree.Function | ESTree.ArrowFunctionExpression },
|
|
7
|
+
bannedHooks: Set<string>,
|
|
8
|
+
banIntrinsicJsx: boolean,
|
|
9
|
+
): void {
|
|
10
|
+
astVisit(item.node.body, [item.node], (node) => {
|
|
11
|
+
if (banIntrinsicJsx && node.type === 'JSXOpeningElement' && node.name.type === 'JSXIdentifier' && /^[a-z]/.test(node.name.name)) {
|
|
12
|
+
context.report({
|
|
13
|
+
node,
|
|
14
|
+
messageId: 'hostJsx',
|
|
15
|
+
data: { name: item.name },
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (node.type === 'CallExpression') {
|
|
20
|
+
const name = astCallName(node)
|
|
21
|
+
|
|
22
|
+
if (name && bannedHooks.has(name)) {
|
|
23
|
+
context.report({
|
|
24
|
+
node,
|
|
25
|
+
messageId: 'bannedHook',
|
|
26
|
+
data: { hook: name, name: item.name },
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { astVisit } from './ast.ts'
|
|
2
|
+
import type { ESTree } from '@oxlint/plugins'
|
|
3
|
+
|
|
4
|
+
export function sanitizersCallsConfigured(node: unknown, names: Set<string>): boolean {
|
|
5
|
+
let found = false
|
|
6
|
+
astVisit(node, [], (candidate) => {
|
|
7
|
+
if (candidate.type === 'CallExpression' && candidate.callee.type === 'Identifier' && names.has(candidate.callee.name)) {
|
|
8
|
+
found = true
|
|
9
|
+
}
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
return found
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function sanitizersConfiguredBindings(program: ESTree.Program, names: Set<string>): Set<string> {
|
|
16
|
+
const result = new Set<string>()
|
|
17
|
+
|
|
18
|
+
for (const statement of program.body) {
|
|
19
|
+
const declaration = statement.type === 'ExportNamedDeclaration' ? statement.declaration : statement
|
|
20
|
+
|
|
21
|
+
if (declaration?.type !== 'VariableDeclaration') {
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const item of declaration.declarations) {
|
|
26
|
+
if (item.id.type === 'Identifier' && sanitizersCallsConfigured(item.init, names)) {
|
|
27
|
+
result.add(item.id.name)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return result
|
|
33
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { astStaticMemberName } from './ast.ts'
|
|
2
|
+
import type { ESTree } from '@oxlint/plugins'
|
|
3
|
+
|
|
4
|
+
function schemasIsRuntimeSpecifier(specifier: ESTree.ImportDeclarationSpecifier): boolean {
|
|
5
|
+
return specifier.type !== 'ImportSpecifier' || specifier.importKind !== 'type'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function schemasMemberRootName(node: ESTree.MemberExpression): string | undefined {
|
|
9
|
+
if (node.object.type === 'Identifier') {
|
|
10
|
+
return node.object.name
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return node.object.type === 'MemberExpression' ? schemasMemberRootName(node.object) : undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function schemasIsPackage(source: string, packages: ReadonlySet<string>): boolean {
|
|
17
|
+
return packages.has(source) || [...packages].some((pkg) => source.startsWith(`${pkg}/`))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function schemasIsCall(node: ESTree.CallExpression, namespaces: Set<string>): boolean {
|
|
21
|
+
return node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && namespaces.has(node.callee.object.name)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function schemasRuntimeImportLocals(node: ESTree.ImportDeclaration): string[] {
|
|
25
|
+
return node.importKind === 'type'
|
|
26
|
+
? []
|
|
27
|
+
: node.specifiers.flatMap((specifier) => schemasIsRuntimeSpecifier(specifier) ? [specifier.local.name] : [])
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function schemasIsRuntimeImport(node: ESTree.ImportDeclaration, packages: ReadonlySet<string>): boolean {
|
|
31
|
+
return schemasIsPackage(node.source.value, packages)
|
|
32
|
+
&& (node.specifiers.length === 0 ? node.importKind !== 'type' : schemasRuntimeImportLocals(node).length > 0)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function schemasIsRuntimeReexport(node: ESTree.ExportNamedDeclaration | ESTree.ExportAllDeclaration, packages: ReadonlySet<string>): boolean {
|
|
36
|
+
if (!node.source || !schemasIsPackage(node.source.value, packages) || node.exportKind === 'type') {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return node.type === 'ExportAllDeclaration'
|
|
41
|
+
|| node.specifiers.length === 0
|
|
42
|
+
|| node.specifiers.some((specifier) => specifier.exportKind !== 'type')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function schemasIsConstruction(node: ESTree.CallExpression, constructors: ReadonlySet<string>): boolean {
|
|
46
|
+
if (node.callee.type === 'Identifier') {
|
|
47
|
+
return constructors.has(node.callee.name)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (node.callee.type !== 'MemberExpression') {
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const root = schemasMemberRootName(node.callee)
|
|
55
|
+
return Boolean(root && constructors.has(root))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function schemasElements(node: ESTree.CallExpression): ESTree.Argument[] {
|
|
59
|
+
return node.arguments.flatMap((argument) => argument.type === 'ArrayExpression'
|
|
60
|
+
? argument.elements.filter((element): element is Exclude<ESTree.ArrayExpressionElement, null> => element !== null)
|
|
61
|
+
: [argument])
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function schemasIsAllowedElement(node: ESTree.Argument, allowScalars: boolean, namespaces: Set<string>, structuralMethods: Set<string>): boolean {
|
|
65
|
+
if (node.type === 'Identifier') {
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!allowScalars || node.type !== 'CallExpression' || !schemasIsCall(node, namespaces)) {
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const name = node.callee.type === 'MemberExpression' ? astStaticMemberName(node.callee) : undefined
|
|
74
|
+
return Boolean(name && !structuralMethods.has(name))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function schemasTypeOperator(node: ESTree.TSTypeReference, namespaces: Set<string>, operators: Set<string>): string | undefined {
|
|
78
|
+
return node.typeName.type === 'TSQualifiedName' && node.typeName.left.type === 'Identifier'
|
|
79
|
+
&& namespaces.has(node.typeName.left.name) && operators.has(node.typeName.right.name)
|
|
80
|
+
? node.typeName.right.name
|
|
81
|
+
: undefined
|
|
82
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './helpers/index.ts'
|