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,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
|
+
}
|