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.
Files changed (43) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +101 -0
  4. package/package.json +64 -0
  5. package/src/index.ts +57 -0
  6. package/src/rules/export-file-prefix.ts +70 -0
  7. package/src/rules/export-name-pattern.ts +71 -0
  8. package/src/rules/filename-export-name.ts +86 -0
  9. package/src/rules/filename-match.ts +47 -0
  10. package/src/rules/folder-prefix.ts +73 -0
  11. package/src/rules/no-extra-exports.ts +60 -0
  12. package/src/rules/no-extra-factory-keys.ts +58 -0
  13. package/src/rules/no-file-level-helpers.ts +58 -0
  14. package/src/rules/no-imported-type-alias.ts +83 -0
  15. package/src/rules/no-inline-schema-elements.ts +70 -0
  16. package/src/rules/no-local-schema-construction.ts +162 -0
  17. package/src/rules/no-rederive-schema.ts +66 -0
  18. package/src/rules/no-restricted-files.ts +40 -0
  19. package/src/rules/no-restricted-token.ts +71 -0
  20. package/src/rules/no-runtime-in-types.ts +72 -0
  21. package/src/rules/no-single-use-scalar-schema.ts +164 -0
  22. package/src/rules/no-top-level-functions.ts +77 -0
  23. package/src/rules/no-trivial-functions.ts +56 -0
  24. package/src/rules/no-unescaped-like.ts +67 -0
  25. package/src/rules/only-export-components.ts +78 -0
  26. package/src/rules/require-file-factory.ts +67 -0
  27. package/src/rules/require-object-params.ts +32 -0
  28. package/src/rules/require-orpc-output.ts +64 -0
  29. package/src/rules/require-paired-call.ts +71 -0
  30. package/src/rules/route-surface.ts +62 -0
  31. package/src/utils/helpers/ast.ts +101 -0
  32. package/src/utils/helpers/declarations.ts +203 -0
  33. package/src/utils/helpers/exports.ts +158 -0
  34. package/src/utils/helpers/factories.ts +67 -0
  35. package/src/utils/helpers/index.ts +11 -0
  36. package/src/utils/helpers/naming.ts +89 -0
  37. package/src/utils/helpers/options.ts +5 -0
  38. package/src/utils/helpers/orpc.ts +31 -0
  39. package/src/utils/helpers/react-components.ts +146 -0
  40. package/src/utils/helpers/routes.ts +31 -0
  41. package/src/utils/helpers/sanitizers.ts +33 -0
  42. package/src/utils/helpers/schemas.ts +82 -0
  43. 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'