@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.
- package/LICENCE.md +287 -0
- package/README.md +93 -0
- package/bin/dev.mjs +18 -0
- package/dist/oxfmt.config.d.ts +24 -0
- package/dist/oxfmt.config.d.ts.map +1 -0
- package/dist/oxfmt.config.js +85 -0
- package/dist/oxlint/config.d.ts +8 -0
- package/dist/oxlint/config.d.ts.map +1 -0
- package/dist/oxlint/config.js +451 -0
- package/dist/oxlint/plugins/classEncapsulation.mjs +108 -0
- package/dist/oxlint/plugins/codeConventions.mjs +278 -0
- package/dist/oxlint/plugins/mongooseSchema.mjs +183 -0
- package/dist/oxlint/plugins/noHardcodedCyrillic.mjs +43 -0
- package/dist/oxlint/plugins/packageCheck.mjs +84 -0
- package/dist/oxlint/plugins/temporalConventions.mjs +169 -0
- package/dist/oxlint/plugins/testConventions.mjs +67 -0
- package/oxlint/plugins/classEncapsulation.mjs +108 -0
- package/oxlint/plugins/codeConventions.mjs +278 -0
- package/oxlint/plugins/mongooseSchema.mjs +183 -0
- package/oxlint/plugins/noHardcodedCyrillic.mjs +43 -0
- package/oxlint/plugins/packageCheck.mjs +84 -0
- package/oxlint/plugins/temporalConventions.mjs +169 -0
- package/oxlint/plugins/testConventions.mjs +67 -0
- package/package.json +68 -0
|
@@ -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
|
+
}
|