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,62 @@
1
+ import { defineRule } from '@oxlint/plugins'
2
+ import {
3
+ declarationsTopLevelUnexportedFunctions,
4
+ exportsCollect,
5
+ optionsFirst,
6
+ routesInspectFunction,
7
+ } from '../utils/index.ts'
8
+
9
+ interface RouteSurfaceOptions {
10
+ exportName: string
11
+ bannedHooks: string[]
12
+ banIntrinsicJsx: boolean
13
+ }
14
+
15
+ /**
16
+ * Keeps route files on a small configurable surface: one export plus adapters without banned hooks or intrinsic JSX.
17
+ *
18
+ * Example: A route rendering `<UserPage />` passes; an adapter rendering `<div>` or calling `useState` fails.
19
+ */
20
+ export const routeSurface = defineRule({
21
+ meta: {
22
+ type: 'problem',
23
+ schema: [{
24
+ type: 'object',
25
+ additionalProperties: false,
26
+ properties: {
27
+ exportName: { type: 'string' },
28
+ bannedHooks: { type: 'array', items: { type: 'string' } },
29
+ banIntrinsicJsx: { type: 'boolean' },
30
+ },
31
+ }],
32
+ defaultOptions: [{ exportName: 'Route', bannedHooks: ['useState', 'useEffect', 'useMutation'], banIntrinsicJsx: true }],
33
+ messages: {
34
+ extraExport: "Route files may only export '{{expected}}'; found '{{actual}}'.",
35
+ hostJsx: 'Route adapter {{name}} must not render intrinsic JSX.',
36
+ bannedHook: 'Route adapter {{name}} must not call {{hook}}.',
37
+ },
38
+ },
39
+ createOnce(context) {
40
+ return {
41
+ Program(program) {
42
+ const { exportName, bannedHooks, banIntrinsicJsx } = optionsFirst<RouteSurfaceOptions>(context)
43
+
44
+ for (const binding of exportsCollect(program)) {
45
+ if (binding.exportedName !== exportName) {
46
+ context.report({
47
+ node: binding.node,
48
+ messageId: 'extraExport',
49
+ data: { expected: exportName, actual: binding.exportedName },
50
+ })
51
+ }
52
+ }
53
+
54
+ const banned = new Set(bannedHooks)
55
+
56
+ for (const item of declarationsTopLevelUnexportedFunctions(program)) {
57
+ routesInspectFunction(context, item, banned, banIntrinsicJsx)
58
+ }
59
+ },
60
+ }
61
+ },
62
+ })
@@ -0,0 +1,101 @@
1
+ import type { ESTree } from '@oxlint/plugins'
2
+
3
+ export type AstRuntimeFunction = ESTree.Function | ESTree.ArrowFunctionExpression
4
+
5
+ export function astIsNode(value: unknown): value is ESTree.Node {
6
+ return value !== null && typeof value === 'object' && typeof Reflect.get(value, 'type') === 'string'
7
+ }
8
+
9
+ export function astVisit(node: unknown, ancestors: ESTree.Node[], callback: (node: ESTree.Node, ancestors: ESTree.Node[]) => void): void {
10
+ if (Array.isArray(node)) {
11
+ for (const child of node) {
12
+ astVisit(child, ancestors, callback)
13
+ }
14
+ return
15
+ }
16
+
17
+ if (!astIsNode(node)) {
18
+ return
19
+ }
20
+
21
+ callback(node, ancestors)
22
+
23
+ for (const [key, value] of Object.entries(node)) {
24
+ if (!['parent', 'type', 'start', 'end', 'range', 'loc'].includes(key)) {
25
+ astVisit(value, [...ancestors, node], callback)
26
+ }
27
+ }
28
+ }
29
+
30
+ export function astContainsJsx(node: unknown): boolean {
31
+ let found = false
32
+ astVisit(node, [], (candidate) => {
33
+ if (candidate.type === 'JSXElement' || candidate.type === 'JSXFragment') {
34
+ found = true
35
+ }
36
+ })
37
+
38
+ return found
39
+ }
40
+
41
+ export function astNearestFunction(ancestors: readonly ESTree.Node[]): AstRuntimeFunction | undefined {
42
+ return [...ancestors].reverse().find((node): node is AstRuntimeFunction => node.type === 'FunctionDeclaration'
43
+ || node.type === 'FunctionExpression'
44
+ || node.type === 'ArrowFunctionExpression')
45
+ }
46
+
47
+ export function astDirectReturnExpressions(node: AstRuntimeFunction): ESTree.Expression[] {
48
+ if (node.type === 'ArrowFunctionExpression' && node.body.type !== 'BlockStatement') {
49
+ return [node.body]
50
+ }
51
+
52
+ const expressions: ESTree.Expression[] = []
53
+ astVisit(node.body, [node], (candidate, ancestors) => {
54
+ if (candidate.type === 'ReturnStatement' && candidate.argument && astNearestFunction(ancestors) === node) {
55
+ expressions.push(candidate.argument)
56
+ }
57
+ })
58
+
59
+ return expressions
60
+ }
61
+
62
+ export function astStaticMemberName(node: ESTree.MemberExpression): string | undefined {
63
+ return !node.computed && node.property.type === 'Identifier' ? node.property.name : undefined
64
+ }
65
+
66
+ export function astCallName(node: ESTree.CallExpression): string | undefined {
67
+ if (node.callee.type === 'Identifier') {
68
+ return node.callee.name
69
+ }
70
+
71
+ return node.callee.type === 'MemberExpression' ? astStaticMemberName(node.callee) : undefined
72
+ }
73
+
74
+ export function astImportedCallAliases(program: ESTree.Program): Map<string, string> {
75
+ const aliases = new Map<string, string>()
76
+
77
+ for (const statement of program.body) {
78
+ if (statement.type !== 'ImportDeclaration') {
79
+ continue
80
+ }
81
+
82
+ for (const specifier of statement.specifiers) {
83
+ if (specifier.type !== 'ImportSpecifier') {
84
+ continue
85
+ }
86
+
87
+ const imported = specifier.imported.type === 'Identifier' ? specifier.imported.name : specifier.imported.value
88
+ if (imported !== specifier.local.name) {
89
+ aliases.set(specifier.local.name, imported)
90
+ }
91
+ }
92
+ }
93
+
94
+ return aliases
95
+ }
96
+
97
+ export function astResolvedCallName(node: ESTree.CallExpression, aliases: ReadonlyMap<string, string>): string | undefined {
98
+ const name = astCallName(node)
99
+ return name ? aliases.get(name) ?? name : undefined
100
+ }
101
+
@@ -0,0 +1,203 @@
1
+ import { astVisit } from './ast.ts'
2
+ import type { AstRuntimeFunction } from './ast.ts'
3
+ import type { ReactComponentsCandidate } from './react-components.ts'
4
+ import type { ESTree } from '@oxlint/plugins'
5
+
6
+ export type DeclarationsRuntime = ESTree.VariableDeclaration | ESTree.Function | ESTree.Class | ESTree.TSEnumDeclaration
7
+
8
+ export function declarationsTopLevelUnexportedFunctions(program: ESTree.Program): { name: string, node: ESTree.Function | ESTree.ArrowFunctionExpression }[] {
9
+ return program.body.flatMap((statement) => {
10
+ if (statement.type === 'FunctionDeclaration') {
11
+ return [{ name: statement.id?.name ?? '<anonymous>', node: statement }]
12
+ }
13
+
14
+ if (statement.type !== 'VariableDeclaration') {
15
+ return []
16
+ }
17
+
18
+ return statement.declarations.flatMap((item) => item.init?.type === 'FunctionExpression' || item.init?.type === 'ArrowFunctionExpression'
19
+ ? [{ name: item.id.type === 'Identifier' ? item.id.name : '<anonymous>', node: item.init }]
20
+ : [])
21
+ })
22
+ }
23
+
24
+ export function declarationsFileLevelFunctionCandidates(program: ESTree.Program): ReactComponentsCandidate[] {
25
+ const result: ReactComponentsCandidate[] = []
26
+
27
+ for (const statement of program.body) {
28
+ const declaration = statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration' ? statement.declaration : statement
29
+
30
+ if (declaration?.type === 'FunctionDeclaration') {
31
+ result.push({ name: declaration.id?.name ?? '<anonymous>', node: declaration })
32
+ continue
33
+ }
34
+
35
+ if (declaration?.type !== 'VariableDeclaration' || declaration.kind !== 'const') {
36
+ continue
37
+ }
38
+
39
+ for (const item of declaration.declarations) {
40
+ if (item.init?.type === 'FunctionExpression' || item.init?.type === 'ArrowFunctionExpression') {
41
+ result.push({
42
+ name: item.id.type === 'Identifier' ? item.id.name : '<anonymous>',
43
+ node: declaration,
44
+ initializer: item.init,
45
+ })
46
+ }
47
+ }
48
+ }
49
+
50
+ return result
51
+ }
52
+
53
+ export function declarationsIsObjectParam(node: ESTree.ParamPattern): boolean {
54
+ if (node.type === 'ObjectPattern') {
55
+ return true
56
+ }
57
+
58
+ if (node.type === 'TSParameterProperty') {
59
+ return declarationsIsObjectParam(node.parameter)
60
+ }
61
+
62
+ const annotation = node.typeAnnotation?.typeAnnotation
63
+ return !annotation || annotation.type === 'TSTypeReference' || annotation.type === 'TSTypeLiteral' || annotation.type === 'TSIntersectionType'
64
+ }
65
+
66
+ export function declarationsFunctionName(node: ESTree.Function | ESTree.VariableDeclarator): string {
67
+ if (node.type === 'VariableDeclarator') {
68
+ return node.id.type === 'Identifier' ? node.id.name : '<anonymous>'
69
+ }
70
+
71
+ return node.id?.name ?? '<anonymous>'
72
+ }
73
+
74
+ export function declarationsIsRuntime(node: ESTree.Node): node is DeclarationsRuntime {
75
+ return node.type === 'VariableDeclaration' || node.type === 'FunctionDeclaration' || node.type === 'ClassDeclaration' || node.type === 'TSEnumDeclaration'
76
+ }
77
+
78
+ export function declarationsRuntimeName(node: DeclarationsRuntime): string {
79
+ if (node.type === 'VariableDeclaration') {
80
+ return node.declarations.map((item) => item.id.type === 'Identifier' ? item.id.name : '<anonymous>').join(', ')
81
+ }
82
+
83
+ return node.id?.name ?? '<anonymous>'
84
+ }
85
+
86
+ export function declarationsRuntimeKind(node: DeclarationsRuntime): 'class' | 'enum' | 'function' | 'variable' {
87
+ if (node.type === 'ClassDeclaration') {
88
+ return 'class'
89
+ }
90
+
91
+ if (node.type === 'TSEnumDeclaration') {
92
+ return 'enum'
93
+ }
94
+
95
+ if (node.type === 'FunctionDeclaration') {
96
+ return 'function'
97
+ }
98
+
99
+ return 'variable'
100
+ }
101
+
102
+ function declarationsUnwrapExpression(node: ESTree.Expression): ESTree.Expression {
103
+ if (node.type === 'ParenthesizedExpression' || node.type === 'TSAsExpression' || node.type === 'TSSatisfiesExpression'
104
+ || node.type === 'TSTypeAssertion' || node.type === 'TSNonNullExpression') {
105
+ return declarationsUnwrapExpression(node.expression)
106
+ }
107
+
108
+ if (node.type === 'ChainExpression') {
109
+ return declarationsUnwrapExpression(node.expression)
110
+ }
111
+
112
+ return node.type === 'AwaitExpression' && node.argument ? declarationsUnwrapExpression(node.argument) : node
113
+ }
114
+
115
+ export function declarationsIsTrivialExpression(node: ESTree.Expression): boolean {
116
+ const value = declarationsUnwrapExpression(node)
117
+
118
+ if (value.type === 'Identifier' || value.type === 'ThisExpression' || value.type === 'MetaProperty' || value.type.endsWith('Literal')) {
119
+ return true
120
+ }
121
+
122
+ if (value.type === 'TemplateLiteral') {
123
+ return value.expressions.length === 0
124
+ }
125
+
126
+ if (value.type === 'MemberExpression' || value.type === 'CallExpression' || value.type === 'NewExpression'
127
+ || value.type === 'TaggedTemplateExpression') {
128
+ return true
129
+ }
130
+
131
+ return value.type === 'LogicalExpression'
132
+ && declarationsIsTrivialExpression(value.left)
133
+ && declarationsIsTrivialExpression(value.right)
134
+ }
135
+
136
+ export function declarationsIsTrivialFunction(node: AstRuntimeFunction): boolean {
137
+ if (node.type === 'ArrowFunctionExpression' && node.body.type !== 'BlockStatement') {
138
+ return declarationsIsTrivialExpression(node.body)
139
+ }
140
+
141
+ if (!node.body || node.body.type !== 'BlockStatement') {
142
+ return false
143
+ }
144
+
145
+ const statements = node.body.body.filter((statement) => statement.type !== 'EmptyStatement'
146
+ && !(statement.type === 'ExpressionStatement' && statement.directive))
147
+
148
+ if (statements.length === 0) {
149
+ return true
150
+ }
151
+
152
+ if (statements.length > 1) {
153
+ return false
154
+ }
155
+
156
+ const statement = statements[0]
157
+ if (statement.type === 'ReturnStatement') {
158
+ return !statement.argument || declarationsIsTrivialExpression(statement.argument)
159
+ }
160
+
161
+ return statement.type === 'ExpressionStatement' && declarationsIsTrivialExpression(statement.expression)
162
+ }
163
+
164
+ export interface DeclarationsNamed {
165
+ name: string
166
+ node: ESTree.Node
167
+ }
168
+
169
+ export function declarationsCollectNamed(root: ESTree.Node): DeclarationsNamed[] {
170
+ const result: DeclarationsNamed[] = []
171
+
172
+ astVisit(root, [], (node, ancestors) => {
173
+ if ((node.type === 'FunctionDeclaration' || node.type === 'ClassDeclaration' || node.type === 'TSTypeAliasDeclaration'
174
+ || node.type === 'TSInterfaceDeclaration' || node.type === 'TSEnumDeclaration') && node.id?.name) {
175
+ result.push({ name: node.id.name, node })
176
+ return
177
+ }
178
+
179
+ if (node.type === 'VariableDeclarator' && node.id.type === 'Identifier'
180
+ && !ancestors.some((ancestor) => ancestor.type === 'ForStatement' || ancestor.type === 'ForInStatement'
181
+ || ancestor.type === 'ForOfStatement')) {
182
+ result.push({ name: node.id.name, node })
183
+ }
184
+ })
185
+
186
+ return result
187
+ }
188
+
189
+ export function declarationsCollectFunctions(root: ESTree.Node): { name: string, node: AstRuntimeFunction }[] {
190
+ return declarationsCollectNamed(root).flatMap((item) => {
191
+ if (item.node.type === 'FunctionDeclaration') {
192
+ return [{ name: item.name, node: item.node }]
193
+ }
194
+
195
+ if (item.node.type === 'VariableDeclarator'
196
+ && (item.node.init?.type === 'FunctionExpression' || item.node.init?.type === 'ArrowFunctionExpression')) {
197
+ return [{ name: item.name, node: item.node.init }]
198
+ }
199
+
200
+ return []
201
+ })
202
+ }
203
+
@@ -0,0 +1,158 @@
1
+ import type { AstRuntimeFunction } from './ast.ts'
2
+ import type { ESTree } from '@oxlint/plugins'
3
+
4
+ export interface ExportsBinding {
5
+ exportedName: string
6
+ localName?: string
7
+ node: ESTree.Node
8
+ declaration?: ESTree.Declaration
9
+ initializer?: ESTree.Expression | null
10
+ typeOnly: boolean
11
+ default: boolean
12
+ reExport: boolean
13
+ }
14
+
15
+ export interface ExportsFunction {
16
+ name: string
17
+ node: AstRuntimeFunction
18
+ }
19
+
20
+ function moduleExportName(node: ESTree.ModuleExportName): string {
21
+ return node.type === 'Identifier' ? node.name : node.value
22
+ }
23
+
24
+ function declarationBindings(declaration: ESTree.Declaration | null): ExportsBinding[] {
25
+ if (!declaration || declaration.type === 'TSModuleDeclaration') {
26
+ return []
27
+ }
28
+
29
+ if (declaration.type === 'VariableDeclaration') {
30
+ return declaration.declarations.map((item) => ({
31
+ exportedName: item.id.type === 'Identifier' ? item.id.name : '<anonymous>',
32
+ localName: item.id.type === 'Identifier' ? item.id.name : undefined,
33
+ node: declaration,
34
+ declaration,
35
+ initializer: item.init,
36
+ typeOnly: false,
37
+ default: false,
38
+ reExport: false,
39
+ }))
40
+ }
41
+
42
+ const name = declaration.id?.name ?? '<anonymous>'
43
+ return [{
44
+ exportedName: name,
45
+ localName: declaration.id?.name,
46
+ node: declaration,
47
+ declaration,
48
+ typeOnly: declaration.type === 'TSInterfaceDeclaration'
49
+ || declaration.type === 'TSTypeAliasDeclaration'
50
+ || declaration.type === 'TSDeclareFunction',
51
+ default: false,
52
+ reExport: false,
53
+ }]
54
+ }
55
+
56
+ function localBindings(program: ESTree.Program): Map<string, ExportsBinding> {
57
+ const result = new Map<string, ExportsBinding>()
58
+
59
+ for (const statement of program.body) {
60
+ if (statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration') {
61
+ continue
62
+ }
63
+
64
+ if (statement.type.endsWith('Declaration')) {
65
+ for (const binding of declarationBindings(statement as ESTree.Declaration)) {
66
+ if (binding.localName) {
67
+ result.set(binding.localName, binding)
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ return result
74
+ }
75
+
76
+ export function exportsCollect(program: ESTree.Program): ExportsBinding[] {
77
+ const locals = localBindings(program)
78
+
79
+ return program.body.flatMap((statement) => {
80
+ if (statement.type === 'ExportAllDeclaration') {
81
+ return [{
82
+ exportedName: '*',
83
+ node: statement,
84
+ typeOnly: statement.exportKind === 'type',
85
+ default: false,
86
+ reExport: true,
87
+ }]
88
+ }
89
+
90
+ if (statement.type === 'ExportDefaultDeclaration') {
91
+ if (statement.declaration.type === 'Identifier') {
92
+ const local = locals.get(statement.declaration.name)
93
+ return [{
94
+ ...(local ?? {
95
+ exportedName: 'default',
96
+ node: statement,
97
+ typeOnly: false,
98
+ default: true,
99
+ reExport: false,
100
+ }),
101
+ exportedName: 'default',
102
+ localName: local?.localName ?? statement.declaration.name,
103
+ default: true,
104
+ }]
105
+ }
106
+
107
+ if (statement.declaration.type.endsWith('Declaration')) {
108
+ const bindings = declarationBindings(statement.declaration as ESTree.Declaration)
109
+ return bindings.length > 0
110
+ ? bindings.map((binding) => ({ ...binding, exportedName: 'default', default: true }))
111
+ : [{ exportedName: 'default', node: statement, typeOnly: false, default: true, reExport: false }]
112
+ }
113
+
114
+ return [{ exportedName: 'default', node: statement, typeOnly: false, default: true, reExport: false }]
115
+ }
116
+
117
+ if (statement.type !== 'ExportNamedDeclaration') {
118
+ return []
119
+ }
120
+
121
+ return [
122
+ ...declarationBindings(statement.declaration),
123
+ ...statement.specifiers.map((specifier) => {
124
+ const localName = moduleExportName(specifier.local)
125
+ const local = locals.get(localName)
126
+ return {
127
+ ...(local ?? {
128
+ exportedName: moduleExportName(specifier.exported),
129
+ node: specifier,
130
+ typeOnly: false,
131
+ default: false,
132
+ reExport: Boolean(statement.source),
133
+ }),
134
+ exportedName: moduleExportName(specifier.exported),
135
+ localName,
136
+ node: local?.node ?? specifier,
137
+ typeOnly: specifier.exportKind === 'type' || statement.exportKind === 'type' || Boolean(local?.typeOnly),
138
+ default: moduleExportName(specifier.exported) === 'default',
139
+ reExport: Boolean(statement.source),
140
+ }
141
+ }),
142
+ ]
143
+ })
144
+ }
145
+
146
+ export function exportsCollectFunctions(program: ESTree.Program): ExportsFunction[] {
147
+ return exportsCollect(program).flatMap((binding) => {
148
+ const name = binding.localName ?? binding.exportedName
149
+
150
+ if (binding.declaration?.type === 'FunctionDeclaration') {
151
+ return [{ name, node: binding.declaration }]
152
+ }
153
+
154
+ return binding.initializer?.type === 'FunctionExpression' || binding.initializer?.type === 'ArrowFunctionExpression'
155
+ ? [{ name, node: binding.initializer }]
156
+ : []
157
+ })
158
+ }
@@ -0,0 +1,67 @@
1
+ import { astNearestFunction, astVisit } from './ast.ts'
2
+ import { namingPascalCase } from './naming.ts'
3
+ import type { ESTree } from '@oxlint/plugins'
4
+
5
+ export interface FactoriesFunction {
6
+ name: string
7
+ node: ESTree.Function | ESTree.ArrowFunctionExpression
8
+ }
9
+
10
+ export function factoriesExpandTemplate(template: string, groups: Record<string, string>): string {
11
+ return template.replaceAll(/\{([^}]+)\}/g, (placeholder, key: string) => {
12
+ const group = groups[key.toLowerCase()]
13
+
14
+ if (!group) {
15
+ return placeholder
16
+ }
17
+
18
+ return /^[A-Z]/.test(key) ? namingPascalCase(group) : group
19
+ })
20
+ }
21
+
22
+ export function factoriesCollectTopLevel(program: ESTree.Program, pattern: RegExp): FactoriesFunction[] {
23
+ return program.body.flatMap((statement) => {
24
+ const declaration = statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration' ? statement.declaration : statement
25
+
26
+ if (declaration?.type === 'FunctionDeclaration') {
27
+ const name = declaration.id?.name ?? '<anonymous>'
28
+ return pattern.test(name) ? [{ name, node: declaration }] : []
29
+ }
30
+
31
+ if (declaration?.type !== 'VariableDeclaration') {
32
+ return []
33
+ }
34
+
35
+ return declaration.declarations.flatMap((item) => item.id.type === 'Identifier' && pattern.test(item.id.name)
36
+ && (item.init?.type === 'FunctionExpression' || item.init?.type === 'ArrowFunctionExpression')
37
+ ? [{ name: item.id.name, node: item.init }]
38
+ : [])
39
+ })
40
+ }
41
+
42
+ export function factoriesDirectlyReturnedObjects(factory: FactoriesFunction): ESTree.ObjectExpression[] {
43
+ if (factory.node.type === 'ArrowFunctionExpression' && factory.node.body.type === 'ObjectExpression') {
44
+ return [factory.node.body]
45
+ }
46
+
47
+ const result: ESTree.ObjectExpression[] = []
48
+ astVisit(factory.node.body, [factory.node], (node, ancestors) => {
49
+ if (node.type === 'ReturnStatement' && node.argument?.type === 'ObjectExpression' && astNearestFunction(ancestors) === factory.node) {
50
+ result.push(node.argument)
51
+ }
52
+ })
53
+
54
+ return result
55
+ }
56
+
57
+ export function factoriesObjectPropertyName(property: ESTree.ObjectPropertyKind): string | undefined {
58
+ if (property.type === 'SpreadElement' || property.computed) {
59
+ return
60
+ }
61
+
62
+ if (property.key.type === 'Identifier') {
63
+ return property.key.name
64
+ }
65
+
66
+ return property.key.type === 'Literal' && typeof property.key.value === 'string' ? property.key.value : undefined
67
+ }
@@ -0,0 +1,11 @@
1
+ export * from './ast.ts'
2
+ export * from './declarations.ts'
3
+ export * from './exports.ts'
4
+ export * from './factories.ts'
5
+ export * from './naming.ts'
6
+ export * from './options.ts'
7
+ export * from './orpc.ts'
8
+ export * from './react-components.ts'
9
+ export * from './routes.ts'
10
+ export * from './sanitizers.ts'
11
+ export * from './schemas.ts'
@@ -0,0 +1,89 @@
1
+ export function namingPosixPath(filePath: string): string {
2
+ return filePath.replaceAll('\\', '/')
3
+ }
4
+
5
+ export function namingFileBasename(filePath: string): string {
6
+ const normalized = namingPosixPath(filePath)
7
+ const slash = normalized.lastIndexOf('/')
8
+ return slash === -1 ? normalized : normalized.slice(slash + 1)
9
+ }
10
+
11
+ export function namingDirSegments(filePath: string): string[] {
12
+ const parts = namingPosixPath(filePath).split('/').filter(Boolean)
13
+ return parts.slice(0, -1)
14
+ }
15
+
16
+ export function namingSegmentsAfter(filePath: string, after: string): string[] | undefined {
17
+ const dirs = namingDirSegments(filePath)
18
+ const marker = after.split('/').filter(Boolean)
19
+
20
+ if (marker.length === 0) {
21
+ return undefined
22
+ }
23
+
24
+ for (let index = dirs.length - marker.length; index >= 0; index -= 1) {
25
+ if (marker.every((part, offset) => dirs[index + offset] === part)) {
26
+ return dirs.slice(index + marker.length)
27
+ }
28
+ }
29
+ }
30
+
31
+ export function namingFolderPrefixes(
32
+ folders: string[],
33
+ separator: string,
34
+ singularize: 'none' | 'trailing-s',
35
+ ): string[] {
36
+ const joined = folders.join(separator)
37
+ const last = folders.at(-1) ?? ''
38
+ const prefixes = [joined]
39
+
40
+ if (singularize === 'trailing-s' && last.endsWith('s') && last.length > 1) {
41
+ prefixes.push([...folders.slice(0, -1), last.slice(0, -1)].join(separator))
42
+ }
43
+
44
+ return prefixes.filter(Boolean)
45
+ }
46
+
47
+ export function namingPascalCase(value: string): string {
48
+ return value
49
+ .split(/[-_\s.]+/)
50
+ .filter(Boolean)
51
+ .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
52
+ .join('')
53
+ }
54
+
55
+ export function namingCamelCase(value: string): string {
56
+ const next = namingPascalCase(value)
57
+ return `${next.charAt(0).toLowerCase()}${next.slice(1)}`
58
+ }
59
+
60
+ export function namingMatchTemplate(
61
+ basename: string,
62
+ template: string,
63
+ placeholderPattern = '[a-z0-9-]+',
64
+ ): Record<string, string> | null {
65
+ let pattern = ''
66
+ let index = 0
67
+
68
+ while (index < template.length) {
69
+ const char = template[index]
70
+
71
+ if (char === '{') {
72
+ const end = template.indexOf('}', index)
73
+
74
+ if (end === -1) {
75
+ return null
76
+ }
77
+
78
+ const key = template.slice(index + 1, end)
79
+ pattern += `(?<${key}>${placeholderPattern})`
80
+ index = end + 1
81
+ continue
82
+ }
83
+
84
+ pattern += /[.+^${}()|[\]\\]/.test(char) ? `\\${char}` : char
85
+ index += 1
86
+ }
87
+
88
+ return new RegExp(`^${pattern}$`).exec(basename)?.groups ?? null
89
+ }
@@ -0,0 +1,5 @@
1
+ import type { Context } from '@oxlint/plugins'
2
+
3
+ export function optionsFirst<T>(context: Context, fallback?: T): T {
4
+ return (context.options[0] ?? fallback) as unknown as T
5
+ }