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