@diia-inhouse/oxc-config 1.9.2

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.
@@ -0,0 +1,169 @@
1
+ function isTestFile(filename) {
2
+ return filename.includes('/tests/') || filename.includes('/test/')
3
+ }
4
+
5
+ function isWorkflowFile(filename) {
6
+ return !isTestFile(filename) && filename.includes('/worker/workflows/') && !filename.includes('.types.')
7
+ }
8
+
9
+ function isActivityFile(filename) {
10
+ return !isTestFile(filename) && filename.includes('/worker/activities/')
11
+ }
12
+
13
+ const FORBIDDEN_NODE_MODULES = new Set([
14
+ 'node:path',
15
+ 'path',
16
+ 'node:fs',
17
+ 'fs',
18
+ 'node:fs/promises',
19
+ 'fs/promises',
20
+ 'node:crypto',
21
+ 'crypto',
22
+ 'node:child_process',
23
+ 'child_process',
24
+ 'node:os',
25
+ 'os',
26
+ 'node:net',
27
+ 'net',
28
+ 'node:dns',
29
+ 'dns',
30
+ 'node:http',
31
+ 'http',
32
+ 'node:https',
33
+ 'https',
34
+ ])
35
+
36
+ export default {
37
+ meta: { name: '@diia-inhouse/oxlint-plugin-temporal' },
38
+ rules: {
39
+ 'workflow-single-param': {
40
+ meta: {
41
+ type: 'problem',
42
+ messages: {
43
+ tooMany:
44
+ 'Workflow function "{{ name }}" has {{ count }} parameters. Workflows must accept a single object parameter for serialization compatibility.',
45
+ },
46
+ },
47
+ create(context) {
48
+ if (!isWorkflowFile(context.filename)) {
49
+ return {}
50
+ }
51
+
52
+ function checkFunction(node, name) {
53
+ if (node.params.length > 1) {
54
+ context.report({
55
+ node,
56
+ messageId: 'tooMany',
57
+ data: { name: name || '<anonymous>', count: String(node.params.length) },
58
+ })
59
+ }
60
+ }
61
+
62
+ return {
63
+ 'ExportNamedDeclaration > FunctionDeclaration'(node) {
64
+ checkFunction(node, node.id?.name)
65
+ },
66
+ 'ExportNamedDeclaration > VariableDeclaration > VariableDeclarator'(node) {
67
+ if (node.init && (node.init.type === 'ArrowFunctionExpression' || node.init.type === 'FunctionExpression')) {
68
+ checkFunction(node.init, node.id?.name)
69
+ }
70
+ },
71
+ }
72
+ },
73
+ },
74
+
75
+ 'async-activity': {
76
+ meta: {
77
+ type: 'problem',
78
+ messages: {
79
+ notAsync: 'Activity method "{{ name }}" must be async. All Temporal activities must be async functions.',
80
+ },
81
+ },
82
+ create(context) {
83
+ if (!isActivityFile(context.filename)) {
84
+ return {}
85
+ }
86
+
87
+ return {
88
+ MethodDefinition(node) {
89
+ if (node.kind === 'constructor' || node.key.type !== 'Identifier' || node.accessibility === 'private') {
90
+ return
91
+ }
92
+
93
+ const fn = node.value
94
+
95
+ if (fn && !fn.async) {
96
+ context.report({
97
+ node: node.key,
98
+ messageId: 'notAsync',
99
+ data: { name: node.key.name },
100
+ })
101
+ }
102
+ },
103
+ }
104
+ },
105
+ },
106
+
107
+ 'no-node-imports': {
108
+ meta: {
109
+ type: 'problem',
110
+ messages: {
111
+ forbidden:
112
+ 'Importing "{{ source }}" in a workflow file will break the Temporal bundle. Workflows run in a sandboxed VM — Node.js built-in modules are not available.',
113
+ },
114
+ },
115
+ create(context) {
116
+ if (!isWorkflowFile(context.filename)) {
117
+ return {}
118
+ }
119
+
120
+ return {
121
+ ImportDeclaration(node) {
122
+ const source = node.source.value
123
+
124
+ if (FORBIDDEN_NODE_MODULES.has(source)) {
125
+ context.report({
126
+ node: node.source,
127
+ messageId: 'forbidden',
128
+ data: { source },
129
+ })
130
+ }
131
+ },
132
+ }
133
+ },
134
+ },
135
+
136
+ 'no-path-alias-imports': {
137
+ meta: {
138
+ type: 'problem',
139
+ messages: {
140
+ forbidden:
141
+ 'Path alias "{{ source }}" cannot be used in workflow files. The Temporal bundler does not resolve tsconfig path aliases. Use relative imports or import only from type-only workflow packages.',
142
+ },
143
+ },
144
+ create(context) {
145
+ if (!isWorkflowFile(context.filename)) {
146
+ return {}
147
+ }
148
+
149
+ return {
150
+ ImportDeclaration(node) {
151
+ if (node.importKind === 'type') {
152
+ return
153
+ }
154
+
155
+ const source = node.source.value
156
+
157
+ if (source.startsWith('@') && !source.startsWith('@diia-inhouse/') && !source.startsWith('@temporalio/')) {
158
+ context.report({
159
+ node: node.source,
160
+ messageId: 'forbidden',
161
+ data: { source },
162
+ })
163
+ }
164
+ },
165
+ }
166
+ },
167
+ },
168
+ },
169
+ }
@@ -0,0 +1,67 @@
1
+ const PERSISTENT_MOCK_METHODS = new Set(['mockResolvedValue', 'mockReturnValue', 'mockRejectedValue', 'mockImplementation'])
2
+
3
+ function getOnceVariant(name) {
4
+ return `${name}Once`
5
+ }
6
+
7
+ export default {
8
+ meta: { name: '@diia-inhouse/oxlint-plugin-test' },
9
+ rules: {
10
+ 'no-persistent-mock': {
11
+ meta: {
12
+ type: 'problem',
13
+ messages: {
14
+ forbidden:
15
+ 'Use {{ once }}() instead of {{ method }}(). Persistent mocks hide test isolation issues and mask unexpected call counts.',
16
+ },
17
+ },
18
+ create(context) {
19
+ return {
20
+ CallExpression(node) {
21
+ if (
22
+ node.callee.type === 'MemberExpression' &&
23
+ node.callee.property.type === 'Identifier' &&
24
+ PERSISTENT_MOCK_METHODS.has(node.callee.property.name)
25
+ ) {
26
+ const method = node.callee.property.name
27
+
28
+ context.report({
29
+ node: node.callee.property,
30
+ messageId: 'forbidden',
31
+ data: { method, once: getOnceVariant(method) },
32
+ })
33
+ }
34
+ },
35
+ }
36
+ },
37
+ },
38
+
39
+ 'no-vi-mock-in-workflows': {
40
+ meta: {
41
+ type: 'problem',
42
+ messages: {
43
+ forbidden: 'vi.mock() is forbidden in workflow tests. Use mockActivities() from Temporal test utilities instead.',
44
+ },
45
+ },
46
+ create(context) {
47
+ if (!context.filename.includes('/worker/')) {
48
+ return {}
49
+ }
50
+
51
+ return {
52
+ CallExpression(node) {
53
+ if (
54
+ node.callee.type === 'MemberExpression' &&
55
+ node.callee.object.type === 'Identifier' &&
56
+ node.callee.object.name === 'vi' &&
57
+ node.callee.property.type === 'Identifier' &&
58
+ node.callee.property.name === 'mock'
59
+ ) {
60
+ context.report({ node, messageId: 'forbidden' })
61
+ }
62
+ },
63
+ }
64
+ },
65
+ },
66
+ },
67
+ }
@@ -0,0 +1,108 @@
1
+ export default {
2
+ meta: { name: '@diia-inhouse/oxlint-plugin-class' },
3
+ rules: {
4
+ 'no-module-level-const': {
5
+ meta: {
6
+ type: 'suggestion',
7
+ messages: {
8
+ forbidden:
9
+ 'Module-level "{{ kind }} {{ name }}" should be a private class property. Only types, interfaces, and re-exports belong at module level alongside a class.',
10
+ },
11
+ },
12
+ create(context) {
13
+ return {
14
+ 'Program:exit'(programNode) {
15
+ const hasDefaultClassExport = programNode.body.some(
16
+ (stmt) =>
17
+ stmt.type === 'ExportDefaultDeclaration' &&
18
+ stmt.declaration &&
19
+ stmt.declaration.type === 'ClassDeclaration',
20
+ )
21
+
22
+ if (!hasDefaultClassExport) {
23
+ return
24
+ }
25
+
26
+ for (const stmt of programNode.body) {
27
+ if (stmt.type !== 'VariableDeclaration') {
28
+ continue
29
+ }
30
+
31
+ for (const decl of stmt.declarations) {
32
+ const name = decl.id.type === 'Identifier' ? decl.id.name : '<destructured>'
33
+
34
+ context.report({
35
+ node: decl,
36
+ messageId: 'forbidden',
37
+ data: { kind: stmt.kind, name },
38
+ })
39
+ }
40
+ }
41
+ },
42
+ }
43
+ },
44
+ },
45
+
46
+ 'no-interface-in-implementation': {
47
+ meta: {
48
+ type: 'problem',
49
+ messages: {
50
+ forbidden:
51
+ '{{ kind }} "{{ name }}" must be in a separate file. Types and interfaces must not share a file with class implementation.',
52
+ },
53
+ },
54
+ create(context) {
55
+ return {
56
+ 'Program:exit'(programNode) {
57
+ const hasDefaultClassExport = programNode.body.some(
58
+ (stmt) =>
59
+ stmt.type === 'ExportDefaultDeclaration' &&
60
+ stmt.declaration &&
61
+ stmt.declaration.type === 'ClassDeclaration',
62
+ )
63
+
64
+ if (!hasDefaultClassExport) {
65
+ return
66
+ }
67
+
68
+ for (const stmt of programNode.body) {
69
+ if (stmt.type === 'TSInterfaceDeclaration') {
70
+ context.report({
71
+ node: stmt,
72
+ messageId: 'forbidden',
73
+ data: { kind: 'interface', name: stmt.id.name },
74
+ })
75
+ }
76
+
77
+ if (stmt.type === 'TSTypeAliasDeclaration') {
78
+ context.report({
79
+ node: stmt,
80
+ messageId: 'forbidden',
81
+ data: { kind: 'type', name: stmt.id.name },
82
+ })
83
+ }
84
+
85
+ if (stmt.type === 'ExportNamedDeclaration' && stmt.declaration) {
86
+ if (stmt.declaration.type === 'TSInterfaceDeclaration') {
87
+ context.report({
88
+ node: stmt.declaration,
89
+ messageId: 'forbidden',
90
+ data: { kind: 'interface', name: stmt.declaration.id.name },
91
+ })
92
+ }
93
+
94
+ if (stmt.declaration.type === 'TSTypeAliasDeclaration') {
95
+ context.report({
96
+ node: stmt.declaration,
97
+ messageId: 'forbidden',
98
+ data: { kind: 'type', name: stmt.declaration.id.name },
99
+ })
100
+ }
101
+ }
102
+ }
103
+ },
104
+ }
105
+ },
106
+ },
107
+ },
108
+ }
@@ -0,0 +1,278 @@
1
+ function hasInlineObjectReturnType(node) {
2
+ return node.returnType && node.returnType.type === 'TSTypeAnnotation' && node.returnType.typeAnnotation.type === 'TSTypeLiteral'
3
+ }
4
+
5
+ function normalize(str) {
6
+ return str.replace(/[_-]/g, '').toLowerCase()
7
+ }
8
+
9
+ function isEnumLikeObject(node) {
10
+ if (node.type !== 'ObjectExpression' || node.properties.length === 0) {
11
+ return false
12
+ }
13
+
14
+ return node.properties.every((prop) => {
15
+ if (prop.type !== 'Property' || prop.computed) {
16
+ return false
17
+ }
18
+
19
+ const key = prop.key.type === 'Identifier' ? prop.key.name : prop.key.type === 'Literal' ? String(prop.key.value) : null
20
+
21
+ if (!key) {
22
+ return false
23
+ }
24
+
25
+ if (prop.value.type !== 'Literal' || typeof prop.value.value !== 'string') {
26
+ return false
27
+ }
28
+
29
+ return normalize(key) === normalize(prop.value.value)
30
+ })
31
+ }
32
+
33
+ function isTopLevel(node) {
34
+ const parent = node.parent
35
+
36
+ return parent.type === 'Program' || (parent.type === 'ExportNamedDeclaration' && parent.parent.type === 'Program')
37
+ }
38
+
39
+ function getEnumObject(init) {
40
+ if (!init) {
41
+ return null
42
+ }
43
+
44
+ let expr = init
45
+
46
+ if (expr.type === 'TSSatisfiesExpression') {
47
+ expr = expr.expression
48
+ }
49
+
50
+ if (expr.type === 'TSAsExpression') {
51
+ expr = expr.expression
52
+ }
53
+
54
+ return isEnumLikeObject(expr) ? expr : null
55
+ }
56
+
57
+ function capitalizeFirst(str) {
58
+ return str.charAt(0).toUpperCase() + str.slice(1)
59
+ }
60
+
61
+ function isValidPlural(rawSingular, rawPlural) {
62
+ const singular = capitalizeFirst(rawSingular)
63
+ const plural = capitalizeFirst(rawPlural)
64
+
65
+ if (plural === singular + 's') {
66
+ return true
67
+ }
68
+
69
+ if (plural === singular + 'es' && /(?:s|x|z|sh|ch)$/.test(singular)) {
70
+ return true
71
+ }
72
+
73
+ if (singular.endsWith('y') && plural === singular.slice(0, -1) + 'ies') {
74
+ return true
75
+ }
76
+
77
+ return false
78
+ }
79
+
80
+ export default {
81
+ meta: { name: '@diia-inhouse/oxlint-plugin-code' },
82
+ rules: {
83
+ 'no-inline-object-return-type': {
84
+ meta: {
85
+ type: 'problem',
86
+ messages: {
87
+ forbidden:
88
+ 'Do not use inline object return types. Declare a named interface or type alias (e.g. `*Response`, `*Result`) instead.',
89
+ },
90
+ },
91
+ create(context) {
92
+ function check(node) {
93
+ if (hasInlineObjectReturnType(node)) {
94
+ context.report({ node: node.returnType, messageId: 'forbidden' })
95
+ }
96
+ }
97
+
98
+ return {
99
+ FunctionDeclaration(node) {
100
+ check(node)
101
+ },
102
+ FunctionExpression(node) {
103
+ if (node.parent && node.parent.type === 'MethodDefinition') {
104
+ return
105
+ }
106
+
107
+ check(node)
108
+ },
109
+ ArrowFunctionExpression(node) {
110
+ check(node)
111
+ },
112
+ MethodDefinition(node) {
113
+ if (node.value) {
114
+ check(node.value)
115
+ }
116
+ },
117
+ }
118
+ },
119
+ },
120
+
121
+ 'no-promise-settimeout': {
122
+ meta: {
123
+ type: 'problem',
124
+ messages: {
125
+ forbidden: 'Do not wrap setTimeout in a Promise. Use `import { setTimeout } from "node:timers/promises"` instead.',
126
+ },
127
+ },
128
+ create(context) {
129
+ return {
130
+ NewExpression(node) {
131
+ if (node.callee.type !== 'Identifier' || node.callee.name !== 'Promise') {
132
+ return
133
+ }
134
+
135
+ const callback = node.arguments[0]
136
+
137
+ if (!callback || (callback.type !== 'ArrowFunctionExpression' && callback.type !== 'FunctionExpression')) {
138
+ return
139
+ }
140
+
141
+ const body = callback.body
142
+
143
+ const exprToCheck =
144
+ body.type === 'CallExpression'
145
+ ? body
146
+ : body.type === 'BlockStatement' && body.body.length === 1 && body.body[0].type === 'ExpressionStatement'
147
+ ? body.body[0].expression
148
+ : null
149
+
150
+ if (
151
+ exprToCheck &&
152
+ exprToCheck.type === 'CallExpression' &&
153
+ exprToCheck.callee.type === 'Identifier' &&
154
+ exprToCheck.callee.name === 'setTimeout'
155
+ ) {
156
+ context.report({ node, messageId: 'forbidden' })
157
+ }
158
+ },
159
+ }
160
+ },
161
+ },
162
+
163
+ 'const-enum-naming': {
164
+ meta: {
165
+ type: 'suggestion',
166
+ messages: {
167
+ notPascalCase: "Enum-like const object '{{name}}' should use PascalCase naming.",
168
+ notPlural: "Enum-like const object '{{name}}' should have a plural name (e.g., '{{name}}s').",
169
+ },
170
+ },
171
+ create(context) {
172
+ return {
173
+ VariableDeclaration(node) {
174
+ if (node.kind !== 'const' || !isTopLevel(node)) {
175
+ return
176
+ }
177
+
178
+ for (const declarator of node.declarations) {
179
+ if (!declarator.id || declarator.id.type !== 'Identifier') {
180
+ continue
181
+ }
182
+
183
+ if (!getEnumObject(declarator.init)) {
184
+ continue
185
+ }
186
+
187
+ const name = declarator.id.name
188
+
189
+ if (!/^[A-Z]/.test(name)) {
190
+ context.report({ node: declarator.id, messageId: 'notPascalCase', data: { name } })
191
+ }
192
+
193
+ if (!name.endsWith('s')) {
194
+ context.report({ node: declarator.id, messageId: 'notPlural', data: { name } })
195
+ }
196
+ }
197
+ },
198
+ }
199
+ },
200
+ },
201
+
202
+ 'const-enum-type-name': {
203
+ meta: {
204
+ type: 'suggestion',
205
+ messages: {
206
+ notSingular: "Type '{{typeName}}' derived from '{{objectName}}' should be the singular form of the object name.",
207
+ },
208
+ },
209
+ create(context) {
210
+ return {
211
+ TSTypeAliasDeclaration(node) {
212
+ const typeAnnotation = node.typeAnnotation
213
+
214
+ if (typeAnnotation.type !== 'TSIndexedAccessType') {
215
+ return
216
+ }
217
+
218
+ const objectType = typeAnnotation.objectType
219
+
220
+ if (objectType.type !== 'TSTypeQuery' || !objectType.exprName || objectType.exprName.type !== 'Identifier') {
221
+ return
222
+ }
223
+
224
+ const objectName = objectType.exprName.name
225
+ const indexType = typeAnnotation.indexType
226
+
227
+ if (indexType.type !== 'TSTypeOperator' || indexType.operator !== 'keyof') {
228
+ return
229
+ }
230
+
231
+ if (
232
+ !indexType.typeAnnotation ||
233
+ indexType.typeAnnotation.type !== 'TSTypeQuery' ||
234
+ !indexType.typeAnnotation.exprName ||
235
+ indexType.typeAnnotation.exprName.type !== 'Identifier'
236
+ ) {
237
+ return
238
+ }
239
+
240
+ if (indexType.typeAnnotation.exprName.name !== objectName) {
241
+ return
242
+ }
243
+
244
+ const typeName = node.id.name
245
+
246
+ if (!isValidPlural(typeName, objectName)) {
247
+ context.report({ node: node.id, messageId: 'notSingular', data: { typeName, objectName } })
248
+ }
249
+ },
250
+ }
251
+ },
252
+ },
253
+
254
+ 'prefer-as-const-object': {
255
+ meta: {
256
+ type: 'suggestion',
257
+ messages: {
258
+ missingAsConst: 'Enum-like objects where values match keys should use `as const` to preserve literal types.',
259
+ },
260
+ },
261
+ create(context) {
262
+ return {
263
+ VariableDeclaration(node) {
264
+ if (node.kind !== 'const' || !isTopLevel(node)) {
265
+ return
266
+ }
267
+
268
+ for (const declarator of node.declarations) {
269
+ if (declarator.init && isEnumLikeObject(declarator.init)) {
270
+ context.report({ node: declarator.init, messageId: 'missingAsConst' })
271
+ }
272
+ }
273
+ },
274
+ }
275
+ },
276
+ },
277
+ },
278
+ }