@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,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
+ }
@@ -0,0 +1,183 @@
1
+ function isSchemaConstructor(callee) {
2
+ return (
3
+ (callee.type === 'Identifier' && callee.name === 'Schema') ||
4
+ (callee.type === 'MemberExpression' &&
5
+ callee.object.type === 'Identifier' &&
6
+ callee.object.name === 'mongoose' &&
7
+ callee.property.type === 'Identifier' &&
8
+ callee.property.name === 'Schema')
9
+ )
10
+ }
11
+
12
+ function getOptionValue(optionsNode, key) {
13
+ if (!optionsNode || optionsNode.type !== 'ObjectExpression') {
14
+ return undefined
15
+ }
16
+
17
+ const prop = optionsNode.properties.find((p) => p.type === 'Property' && p.key.type === 'Identifier' && p.key.name === key)
18
+
19
+ if (!prop) {
20
+ return undefined
21
+ }
22
+
23
+ return prop.value.type === 'Literal' ? prop.value.value : undefined
24
+ }
25
+
26
+ function isModelFile(filename) {
27
+ return filename.includes('/models/')
28
+ }
29
+
30
+ export default {
31
+ meta: { name: '@diia-inhouse/oxlint-plugin-mongoose' },
32
+ rules: {
33
+ 'schema-timestamps': {
34
+ meta: {
35
+ type: 'problem',
36
+ messages: {
37
+ missing: 'Mongoose schema is missing { timestamps: true } in options.',
38
+ },
39
+ },
40
+ create(context) {
41
+ if (!isModelFile(context.filename)) {
42
+ return {}
43
+ }
44
+
45
+ const subSchemaVars = new Set()
46
+
47
+ return {
48
+ 'VariableDeclarator[init.type="NewExpression"]'(node) {
49
+ if (!isSchemaConstructor(node.init.callee)) {
50
+ return
51
+ }
52
+
53
+ const options = node.init.arguments[1]
54
+
55
+ if (getOptionValue(options, '_id') === false) {
56
+ subSchemaVars.add(node.id.name)
57
+ }
58
+ },
59
+ 'ExportDefaultDeclaration, ExportNamedDeclaration'() {
60
+ // Reset tracking per export boundary — not needed
61
+ },
62
+ 'Program:exit'(programNode) {
63
+ for (const stmt of programNode.body) {
64
+ if (stmt.type !== 'VariableDeclaration') {
65
+ continue
66
+ }
67
+
68
+ for (const decl of stmt.declarations) {
69
+ if (!decl.init || decl.init.type !== 'NewExpression' || !isSchemaConstructor(decl.init.callee)) {
70
+ continue
71
+ }
72
+
73
+ if (subSchemaVars.has(decl.id.name)) {
74
+ continue
75
+ }
76
+
77
+ const options = decl.init.arguments[1]
78
+
79
+ if (getOptionValue(options, 'timestamps') !== true) {
80
+ context.report({ node: decl, messageId: 'missing' })
81
+ }
82
+ }
83
+ }
84
+ },
85
+ }
86
+ },
87
+ },
88
+
89
+ 'sub-schema-id-false': {
90
+ meta: {
91
+ type: 'problem',
92
+ messages: {
93
+ missing: 'Sub-schema is missing { _id: false } in options. Embedded documents should not generate _id.',
94
+ },
95
+ },
96
+ create(context) {
97
+ if (!isModelFile(context.filename)) {
98
+ return {}
99
+ }
100
+
101
+ const schemaVars = new Map()
102
+
103
+ return {
104
+ VariableDeclarator(node) {
105
+ if (!node.init || node.init.type !== 'NewExpression' || !isSchemaConstructor(node.init.callee)) {
106
+ return
107
+ }
108
+
109
+ schemaVars.set(node.id.name, {
110
+ node: node.init,
111
+ hasIdFalse: getOptionValue(node.init.arguments[1], '_id') === false,
112
+ hasTimestamps: getOptionValue(node.init.arguments[1], 'timestamps') === true,
113
+ usedAsSubSchema: false,
114
+ })
115
+ },
116
+ Property(node) {
117
+ if (node.key.type !== 'Identifier' || node.key.name !== 'type' || node.value.type !== 'ArrayExpression') {
118
+ return
119
+ }
120
+
121
+ for (const el of node.value.elements) {
122
+ if (el && el.type === 'Identifier' && schemaVars.has(el.name)) {
123
+ schemaVars.get(el.name).usedAsSubSchema = true
124
+ }
125
+ }
126
+ },
127
+ 'Program:exit'() {
128
+ for (const [, info] of schemaVars) {
129
+ if (info.usedAsSubSchema && !info.hasIdFalse) {
130
+ context.report({ node: info.node, messageId: 'missing' })
131
+ }
132
+ }
133
+ },
134
+ }
135
+ },
136
+ },
137
+
138
+ 'status-requires-history': {
139
+ meta: {
140
+ type: 'problem',
141
+ messages: {
142
+ missing:
143
+ 'Schema has "status" field but no "statusHistory". Every model with status MUST have statusHistory — missing it is a data corruption bug.',
144
+ },
145
+ },
146
+ create(context) {
147
+ if (!isModelFile(context.filename)) {
148
+ return {}
149
+ }
150
+
151
+ return {
152
+ NewExpression(node) {
153
+ if (!isSchemaConstructor(node.callee)) {
154
+ return
155
+ }
156
+
157
+ const options = node.arguments[1]
158
+
159
+ if (getOptionValue(options, '_id') === false) {
160
+ return
161
+ }
162
+
163
+ const fields = node.arguments[0]
164
+
165
+ if (!fields || fields.type !== 'ObjectExpression') {
166
+ return
167
+ }
168
+
169
+ const fieldNames = new Set(
170
+ fields.properties.filter((p) => p.type === 'Property' && p.key.type === 'Identifier').map((p) => p.key.name),
171
+ )
172
+
173
+ const hasHistory = [...fieldNames].some((name) => name.toLowerCase().includes('statushistor'))
174
+
175
+ if (fieldNames.has('status') && !hasHistory) {
176
+ context.report({ node, messageId: 'missing' })
177
+ }
178
+ },
179
+ }
180
+ },
181
+ },
182
+ },
183
+ }
@@ -0,0 +1,43 @@
1
+ const CYRILLIC_PATTERN = /[\u0400-\u04FF\u0500-\u052F]/
2
+
3
+ const EXCLUDED_PATHS = ['/locales/', '.spec.ts', '.test.ts', '/tests/']
4
+
5
+ function isExcludedFile(filename) {
6
+ return EXCLUDED_PATHS.some((path) => filename.includes(path))
7
+ }
8
+
9
+ function checkStringNode(context, node) {
10
+ if (isExcludedFile(context.filename)) {
11
+ return
12
+ }
13
+
14
+ const value = node.type === 'TemplateLiteral' ? node.quasis.map((q) => q.value.raw).join('') : node.value
15
+
16
+ if (typeof value === 'string' && CYRILLIC_PATTERN.test(value)) {
17
+ context.report({ node, messageId: 'forbidden', data: { text: value.length > 50 ? value.slice(0, 50) + '...' : value } })
18
+ }
19
+ }
20
+
21
+ export default {
22
+ meta: { name: '@diia-inhouse/oxlint-plugin-locale' },
23
+ rules: {
24
+ 'no-hardcoded-cyrillic': {
25
+ meta: {
26
+ type: 'problem',
27
+ messages: {
28
+ forbidden: 'Hardcoded Cyrillic string "{{ text }}" found. Move user-facing text to locale files and use i18n service.',
29
+ },
30
+ },
31
+ create(context) {
32
+ return {
33
+ Literal(node) {
34
+ checkStringNode(context, node)
35
+ },
36
+ TemplateLiteral(node) {
37
+ checkStringNode(context, node)
38
+ },
39
+ }
40
+ },
41
+ },
42
+ },
43
+ }
@@ -0,0 +1,84 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+
4
+ let pkg = null
5
+ let loaded = false
6
+
7
+ function loadPackageJson() {
8
+ if (loaded) {
9
+ return pkg
10
+ }
11
+
12
+ loaded = true
13
+
14
+ try {
15
+ pkg = JSON.parse(readFileSync(resolve('package.json'), 'utf8'))
16
+ } catch {
17
+ // package.json not found or not parseable
18
+ }
19
+
20
+ return pkg
21
+ }
22
+
23
+ const VERSION_RANGE_PATTERN = /^[\^~]/
24
+
25
+ const DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
26
+
27
+ export default {
28
+ meta: { name: '@diia-inhouse/oxlint-plugin-package' },
29
+ rules: {
30
+ 'no-service-in-package-name': {
31
+ meta: {
32
+ type: 'problem',
33
+ messages: {
34
+ forbidden: 'package.json "name" field must not contain the word "service" (found: "{{ name }}")',
35
+ },
36
+ },
37
+ create(context) {
38
+ return {
39
+ Program(node) {
40
+ const data = loadPackageJson()
41
+
42
+ if (data?.name?.includes('service')) {
43
+ context.report({ node, messageId: 'forbidden', data: { name: data.name } })
44
+ }
45
+ },
46
+ }
47
+ },
48
+ },
49
+
50
+ 'pinned-dependencies': {
51
+ meta: {
52
+ type: 'problem',
53
+ messages: {
54
+ unpinned: 'Dependency "{{ name }}" has unpinned version "{{ version }}". Pin to an exact version (remove ^ or ~).',
55
+ },
56
+ },
57
+ create(context) {
58
+ return {
59
+ Program(node) {
60
+ const data = loadPackageJson()
61
+
62
+ if (!data) {
63
+ return
64
+ }
65
+
66
+ for (const field of DEP_FIELDS) {
67
+ const deps = data[field]
68
+
69
+ if (!deps) {
70
+ continue
71
+ }
72
+
73
+ for (const [name, version] of Object.entries(deps)) {
74
+ if (typeof version === 'string' && VERSION_RANGE_PATTERN.test(version)) {
75
+ context.report({ node, messageId: 'unpinned', data: { name, version } })
76
+ }
77
+ }
78
+ }
79
+ },
80
+ }
81
+ },
82
+ },
83
+ },
84
+ }