invar-tools 1.17.2__py3-none-any.whl → 1.17.3__py3-none-any.whl
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.
- invar/node_tools/.gitignore +10 -6
- invar/node_tools/eslint-plugin/rules/__tests__/behavior.test.js +1321 -0
- invar/node_tools/eslint-plugin/rules/__tests__/behavior.test.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/__tests__/e2e-scenarios.test.js +414 -0
- invar/node_tools/eslint-plugin/rules/__tests__/e2e-scenarios.test.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/__tests__/fixtures/core/function-lengths.js +142 -0
- invar/node_tools/eslint-plugin/rules/__tests__/fixtures/core/function-lengths.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/__tests__/fixtures/core/has-io-imports.js +15 -0
- invar/node_tools/eslint-plugin/rules/__tests__/fixtures/core/has-io-imports.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/__tests__/fixtures/core/valid-small.js +27 -0
- invar/node_tools/eslint-plugin/rules/__tests__/fixtures/core/valid-small.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/__tests__/fixtures/exported-functions.js +43 -0
- invar/node_tools/eslint-plugin/rules/__tests__/fixtures/exported-functions.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/__tests__/fixtures/shell/with-io.js +27 -0
- invar/node_tools/eslint-plugin/rules/__tests__/fixtures/shell/with-io.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/__tests__/fixtures/tests/large.test.js +260 -0
- invar/node_tools/eslint-plugin/rules/__tests__/fixtures/tests/large.test.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/max-file-lines.js +105 -0
- invar/node_tools/eslint-plugin/rules/max-file-lines.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/max-function-lines.js +133 -0
- invar/node_tools/eslint-plugin/rules/max-function-lines.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/no-any-in-schema.js +39 -0
- invar/node_tools/eslint-plugin/rules/no-any-in-schema.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/no-empty-schema.js +69 -0
- invar/node_tools/eslint-plugin/rules/no-empty-schema.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/no-impure-calls-in-core.js +52 -0
- invar/node_tools/eslint-plugin/rules/no-impure-calls-in-core.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/no-io-in-core.js +99 -0
- invar/node_tools/eslint-plugin/rules/no-io-in-core.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/no-pure-logic-in-shell.js +197 -0
- invar/node_tools/eslint-plugin/rules/no-pure-logic-in-shell.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/no-redundant-type-schema.js +99 -0
- invar/node_tools/eslint-plugin/rules/no-redundant-type-schema.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/no-runtime-imports.js +66 -0
- invar/node_tools/eslint-plugin/rules/no-runtime-imports.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/require-complete-validation.js +104 -0
- invar/node_tools/eslint-plugin/rules/require-complete-validation.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/require-jsdoc-example.js +81 -0
- invar/node_tools/eslint-plugin/rules/require-jsdoc-example.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/require-schema-validation.js +308 -0
- invar/node_tools/eslint-plugin/rules/require-schema-validation.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/shell-complexity.js +273 -0
- invar/node_tools/eslint-plugin/rules/shell-complexity.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/shell-result-type.js +138 -0
- invar/node_tools/eslint-plugin/rules/shell-result-type.js.map +1 -0
- invar/node_tools/eslint-plugin/rules/thin-entry-points.js +174 -0
- invar/node_tools/eslint-plugin/rules/thin-entry-points.js.map +1 -0
- invar/node_tools/eslint-plugin/utils/layer-detection.js +91 -0
- invar/node_tools/eslint-plugin/utils/layer-detection.js.map +1 -0
- invar/node_tools/eslint-plugin/utils/math-example.js +31 -0
- invar/node_tools/eslint-plugin/utils/math-example.js.map +1 -0
- {invar_tools-1.17.2.dist-info → invar_tools-1.17.3.dist-info}/METADATA +1 -1
- {invar_tools-1.17.2.dist-info → invar_tools-1.17.3.dist-info}/RECORD +58 -8
- {invar_tools-1.17.2.dist-info → invar_tools-1.17.3.dist-info}/WHEEL +0 -0
- {invar_tools-1.17.2.dist-info → invar_tools-1.17.3.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.17.2.dist-info → invar_tools-1.17.3.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.17.2.dist-info → invar_tools-1.17.3.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.17.2.dist-info → invar_tools-1.17.3.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,1321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavior tests for Invar ESLint rules
|
|
3
|
+
*
|
|
4
|
+
* Tests rules with inline code examples (JavaScript syntax for compatibility)
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import { RuleTester } from 'eslint';
|
|
8
|
+
// Import rules
|
|
9
|
+
import { maxFileLines } from '../max-file-lines.js';
|
|
10
|
+
import { maxFunctionLines } from '../max-function-lines.js';
|
|
11
|
+
import { requireJsdocExample } from '../require-jsdoc-example.js';
|
|
12
|
+
import { noIoInCore } from '../no-io-in-core.js';
|
|
13
|
+
import { noEmptySchema } from '../no-empty-schema.js';
|
|
14
|
+
import { noRedundantTypeSchema } from '../no-redundant-type-schema.js';
|
|
15
|
+
import { requireCompleteValidation } from '../require-complete-validation.js';
|
|
16
|
+
import { requireSchemaValidation } from '../require-schema-validation.js';
|
|
17
|
+
import { noRuntimeImports } from '../no-runtime-imports.js';
|
|
18
|
+
import { noImpureCallsInCore } from '../no-impure-calls-in-core.js';
|
|
19
|
+
import { noPureLogicInShell } from '../no-pure-logic-in-shell.js';
|
|
20
|
+
import { shellComplexity } from '../shell-complexity.js';
|
|
21
|
+
import { thinEntryPoints } from '../thin-entry-points.js';
|
|
22
|
+
import { getLayer, getLimits } from '../../utils/layer-detection.js';
|
|
23
|
+
// Create RuleTester with modern JS configuration
|
|
24
|
+
const ruleTester = new RuleTester({
|
|
25
|
+
languageOptions: {
|
|
26
|
+
ecmaVersion: 2022,
|
|
27
|
+
sourceType: 'module',
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
describe('layer-detection', () => {
|
|
31
|
+
it('should detect core layer from /core/ path', () => {
|
|
32
|
+
expect(getLayer('/project/src/core/parser.ts')).toBe('core');
|
|
33
|
+
});
|
|
34
|
+
it('should detect shell layer from /shell/ path', () => {
|
|
35
|
+
expect(getLayer('/project/src/shell/io.ts')).toBe('shell');
|
|
36
|
+
});
|
|
37
|
+
it('should detect tests layer from .test.ts extension', () => {
|
|
38
|
+
expect(getLayer('/project/src/utils.test.ts')).toBe('tests');
|
|
39
|
+
});
|
|
40
|
+
it('should detect tests layer from .spec.ts extension', () => {
|
|
41
|
+
expect(getLayer('/project/src/utils.spec.ts')).toBe('tests');
|
|
42
|
+
});
|
|
43
|
+
it('should detect tests layer from /tests/ directory', () => {
|
|
44
|
+
expect(getLayer('/project/tests/unit.ts')).toBe('tests');
|
|
45
|
+
});
|
|
46
|
+
it('should prioritize tests over core', () => {
|
|
47
|
+
expect(getLayer('/project/src/core/parser.test.ts')).toBe('tests');
|
|
48
|
+
});
|
|
49
|
+
it('should detect core from relative path', () => {
|
|
50
|
+
expect(getLayer('core/parser.ts')).toBe('core');
|
|
51
|
+
});
|
|
52
|
+
it('should detect shell from relative path', () => {
|
|
53
|
+
expect(getLayer('shell/io.ts')).toBe('shell');
|
|
54
|
+
});
|
|
55
|
+
it('should handle Windows paths', () => {
|
|
56
|
+
expect(getLayer('C:\\Project\\core\\parser.ts')).toBe('core');
|
|
57
|
+
expect(getLayer('C:\\Project\\shell\\io.ts')).toBe('shell');
|
|
58
|
+
});
|
|
59
|
+
it('should not match /hardcore/ as core', () => {
|
|
60
|
+
expect(getLayer('/project/hardcore/file.ts')).toBe('default');
|
|
61
|
+
});
|
|
62
|
+
it('should not match /eggshell/ as shell', () => {
|
|
63
|
+
expect(getLayer('/project/eggshell/file.ts')).toBe('default');
|
|
64
|
+
});
|
|
65
|
+
it('should return correct limits for each layer', () => {
|
|
66
|
+
expect(getLimits('/core/file.ts')).toEqual({
|
|
67
|
+
maxFileLines: 650,
|
|
68
|
+
maxFunctionLines: 65,
|
|
69
|
+
});
|
|
70
|
+
expect(getLimits('/shell/file.ts')).toEqual({
|
|
71
|
+
maxFileLines: 910,
|
|
72
|
+
maxFunctionLines: 130,
|
|
73
|
+
});
|
|
74
|
+
expect(getLimits('/file.test.ts')).toEqual({
|
|
75
|
+
maxFileLines: 1300,
|
|
76
|
+
maxFunctionLines: 260,
|
|
77
|
+
});
|
|
78
|
+
expect(getLimits('/file.ts')).toEqual({
|
|
79
|
+
maxFileLines: 780,
|
|
80
|
+
maxFunctionLines: 104,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe('max-file-lines', () => {
|
|
85
|
+
it('should detect files exceeding core limit (650 lines)', () => {
|
|
86
|
+
ruleTester.run('max-file-lines', maxFileLines, {
|
|
87
|
+
valid: [
|
|
88
|
+
{
|
|
89
|
+
code: '// Valid core file\n' + 'const x = 1;\n'.repeat(648), // 649 lines total
|
|
90
|
+
filename: '/project/core/valid.js',
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
invalid: [
|
|
94
|
+
{
|
|
95
|
+
code: '// Invalid core file\n' + 'const x = 1;\n'.repeat(650), // 651 lines total
|
|
96
|
+
filename: '/project/core/invalid.js',
|
|
97
|
+
errors: [{ messageId: 'tooManyLines' }],
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
it('should use different limits for shell (910 lines)', () => {
|
|
103
|
+
ruleTester.run('max-file-lines', maxFileLines, {
|
|
104
|
+
valid: [
|
|
105
|
+
{
|
|
106
|
+
code: '// Valid shell file\n' + 'const x = 1;\n'.repeat(908), // 909 lines total
|
|
107
|
+
filename: '/project/shell/valid.js',
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
invalid: [
|
|
111
|
+
{
|
|
112
|
+
code: '// Invalid shell file\n' + 'const x = 1;\n'.repeat(910), // 911 lines total
|
|
113
|
+
filename: '/project/shell/invalid.js',
|
|
114
|
+
errors: [{ messageId: 'tooManyLines' }],
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
it('should use different limits for tests (1300 lines)', () => {
|
|
120
|
+
ruleTester.run('max-file-lines', maxFileLines, {
|
|
121
|
+
valid: [
|
|
122
|
+
{
|
|
123
|
+
code: '// Valid test file\n' + 'const x = 1;\n'.repeat(1298), // 1299 lines total
|
|
124
|
+
filename: '/project/tests/valid.test.js',
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
invalid: [
|
|
128
|
+
{
|
|
129
|
+
code: '// Invalid test file\n' + 'const x = 1;\n'.repeat(1300), // 1301 lines total
|
|
130
|
+
filename: '/project/tests/invalid.test.js',
|
|
131
|
+
errors: [{ messageId: 'tooManyLines' }],
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
describe('max-function-lines', () => {
|
|
138
|
+
it('should detect functions exceeding core limit (65 lines)', () => {
|
|
139
|
+
ruleTester.run('max-function-lines', maxFunctionLines, {
|
|
140
|
+
valid: [
|
|
141
|
+
{
|
|
142
|
+
code: `function validCoreFunction() {\n${' const x = 1;\n'.repeat(63)}}`, // 65 lines total
|
|
143
|
+
filename: '/project/core/valid.js',
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
invalid: [
|
|
147
|
+
{
|
|
148
|
+
code: `function invalidCoreFunction() {\n${' const x = 1;\n'.repeat(65)}}`, // 67 lines total
|
|
149
|
+
filename: '/project/core/invalid.js',
|
|
150
|
+
errors: [{ messageId: 'tooManyLines' }],
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
it('should use different limits for shell (130 lines)', () => {
|
|
156
|
+
ruleTester.run('max-function-lines', maxFunctionLines, {
|
|
157
|
+
valid: [
|
|
158
|
+
{
|
|
159
|
+
code: `function validShellFunction() {\n${' const x = 1;\n'.repeat(128)}}`, // 130 lines total
|
|
160
|
+
filename: '/project/shell/valid.js',
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
invalid: [
|
|
164
|
+
{
|
|
165
|
+
code: `function invalidShellFunction() {\n${' const x = 1;\n'.repeat(130)}}`, // 132 lines total
|
|
166
|
+
filename: '/project/shell/invalid.js',
|
|
167
|
+
errors: [{ messageId: 'tooManyLines' }],
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
it('should use different limits for tests (260 lines)', () => {
|
|
173
|
+
ruleTester.run('max-function-lines', maxFunctionLines, {
|
|
174
|
+
valid: [
|
|
175
|
+
{
|
|
176
|
+
code: `function validTestFunction() {\n${' const x = 1;\n'.repeat(258)}}`, // 260 lines total
|
|
177
|
+
filename: '/project/tests/valid.test.js',
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
invalid: [
|
|
181
|
+
{
|
|
182
|
+
code: `function invalidTestFunction() {\n${' const x = 1;\n'.repeat(260)}}`, // 262 lines total
|
|
183
|
+
filename: '/project/tests/invalid.test.js',
|
|
184
|
+
errors: [{ messageId: 'tooManyLines' }],
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
describe('require-jsdoc-example', () => {
|
|
191
|
+
it('should require @example for exported functions', () => {
|
|
192
|
+
ruleTester.run('require-jsdoc-example', requireJsdocExample, {
|
|
193
|
+
valid: [
|
|
194
|
+
{
|
|
195
|
+
code: `
|
|
196
|
+
/**
|
|
197
|
+
* Valid function with example
|
|
198
|
+
* @example
|
|
199
|
+
* foo() // => 'bar'
|
|
200
|
+
*/
|
|
201
|
+
export function foo() { return 'bar'; }
|
|
202
|
+
`,
|
|
203
|
+
filename: '/project/test.js',
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
code: `
|
|
207
|
+
// Non-exported function - no @example required
|
|
208
|
+
function privateHelper() { return 'private'; }
|
|
209
|
+
`,
|
|
210
|
+
filename: '/project/test.js',
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
invalid: [
|
|
214
|
+
{
|
|
215
|
+
code: `
|
|
216
|
+
/**
|
|
217
|
+
* Missing @example
|
|
218
|
+
*/
|
|
219
|
+
export function foo() { return 'bar'; }
|
|
220
|
+
`,
|
|
221
|
+
filename: '/project/test.js',
|
|
222
|
+
errors: [{ messageId: 'missingExample' }],
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
it('should require @example for exported arrow functions', () => {
|
|
228
|
+
ruleTester.run('require-jsdoc-example', requireJsdocExample, {
|
|
229
|
+
valid: [
|
|
230
|
+
{
|
|
231
|
+
code: `
|
|
232
|
+
/**
|
|
233
|
+
* Valid arrow function with example
|
|
234
|
+
* @example
|
|
235
|
+
* foo() // => 'bar'
|
|
236
|
+
*/
|
|
237
|
+
export const foo = () => 'bar';
|
|
238
|
+
`,
|
|
239
|
+
filename: '/project/test.js',
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
invalid: [
|
|
243
|
+
{
|
|
244
|
+
code: `
|
|
245
|
+
/**
|
|
246
|
+
* Missing @example
|
|
247
|
+
*/
|
|
248
|
+
export const foo = () => 'bar';
|
|
249
|
+
`,
|
|
250
|
+
filename: '/project/test.js',
|
|
251
|
+
errors: [{ messageId: 'missingExample' }],
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
describe('no-io-in-core', () => {
|
|
258
|
+
it('should forbid I/O imports in /core/ directories', () => {
|
|
259
|
+
ruleTester.run('no-io-in-core', noIoInCore, {
|
|
260
|
+
valid: [
|
|
261
|
+
{
|
|
262
|
+
code: `import { something } from 'lodash';`,
|
|
263
|
+
filename: '/project/core/valid.js',
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
code: `import * as fs from 'fs';`,
|
|
267
|
+
filename: '/project/shell/valid.js', // Allowed in shell
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
invalid: [
|
|
271
|
+
{
|
|
272
|
+
code: `import * as fs from 'fs';`,
|
|
273
|
+
filename: '/project/core/invalid.js',
|
|
274
|
+
errors: [{ messageId: 'ioInCore' }],
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
code: `import { readFile } from 'node:fs/promises';`,
|
|
278
|
+
filename: '/project/core/invalid.js',
|
|
279
|
+
errors: [{ messageId: 'ioInCore' }],
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
code: `import axios from 'axios';`,
|
|
283
|
+
filename: '/project/core/invalid.js',
|
|
284
|
+
errors: [{ messageId: 'ioInCore' }],
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
code: `import { S3Client } from '@aws-sdk/client-s3';`,
|
|
288
|
+
filename: '/project/core/invalid.js',
|
|
289
|
+
errors: [{ messageId: 'ioInCore' }],
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
code: `import { deploy } from '@vercel/node';`,
|
|
293
|
+
filename: '/project/core/invalid.js',
|
|
294
|
+
errors: [{ messageId: 'ioInCore' }],
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
it('should handle Windows-style core paths', () => {
|
|
300
|
+
ruleTester.run('no-io-in-core', noIoInCore, {
|
|
301
|
+
valid: [],
|
|
302
|
+
invalid: [
|
|
303
|
+
{
|
|
304
|
+
code: `import * as fs from 'fs';`,
|
|
305
|
+
filename: 'C:\\\\Project\\\\core\\\\test.js',
|
|
306
|
+
errors: [{ messageId: 'ioInCore' }],
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
it('should detect require() calls', () => {
|
|
312
|
+
ruleTester.run('no-io-in-core', noIoInCore, {
|
|
313
|
+
valid: [],
|
|
314
|
+
invalid: [
|
|
315
|
+
{
|
|
316
|
+
code: `const fs = require('fs');`,
|
|
317
|
+
filename: '/project/core/test.js',
|
|
318
|
+
errors: [{ messageId: 'ioInCore' }],
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
describe('Integration: Cross-platform path normalization', () => {
|
|
325
|
+
it('should handle Unix paths correctly', () => {
|
|
326
|
+
expect(getLayer('/Users/project/core/parser.ts')).toBe('core');
|
|
327
|
+
expect(getLayer('/home/user/project/shell/io.ts')).toBe('shell');
|
|
328
|
+
});
|
|
329
|
+
it('should handle Windows paths correctly', () => {
|
|
330
|
+
expect(getLayer('C:\\Users\\project\\core\\parser.ts')).toBe('core');
|
|
331
|
+
expect(getLayer('D:\\Projects\\shell\\io.ts')).toBe('shell');
|
|
332
|
+
});
|
|
333
|
+
it('should handle relative paths correctly', () => {
|
|
334
|
+
expect(getLayer('core/parser.ts')).toBe('core');
|
|
335
|
+
expect(getLayer('shell/io.ts')).toBe('shell');
|
|
336
|
+
expect(getLayer('src/core/parser.ts')).toBe('core');
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
describe('no-empty-schema', () => {
|
|
340
|
+
it('should detect empty z.object({})', () => {
|
|
341
|
+
ruleTester.run('no-empty-schema', noEmptySchema, {
|
|
342
|
+
valid: [
|
|
343
|
+
{ code: `const Schema = z.object({ id: z.string() });` },
|
|
344
|
+
{ code: `const Schema = z.object({ name: z.string(), age: z.number() });` },
|
|
345
|
+
],
|
|
346
|
+
invalid: [
|
|
347
|
+
{
|
|
348
|
+
code: `const Schema = z.object({});`,
|
|
349
|
+
errors: [{ messageId: 'emptyObject' }],
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
it('should detect .passthrough() calls', () => {
|
|
355
|
+
ruleTester.run('no-empty-schema', noEmptySchema, {
|
|
356
|
+
valid: [
|
|
357
|
+
{ code: `const Schema = z.object({ id: z.string() }).strict();` },
|
|
358
|
+
],
|
|
359
|
+
invalid: [
|
|
360
|
+
{
|
|
361
|
+
code: `const Schema = z.object({ id: z.string() }).passthrough();`,
|
|
362
|
+
errors: [{ messageId: 'passthrough' }],
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
it('should detect .loose() calls', () => {
|
|
368
|
+
ruleTester.run('no-empty-schema', noEmptySchema, {
|
|
369
|
+
valid: [
|
|
370
|
+
{ code: `const Schema = z.object({ id: z.string() });` },
|
|
371
|
+
],
|
|
372
|
+
invalid: [
|
|
373
|
+
{
|
|
374
|
+
code: `const Schema = z.object({ id: z.string() }).loose();`,
|
|
375
|
+
errors: [{ messageId: 'loose' }],
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
describe('no-redundant-type-schema', () => {
|
|
382
|
+
it('should detect z.string() without constraints', () => {
|
|
383
|
+
ruleTester.run('no-redundant-type-schema', noRedundantTypeSchema, {
|
|
384
|
+
valid: [
|
|
385
|
+
{ code: `const Schema = z.string().min(1);` },
|
|
386
|
+
{ code: `const Schema = z.string().max(100);` },
|
|
387
|
+
{ code: `const Schema = z.string().email();` },
|
|
388
|
+
{ code: `const Schema = z.string().regex(/^[a-z]+$/);` },
|
|
389
|
+
],
|
|
390
|
+
invalid: [
|
|
391
|
+
{
|
|
392
|
+
code: `const Schema = z.string();`,
|
|
393
|
+
errors: [{ messageId: 'redundantString' }],
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
it('should detect z.number() without constraints', () => {
|
|
399
|
+
ruleTester.run('no-redundant-type-schema', noRedundantTypeSchema, {
|
|
400
|
+
valid: [
|
|
401
|
+
{ code: `const Schema = z.number().min(0);` },
|
|
402
|
+
{ code: `const Schema = z.number().max(100);` },
|
|
403
|
+
{ code: `const Schema = z.number().int();` },
|
|
404
|
+
{ code: `const Schema = z.number().positive();` },
|
|
405
|
+
],
|
|
406
|
+
invalid: [
|
|
407
|
+
{
|
|
408
|
+
code: `const Schema = z.number();`,
|
|
409
|
+
errors: [{ messageId: 'redundantNumber' }],
|
|
410
|
+
},
|
|
411
|
+
],
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
it('should detect z.boolean() (always redundant)', () => {
|
|
415
|
+
ruleTester.run('no-redundant-type-schema', noRedundantTypeSchema, {
|
|
416
|
+
valid: [],
|
|
417
|
+
invalid: [
|
|
418
|
+
{
|
|
419
|
+
code: `const Schema = z.boolean();`,
|
|
420
|
+
errors: [{ messageId: 'redundantBoolean' }],
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
describe('require-complete-validation', () => {
|
|
427
|
+
// Create RuleTester with TypeScript parser for this suite
|
|
428
|
+
const tsRuleTester = new RuleTester({
|
|
429
|
+
languageOptions: {
|
|
430
|
+
ecmaVersion: 2022,
|
|
431
|
+
sourceType: 'module',
|
|
432
|
+
parser: require('@typescript-eslint/parser'),
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
it('should detect mixed validated/unvalidated parameters', () => {
|
|
436
|
+
tsRuleTester.run('require-complete-validation', requireCompleteValidation, {
|
|
437
|
+
valid: [
|
|
438
|
+
{
|
|
439
|
+
code: `function transfer(
|
|
440
|
+
user: z.infer<typeof UserSchema>,
|
|
441
|
+
amount: z.infer<typeof AmountSchema>
|
|
442
|
+
) {}`,
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
code: `function calculate(x: number, y: number) {}`,
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
code: `function greet() {}`,
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
invalid: [
|
|
452
|
+
{
|
|
453
|
+
code: `function transfer(
|
|
454
|
+
user: z.infer<typeof UserSchema>,
|
|
455
|
+
amount: number
|
|
456
|
+
) {}`,
|
|
457
|
+
errors: [{ messageId: 'partialValidation' }],
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
code: `function process(
|
|
461
|
+
data: z.infer<typeof DataSchema>,
|
|
462
|
+
count: number,
|
|
463
|
+
name: string
|
|
464
|
+
) {}`,
|
|
465
|
+
errors: [{ messageId: 'partialValidation' }],
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
it('should handle arrow functions', () => {
|
|
471
|
+
tsRuleTester.run('require-complete-validation', requireCompleteValidation, {
|
|
472
|
+
valid: [
|
|
473
|
+
{
|
|
474
|
+
code: `const fn = (user: z.infer<typeof UserSchema>, id: z.infer<typeof IdSchema>) => {};`,
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
invalid: [
|
|
478
|
+
{
|
|
479
|
+
code: `const fn = (user: z.infer<typeof UserSchema>, id: number) => {};`,
|
|
480
|
+
errors: [{ messageId: 'partialValidation' }],
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
describe('require-schema-validation modes', () => {
|
|
487
|
+
const tsRuleTester = new RuleTester({
|
|
488
|
+
languageOptions: {
|
|
489
|
+
ecmaVersion: 2022,
|
|
490
|
+
sourceType: 'module',
|
|
491
|
+
parser: require('@typescript-eslint/parser'),
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
it('should check all functions in recommended mode (default)', () => {
|
|
495
|
+
tsRuleTester.run('require-schema-validation', requireSchemaValidation, {
|
|
496
|
+
valid: [
|
|
497
|
+
{
|
|
498
|
+
code: `
|
|
499
|
+
function process(user: z.infer<typeof UserSchema>) {
|
|
500
|
+
const validated = UserSchema.parse(user);
|
|
501
|
+
}
|
|
502
|
+
`,
|
|
503
|
+
},
|
|
504
|
+
],
|
|
505
|
+
invalid: [
|
|
506
|
+
{
|
|
507
|
+
code: `
|
|
508
|
+
function process(user: z.infer<typeof UserSchema>) {
|
|
509
|
+
console.log(user);
|
|
510
|
+
}
|
|
511
|
+
`,
|
|
512
|
+
errors: [{ messageId: 'missingValidation' }],
|
|
513
|
+
},
|
|
514
|
+
],
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
it('should check all functions in strict mode', () => {
|
|
518
|
+
tsRuleTester.run('require-schema-validation', requireSchemaValidation, {
|
|
519
|
+
valid: [
|
|
520
|
+
{
|
|
521
|
+
code: `
|
|
522
|
+
function process(user: z.infer<typeof UserSchema>) {
|
|
523
|
+
const validated = UserSchema.parse(user);
|
|
524
|
+
}
|
|
525
|
+
`,
|
|
526
|
+
options: [{ mode: 'strict' }],
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
invalid: [
|
|
530
|
+
{
|
|
531
|
+
code: `
|
|
532
|
+
function process(user: z.infer<typeof UserSchema>) {
|
|
533
|
+
console.log(user);
|
|
534
|
+
}
|
|
535
|
+
`,
|
|
536
|
+
options: [{ mode: 'strict' }],
|
|
537
|
+
errors: [{ messageId: 'missingValidation' }],
|
|
538
|
+
},
|
|
539
|
+
],
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
it('should only check high-risk functions in risk-based mode', () => {
|
|
543
|
+
tsRuleTester.run('require-schema-validation', requireSchemaValidation, {
|
|
544
|
+
valid: [
|
|
545
|
+
{
|
|
546
|
+
// Non-risk function should pass even without validation
|
|
547
|
+
code: `
|
|
548
|
+
function getData(user: z.infer<typeof UserSchema>) {
|
|
549
|
+
console.log(user);
|
|
550
|
+
}
|
|
551
|
+
`,
|
|
552
|
+
options: [{ mode: 'risk-based' }],
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
// Risk function with validation should pass
|
|
556
|
+
code: `
|
|
557
|
+
function processPayment(user: z.infer<typeof UserSchema>) {
|
|
558
|
+
const validated = UserSchema.parse(user);
|
|
559
|
+
}
|
|
560
|
+
`,
|
|
561
|
+
options: [{ mode: 'risk-based' }],
|
|
562
|
+
},
|
|
563
|
+
],
|
|
564
|
+
invalid: [
|
|
565
|
+
{
|
|
566
|
+
// Risk function without validation should fail
|
|
567
|
+
code: `
|
|
568
|
+
function processPayment(user: z.infer<typeof UserSchema>) {
|
|
569
|
+
console.log(user);
|
|
570
|
+
}
|
|
571
|
+
`,
|
|
572
|
+
options: [{ mode: 'risk-based' }],
|
|
573
|
+
errors: [{ messageId: 'missingValidationRisk' }],
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
code: `
|
|
577
|
+
function authenticateUser(token: z.infer<typeof TokenSchema>) {
|
|
578
|
+
console.log(token);
|
|
579
|
+
}
|
|
580
|
+
`,
|
|
581
|
+
options: [{ mode: 'risk-based' }],
|
|
582
|
+
errors: [{ messageId: 'missingValidationRisk' }],
|
|
583
|
+
},
|
|
584
|
+
],
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
it('should enforce for specific paths when enforceFor is set', () => {
|
|
588
|
+
tsRuleTester.run('require-schema-validation', requireSchemaValidation, {
|
|
589
|
+
valid: [
|
|
590
|
+
{
|
|
591
|
+
// File outside enforceFor paths should pass
|
|
592
|
+
code: `
|
|
593
|
+
function process(user: z.infer<typeof UserSchema>) {
|
|
594
|
+
console.log(user);
|
|
595
|
+
}
|
|
596
|
+
`,
|
|
597
|
+
filename: '/project/src/utils/helper.ts',
|
|
598
|
+
options: [{ mode: 'risk-based', enforceFor: ['**/payment/**', '**/auth/**'] }],
|
|
599
|
+
},
|
|
600
|
+
],
|
|
601
|
+
invalid: [
|
|
602
|
+
{
|
|
603
|
+
// File in payment path should fail
|
|
604
|
+
code: `
|
|
605
|
+
function process(user: z.infer<typeof UserSchema>) {
|
|
606
|
+
console.log(user);
|
|
607
|
+
}
|
|
608
|
+
`,
|
|
609
|
+
filename: '/project/src/payment/processor.ts',
|
|
610
|
+
options: [{ mode: 'risk-based', enforceFor: ['**/payment/**', '**/auth/**'] }],
|
|
611
|
+
errors: [{ messageId: 'missingValidation' }],
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
// File in auth path should fail
|
|
615
|
+
code: `
|
|
616
|
+
function process(user: z.infer<typeof UserSchema>) {
|
|
617
|
+
console.log(user);
|
|
618
|
+
}
|
|
619
|
+
`,
|
|
620
|
+
filename: '/project/src/auth/login.ts',
|
|
621
|
+
options: [{ mode: 'risk-based', enforceFor: ['**/payment/**', '**/auth/**'] }],
|
|
622
|
+
errors: [{ messageId: 'missingValidation' }],
|
|
623
|
+
},
|
|
624
|
+
],
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
describe('no-runtime-imports', () => {
|
|
629
|
+
it('should detect require() inside functions', () => {
|
|
630
|
+
ruleTester.run('no-runtime-imports', noRuntimeImports, {
|
|
631
|
+
valid: [
|
|
632
|
+
{
|
|
633
|
+
code: `const fs = require('fs');`, // Top-level require is OK
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
code: `import fs from 'fs';`, // Top-level import is OK
|
|
637
|
+
},
|
|
638
|
+
],
|
|
639
|
+
invalid: [
|
|
640
|
+
{
|
|
641
|
+
code: `
|
|
642
|
+
function loadModule() {
|
|
643
|
+
const fs = require('fs');
|
|
644
|
+
}
|
|
645
|
+
`,
|
|
646
|
+
errors: [{ messageId: 'runtimeRequire' }],
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
code: `
|
|
650
|
+
const handler = () => {
|
|
651
|
+
const path = require('path');
|
|
652
|
+
};
|
|
653
|
+
`,
|
|
654
|
+
errors: [{ messageId: 'runtimeRequire' }],
|
|
655
|
+
},
|
|
656
|
+
],
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
it('should detect dynamic import() inside functions', () => {
|
|
660
|
+
ruleTester.run('no-runtime-imports', noRuntimeImports, {
|
|
661
|
+
valid: [],
|
|
662
|
+
invalid: [
|
|
663
|
+
{
|
|
664
|
+
code: `
|
|
665
|
+
async function loadModule() {
|
|
666
|
+
const mod = await import('./module');
|
|
667
|
+
}
|
|
668
|
+
`,
|
|
669
|
+
errors: [{ messageId: 'runtimeImport' }],
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
code: `
|
|
673
|
+
const handler = async () => {
|
|
674
|
+
const { foo } = await import('./foo');
|
|
675
|
+
};
|
|
676
|
+
`,
|
|
677
|
+
errors: [{ messageId: 'runtimeImport' }],
|
|
678
|
+
},
|
|
679
|
+
],
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
describe('no-impure-calls-in-core', () => {
|
|
684
|
+
it('should detect Core importing from Shell', () => {
|
|
685
|
+
ruleTester.run('no-impure-calls-in-core', noImpureCallsInCore, {
|
|
686
|
+
valid: [
|
|
687
|
+
{
|
|
688
|
+
code: `import { helper } from '../utils';`,
|
|
689
|
+
filename: '/project/core/logic.js',
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
code: `import { ioFunc } from '../shell/io';`,
|
|
693
|
+
filename: '/project/shell/handler.js', // OK in shell
|
|
694
|
+
},
|
|
695
|
+
],
|
|
696
|
+
invalid: [
|
|
697
|
+
{
|
|
698
|
+
code: `import { ioFunc } from '../shell/io';`,
|
|
699
|
+
filename: '/project/core/logic.js',
|
|
700
|
+
errors: [{ messageId: 'shellImportInCore' }],
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
code: `import { readData } from '../../shell/data';`,
|
|
704
|
+
filename: '/project/src/core/parser.js',
|
|
705
|
+
errors: [{ messageId: 'shellImportInCore' }],
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
code: `import { handler } from 'shell/handler';`,
|
|
709
|
+
filename: '/project/core/logic.js',
|
|
710
|
+
errors: [{ messageId: 'shellImportInCore' }],
|
|
711
|
+
},
|
|
712
|
+
],
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
it('should handle Windows-style paths', () => {
|
|
716
|
+
ruleTester.run('no-impure-calls-in-core', noImpureCallsInCore, {
|
|
717
|
+
valid: [],
|
|
718
|
+
invalid: [
|
|
719
|
+
{
|
|
720
|
+
code: `import { ioFunc } from '..\\\\shell\\\\io';`,
|
|
721
|
+
filename: 'C:\\\\Project\\\\core\\\\logic.js',
|
|
722
|
+
errors: [{ messageId: 'shellImportInCore' }],
|
|
723
|
+
},
|
|
724
|
+
],
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
describe('no-pure-logic-in-shell', () => {
|
|
729
|
+
it('should warn when Shell function has no I/O indicators', () => {
|
|
730
|
+
ruleTester.run('no-pure-logic-in-shell', noPureLogicInShell, {
|
|
731
|
+
valid: [
|
|
732
|
+
{
|
|
733
|
+
// Async function - has I/O indicator
|
|
734
|
+
code: `
|
|
735
|
+
async function fetchData() {
|
|
736
|
+
const x = 1;
|
|
737
|
+
const y = 2;
|
|
738
|
+
const z = 3;
|
|
739
|
+
const w = 4;
|
|
740
|
+
return x + y + z + w;
|
|
741
|
+
}
|
|
742
|
+
`,
|
|
743
|
+
filename: '/project/shell/data.js',
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
// Uses fs - has I/O indicator
|
|
747
|
+
code: `
|
|
748
|
+
function readConfig() {
|
|
749
|
+
const x = 1;
|
|
750
|
+
const y = 2;
|
|
751
|
+
const z = 3;
|
|
752
|
+
const w = 4;
|
|
753
|
+
return fs.readFileSync('config.json');
|
|
754
|
+
}
|
|
755
|
+
`,
|
|
756
|
+
filename: '/project/shell/config.js',
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
// Returns Result - has I/O indicator
|
|
760
|
+
code: `
|
|
761
|
+
function loadData() {
|
|
762
|
+
const x = 1;
|
|
763
|
+
const y = 2;
|
|
764
|
+
const z = 3;
|
|
765
|
+
const w = 4;
|
|
766
|
+
return Success(data);
|
|
767
|
+
}
|
|
768
|
+
`,
|
|
769
|
+
filename: '/project/shell/loader.js',
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
// Small function - not substantial logic
|
|
773
|
+
code: `
|
|
774
|
+
function helper() {
|
|
775
|
+
const x = 1;
|
|
776
|
+
return x;
|
|
777
|
+
}
|
|
778
|
+
`,
|
|
779
|
+
filename: '/project/shell/utils.js',
|
|
780
|
+
},
|
|
781
|
+
],
|
|
782
|
+
invalid: [
|
|
783
|
+
{
|
|
784
|
+
// Pure logic in Shell - no I/O, substantial statements
|
|
785
|
+
code: `
|
|
786
|
+
function calculateTotal() {
|
|
787
|
+
const a = 1;
|
|
788
|
+
const b = 2;
|
|
789
|
+
const c = 3;
|
|
790
|
+
const d = 4;
|
|
791
|
+
return a + b + c + d;
|
|
792
|
+
}
|
|
793
|
+
`,
|
|
794
|
+
filename: '/project/shell/calculator.js',
|
|
795
|
+
errors: [{ messageId: 'pureLogicInShell' }],
|
|
796
|
+
},
|
|
797
|
+
],
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
it('should not check non-shell files', () => {
|
|
801
|
+
ruleTester.run('no-pure-logic-in-shell', noPureLogicInShell, {
|
|
802
|
+
valid: [
|
|
803
|
+
{
|
|
804
|
+
// Core file - rule should skip it
|
|
805
|
+
code: `
|
|
806
|
+
function pureCalculation() {
|
|
807
|
+
const a = 1;
|
|
808
|
+
const b = 2;
|
|
809
|
+
const c = 3;
|
|
810
|
+
const d = 4;
|
|
811
|
+
return a + b + c + d;
|
|
812
|
+
}
|
|
813
|
+
`,
|
|
814
|
+
filename: '/project/core/logic.js',
|
|
815
|
+
},
|
|
816
|
+
],
|
|
817
|
+
invalid: [],
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
it('should extract function name from FunctionExpression with id', () => {
|
|
821
|
+
ruleTester.run('no-pure-logic-in-shell', noPureLogicInShell, {
|
|
822
|
+
valid: [],
|
|
823
|
+
invalid: [
|
|
824
|
+
{
|
|
825
|
+
// Named FunctionExpression should use its own name
|
|
826
|
+
code: `
|
|
827
|
+
const foo = function namedFunc() {
|
|
828
|
+
const a = 1;
|
|
829
|
+
const b = 2;
|
|
830
|
+
const c = 3;
|
|
831
|
+
const d = 4;
|
|
832
|
+
return a + b + c + d;
|
|
833
|
+
};
|
|
834
|
+
`,
|
|
835
|
+
filename: '/project/shell/calculator.js',
|
|
836
|
+
errors: [{
|
|
837
|
+
messageId: 'pureLogicInShell',
|
|
838
|
+
data: { name: 'namedFunc' }, // Should use function's own name
|
|
839
|
+
}],
|
|
840
|
+
},
|
|
841
|
+
],
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
it('should extract function name from parent VariableDeclarator for anonymous FunctionExpression', () => {
|
|
845
|
+
ruleTester.run('no-pure-logic-in-shell', noPureLogicInShell, {
|
|
846
|
+
valid: [],
|
|
847
|
+
invalid: [
|
|
848
|
+
{
|
|
849
|
+
// Anonymous FunctionExpression should use variable name
|
|
850
|
+
code: `
|
|
851
|
+
const calculateTotal = function() {
|
|
852
|
+
const a = 1;
|
|
853
|
+
const b = 2;
|
|
854
|
+
const c = 3;
|
|
855
|
+
const d = 4;
|
|
856
|
+
return a + b + c + d;
|
|
857
|
+
};
|
|
858
|
+
`,
|
|
859
|
+
filename: '/project/shell/calculator.js',
|
|
860
|
+
errors: [{
|
|
861
|
+
messageId: 'pureLogicInShell',
|
|
862
|
+
data: { name: 'calculateTotal' }, // Should use variable name
|
|
863
|
+
}],
|
|
864
|
+
},
|
|
865
|
+
],
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
it('should extract function name from parent VariableDeclarator for ArrowFunctionExpression', () => {
|
|
869
|
+
ruleTester.run('no-pure-logic-in-shell', noPureLogicInShell, {
|
|
870
|
+
valid: [],
|
|
871
|
+
invalid: [
|
|
872
|
+
{
|
|
873
|
+
// Arrow function should use variable name
|
|
874
|
+
code: `
|
|
875
|
+
const processData = () => {
|
|
876
|
+
const a = 1;
|
|
877
|
+
const b = 2;
|
|
878
|
+
const c = 3;
|
|
879
|
+
const d = 4;
|
|
880
|
+
return a + b + c + d;
|
|
881
|
+
};
|
|
882
|
+
`,
|
|
883
|
+
filename: '/project/shell/processor.js',
|
|
884
|
+
errors: [{
|
|
885
|
+
messageId: 'pureLogicInShell',
|
|
886
|
+
data: { name: 'processData' }, // Should use variable name
|
|
887
|
+
}],
|
|
888
|
+
},
|
|
889
|
+
],
|
|
890
|
+
});
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
describe('shell-complexity', () => {
|
|
894
|
+
it('should detect functions with too many statements', () => {
|
|
895
|
+
ruleTester.run('shell-complexity', shellComplexity, {
|
|
896
|
+
valid: [
|
|
897
|
+
{
|
|
898
|
+
code: `
|
|
899
|
+
function simpleHandler() {
|
|
900
|
+
${'const x = 1;\n'.repeat(19)}
|
|
901
|
+
}
|
|
902
|
+
`,
|
|
903
|
+
filename: '/project/shell/handler.js',
|
|
904
|
+
},
|
|
905
|
+
],
|
|
906
|
+
invalid: [
|
|
907
|
+
{
|
|
908
|
+
code: `
|
|
909
|
+
function complexHandler() {
|
|
910
|
+
${'const x = 1;\n'.repeat(21)}
|
|
911
|
+
}
|
|
912
|
+
`,
|
|
913
|
+
filename: '/project/shell/handler.js',
|
|
914
|
+
errors: [{ messageId: 'tooManyStatements' }],
|
|
915
|
+
},
|
|
916
|
+
],
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
it('should detect functions with high cyclomatic complexity', () => {
|
|
920
|
+
ruleTester.run('shell-complexity', shellComplexity, {
|
|
921
|
+
valid: [
|
|
922
|
+
{
|
|
923
|
+
code: `
|
|
924
|
+
function lowComplexity() {
|
|
925
|
+
if (a) return 1;
|
|
926
|
+
if (b) return 2;
|
|
927
|
+
if (c) return 3;
|
|
928
|
+
return 0;
|
|
929
|
+
}
|
|
930
|
+
`,
|
|
931
|
+
filename: '/project/shell/handler.js',
|
|
932
|
+
},
|
|
933
|
+
],
|
|
934
|
+
invalid: [
|
|
935
|
+
{
|
|
936
|
+
code: `
|
|
937
|
+
function highComplexity() {
|
|
938
|
+
if (a) return 1;
|
|
939
|
+
else if (b) return 2;
|
|
940
|
+
else if (c) return 3;
|
|
941
|
+
else if (d) return 4;
|
|
942
|
+
else if (e) return 5;
|
|
943
|
+
else if (f) return 6;
|
|
944
|
+
else if (g) return 7;
|
|
945
|
+
else if (h) return 8;
|
|
946
|
+
else if (i) return 9;
|
|
947
|
+
else if (j) return 10;
|
|
948
|
+
else return 0;
|
|
949
|
+
}
|
|
950
|
+
`,
|
|
951
|
+
filename: '/project/shell/handler.js',
|
|
952
|
+
errors: [{ messageId: 'tooComplex' }],
|
|
953
|
+
},
|
|
954
|
+
],
|
|
955
|
+
});
|
|
956
|
+
});
|
|
957
|
+
it('should use custom thresholds', () => {
|
|
958
|
+
ruleTester.run('shell-complexity', shellComplexity, {
|
|
959
|
+
valid: [
|
|
960
|
+
{
|
|
961
|
+
code: `
|
|
962
|
+
function handler() {
|
|
963
|
+
${'const x = 1;\n'.repeat(6)}
|
|
964
|
+
}
|
|
965
|
+
`,
|
|
966
|
+
filename: '/project/shell/handler.js',
|
|
967
|
+
options: [{ maxStatements: 5 }],
|
|
968
|
+
},
|
|
969
|
+
],
|
|
970
|
+
invalid: [
|
|
971
|
+
{
|
|
972
|
+
code: `
|
|
973
|
+
function handler() {
|
|
974
|
+
${'const x = 1;\n'.repeat(6)}
|
|
975
|
+
}
|
|
976
|
+
`,
|
|
977
|
+
filename: '/project/shell/handler.js',
|
|
978
|
+
options: [{ maxStatements: 5 }],
|
|
979
|
+
errors: [{ messageId: 'tooManyStatements' }],
|
|
980
|
+
},
|
|
981
|
+
],
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
it('should not count default case in complexity', () => {
|
|
985
|
+
ruleTester.run('shell-complexity', shellComplexity, {
|
|
986
|
+
valid: [
|
|
987
|
+
{
|
|
988
|
+
// Switch with default should not add to complexity
|
|
989
|
+
code: `
|
|
990
|
+
function handler(type) {
|
|
991
|
+
switch (type) {
|
|
992
|
+
case 'a': return 1;
|
|
993
|
+
case 'b': return 2;
|
|
994
|
+
case 'c': return 3;
|
|
995
|
+
default: return 0;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
`,
|
|
999
|
+
filename: '/project/shell/handler.js',
|
|
1000
|
+
options: [{ maxComplexity: 3 }],
|
|
1001
|
+
},
|
|
1002
|
+
],
|
|
1003
|
+
invalid: [],
|
|
1004
|
+
});
|
|
1005
|
+
});
|
|
1006
|
+
it('should count nullish coalescing operator', () => {
|
|
1007
|
+
ruleTester.run('shell-complexity', shellComplexity, {
|
|
1008
|
+
valid: [],
|
|
1009
|
+
invalid: [
|
|
1010
|
+
{
|
|
1011
|
+
// Nullish coalescing should add to complexity
|
|
1012
|
+
code: `
|
|
1013
|
+
function handler() {
|
|
1014
|
+
const a = x ?? 1;
|
|
1015
|
+
const b = y ?? 2;
|
|
1016
|
+
const c = z ?? 3;
|
|
1017
|
+
const d = w ?? 4;
|
|
1018
|
+
const e = v ?? 5;
|
|
1019
|
+
if (a) return a;
|
|
1020
|
+
if (b) return b;
|
|
1021
|
+
if (c) return c;
|
|
1022
|
+
if (d) return d;
|
|
1023
|
+
if (e) return e;
|
|
1024
|
+
return 0;
|
|
1025
|
+
}
|
|
1026
|
+
`,
|
|
1027
|
+
filename: '/project/shell/handler.js',
|
|
1028
|
+
options: [{ maxComplexity: 10 }],
|
|
1029
|
+
errors: [{ messageId: 'tooComplex' }],
|
|
1030
|
+
},
|
|
1031
|
+
],
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1034
|
+
it('should not check non-shell files', () => {
|
|
1035
|
+
ruleTester.run('shell-complexity', shellComplexity, {
|
|
1036
|
+
valid: [
|
|
1037
|
+
{
|
|
1038
|
+
code: `
|
|
1039
|
+
function complexCore() {
|
|
1040
|
+
${'const x = 1;\n'.repeat(30)}
|
|
1041
|
+
}
|
|
1042
|
+
`,
|
|
1043
|
+
filename: '/project/core/logic.js',
|
|
1044
|
+
},
|
|
1045
|
+
],
|
|
1046
|
+
invalid: [],
|
|
1047
|
+
});
|
|
1048
|
+
});
|
|
1049
|
+
it('should extract function name from FunctionExpression with id', () => {
|
|
1050
|
+
ruleTester.run('shell-complexity', shellComplexity, {
|
|
1051
|
+
valid: [],
|
|
1052
|
+
invalid: [
|
|
1053
|
+
{
|
|
1054
|
+
// Named FunctionExpression should use its own name
|
|
1055
|
+
code: `
|
|
1056
|
+
const foo = function namedHandler() {
|
|
1057
|
+
${'const x = 1;\n'.repeat(21)}
|
|
1058
|
+
};
|
|
1059
|
+
`,
|
|
1060
|
+
filename: '/project/shell/handler.js',
|
|
1061
|
+
errors: [{
|
|
1062
|
+
messageId: 'tooManyStatements',
|
|
1063
|
+
data: { name: 'namedHandler' }, // Should use function's own name
|
|
1064
|
+
}],
|
|
1065
|
+
},
|
|
1066
|
+
],
|
|
1067
|
+
});
|
|
1068
|
+
});
|
|
1069
|
+
it('should extract function name from parent VariableDeclarator for anonymous FunctionExpression', () => {
|
|
1070
|
+
ruleTester.run('shell-complexity', shellComplexity, {
|
|
1071
|
+
valid: [],
|
|
1072
|
+
invalid: [
|
|
1073
|
+
{
|
|
1074
|
+
// Anonymous FunctionExpression should use variable name
|
|
1075
|
+
code: `
|
|
1076
|
+
const processOrder = function() {
|
|
1077
|
+
${'const x = 1;\n'.repeat(21)}
|
|
1078
|
+
};
|
|
1079
|
+
`,
|
|
1080
|
+
filename: '/project/shell/orders.js',
|
|
1081
|
+
errors: [{
|
|
1082
|
+
messageId: 'tooManyStatements',
|
|
1083
|
+
data: { name: 'processOrder' }, // Should use variable name
|
|
1084
|
+
}],
|
|
1085
|
+
},
|
|
1086
|
+
],
|
|
1087
|
+
});
|
|
1088
|
+
});
|
|
1089
|
+
it('should extract function name from parent VariableDeclarator for ArrowFunctionExpression', () => {
|
|
1090
|
+
ruleTester.run('shell-complexity', shellComplexity, {
|
|
1091
|
+
valid: [],
|
|
1092
|
+
invalid: [
|
|
1093
|
+
{
|
|
1094
|
+
// Arrow function should use variable name
|
|
1095
|
+
code: `
|
|
1096
|
+
const handleRequest = () => {
|
|
1097
|
+
${'const x = 1;\n'.repeat(21)}
|
|
1098
|
+
};
|
|
1099
|
+
`,
|
|
1100
|
+
filename: '/project/shell/api.js',
|
|
1101
|
+
errors: [{
|
|
1102
|
+
messageId: 'tooManyStatements',
|
|
1103
|
+
data: { name: 'handleRequest' }, // Should use variable name
|
|
1104
|
+
}],
|
|
1105
|
+
},
|
|
1106
|
+
],
|
|
1107
|
+
});
|
|
1108
|
+
});
|
|
1109
|
+
});
|
|
1110
|
+
describe('thin-entry-points', () => {
|
|
1111
|
+
it('should detect entry point files with too much logic', () => {
|
|
1112
|
+
ruleTester.run('thin-entry-points', thinEntryPoints, {
|
|
1113
|
+
valid: [
|
|
1114
|
+
{
|
|
1115
|
+
// Simple index.ts with imports and exports
|
|
1116
|
+
code: `
|
|
1117
|
+
import { foo } from './foo';
|
|
1118
|
+
import { bar } from './bar';
|
|
1119
|
+
export { foo, bar };
|
|
1120
|
+
export default foo;
|
|
1121
|
+
`,
|
|
1122
|
+
filename: '/project/index.ts',
|
|
1123
|
+
},
|
|
1124
|
+
{
|
|
1125
|
+
// Few config statements are OK
|
|
1126
|
+
code: `
|
|
1127
|
+
import express from 'express';
|
|
1128
|
+
const app = express();
|
|
1129
|
+
const PORT = 3000;
|
|
1130
|
+
export { app, PORT };
|
|
1131
|
+
`,
|
|
1132
|
+
filename: '/project/main.ts',
|
|
1133
|
+
},
|
|
1134
|
+
],
|
|
1135
|
+
invalid: [
|
|
1136
|
+
{
|
|
1137
|
+
// Too many statements
|
|
1138
|
+
code: `
|
|
1139
|
+
import express from 'express';
|
|
1140
|
+
const app = express();
|
|
1141
|
+
const x1 = 1;
|
|
1142
|
+
const x2 = 2;
|
|
1143
|
+
const x3 = 3;
|
|
1144
|
+
const x4 = 4;
|
|
1145
|
+
const x5 = 5;
|
|
1146
|
+
const x6 = 6;
|
|
1147
|
+
const x7 = 7;
|
|
1148
|
+
const x8 = 8;
|
|
1149
|
+
const x9 = 9;
|
|
1150
|
+
const x10 = 10;
|
|
1151
|
+
const x11 = 11;
|
|
1152
|
+
export { app };
|
|
1153
|
+
`,
|
|
1154
|
+
filename: '/project/index.ts',
|
|
1155
|
+
errors: [{ messageId: 'tooMuchLogic' }],
|
|
1156
|
+
},
|
|
1157
|
+
],
|
|
1158
|
+
});
|
|
1159
|
+
});
|
|
1160
|
+
it('should detect complex logic in entry points', () => {
|
|
1161
|
+
ruleTester.run('thin-entry-points', thinEntryPoints, {
|
|
1162
|
+
valid: [
|
|
1163
|
+
{
|
|
1164
|
+
code: `
|
|
1165
|
+
import { handler } from './handler';
|
|
1166
|
+
export { handler };
|
|
1167
|
+
`,
|
|
1168
|
+
filename: '/project/cli.ts',
|
|
1169
|
+
},
|
|
1170
|
+
],
|
|
1171
|
+
invalid: [
|
|
1172
|
+
{
|
|
1173
|
+
// Function definition in entry point
|
|
1174
|
+
code: `
|
|
1175
|
+
function processData() {
|
|
1176
|
+
return 42;
|
|
1177
|
+
}
|
|
1178
|
+
export { processData };
|
|
1179
|
+
`,
|
|
1180
|
+
filename: '/project/index.ts',
|
|
1181
|
+
errors: [{ messageId: 'hasComplexLogic' }],
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
// Class definition in entry point
|
|
1185
|
+
code: `
|
|
1186
|
+
class App {
|
|
1187
|
+
run() { return 'running'; }
|
|
1188
|
+
}
|
|
1189
|
+
export { App };
|
|
1190
|
+
`,
|
|
1191
|
+
filename: '/project/main.ts',
|
|
1192
|
+
errors: [{ messageId: 'hasComplexLogic' }],
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
// Control flow in entry point
|
|
1196
|
+
code: `
|
|
1197
|
+
import { config } from './config';
|
|
1198
|
+
if (config.enabled) {
|
|
1199
|
+
console.log('enabled');
|
|
1200
|
+
}
|
|
1201
|
+
export { config };
|
|
1202
|
+
`,
|
|
1203
|
+
filename: '/project/app.ts',
|
|
1204
|
+
errors: [{ messageId: 'hasComplexLogic' }],
|
|
1205
|
+
},
|
|
1206
|
+
],
|
|
1207
|
+
});
|
|
1208
|
+
});
|
|
1209
|
+
it('should only check entry point files', () => {
|
|
1210
|
+
ruleTester.run('thin-entry-points', thinEntryPoints, {
|
|
1211
|
+
valid: [
|
|
1212
|
+
{
|
|
1213
|
+
// Non-entry point file can have complex logic
|
|
1214
|
+
code: `
|
|
1215
|
+
function complexFunction() {
|
|
1216
|
+
if (a) return 1;
|
|
1217
|
+
if (b) return 2;
|
|
1218
|
+
if (c) return 3;
|
|
1219
|
+
return 0;
|
|
1220
|
+
}
|
|
1221
|
+
export { complexFunction };
|
|
1222
|
+
`,
|
|
1223
|
+
filename: '/project/utils/helper.ts',
|
|
1224
|
+
},
|
|
1225
|
+
],
|
|
1226
|
+
invalid: [],
|
|
1227
|
+
});
|
|
1228
|
+
});
|
|
1229
|
+
it('should detect all entry point patterns', () => {
|
|
1230
|
+
const patterns = ['index.ts', 'main.ts', 'cli.ts', 'app.ts', 'server.ts'];
|
|
1231
|
+
for (const pattern of patterns) {
|
|
1232
|
+
ruleTester.run('thin-entry-points', thinEntryPoints, {
|
|
1233
|
+
valid: [],
|
|
1234
|
+
invalid: [
|
|
1235
|
+
{
|
|
1236
|
+
code: `
|
|
1237
|
+
function logic() { return 42; }
|
|
1238
|
+
export { logic };
|
|
1239
|
+
`,
|
|
1240
|
+
filename: `/project/${pattern}`,
|
|
1241
|
+
errors: [{ messageId: 'hasComplexLogic' }],
|
|
1242
|
+
},
|
|
1243
|
+
],
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
it('should handle Windows paths correctly in error messages', () => {
|
|
1248
|
+
ruleTester.run('thin-entry-points', thinEntryPoints, {
|
|
1249
|
+
valid: [],
|
|
1250
|
+
invalid: [
|
|
1251
|
+
{
|
|
1252
|
+
// Windows path should be normalized to show just filename
|
|
1253
|
+
code: `
|
|
1254
|
+
function logic() { return 42; }
|
|
1255
|
+
export { logic };
|
|
1256
|
+
`,
|
|
1257
|
+
filename: 'C:\\\\Project\\\\src\\\\index.ts',
|
|
1258
|
+
errors: [{
|
|
1259
|
+
messageId: 'hasComplexLogic',
|
|
1260
|
+
// Error message should show 'index.ts' not full path
|
|
1261
|
+
}],
|
|
1262
|
+
},
|
|
1263
|
+
],
|
|
1264
|
+
});
|
|
1265
|
+
});
|
|
1266
|
+
it('should treat export declarations with bodies as complex logic', () => {
|
|
1267
|
+
ruleTester.run('thin-entry-points', thinEntryPoints, {
|
|
1268
|
+
valid: [
|
|
1269
|
+
{
|
|
1270
|
+
// Pure re-export is OK
|
|
1271
|
+
code: `
|
|
1272
|
+
import { foo } from './foo';
|
|
1273
|
+
export { foo };
|
|
1274
|
+
`,
|
|
1275
|
+
filename: '/project/index.ts',
|
|
1276
|
+
},
|
|
1277
|
+
{
|
|
1278
|
+
// Type-only exports are OK
|
|
1279
|
+
code: `
|
|
1280
|
+
export type { User } from './types';
|
|
1281
|
+
export interface Config { port: number; }
|
|
1282
|
+
`,
|
|
1283
|
+
filename: '/project/index.ts',
|
|
1284
|
+
},
|
|
1285
|
+
],
|
|
1286
|
+
invalid: [
|
|
1287
|
+
{
|
|
1288
|
+
// Export with function definition is complex logic
|
|
1289
|
+
code: `
|
|
1290
|
+
export function processData() {
|
|
1291
|
+
return 42;
|
|
1292
|
+
}
|
|
1293
|
+
`,
|
|
1294
|
+
filename: '/project/index.ts',
|
|
1295
|
+
errors: [{ messageId: 'hasComplexLogic' }],
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
// Export with class definition is complex logic
|
|
1299
|
+
code: `
|
|
1300
|
+
export class Handler {
|
|
1301
|
+
handle() { return 'handled'; }
|
|
1302
|
+
}
|
|
1303
|
+
`,
|
|
1304
|
+
filename: '/project/index.ts',
|
|
1305
|
+
errors: [{ messageId: 'hasComplexLogic' }],
|
|
1306
|
+
},
|
|
1307
|
+
{
|
|
1308
|
+
// Export default with function is complex logic
|
|
1309
|
+
code: `
|
|
1310
|
+
export default function main() {
|
|
1311
|
+
console.log('running');
|
|
1312
|
+
}
|
|
1313
|
+
`,
|
|
1314
|
+
filename: '/project/main.ts',
|
|
1315
|
+
errors: [{ messageId: 'hasComplexLogic' }],
|
|
1316
|
+
},
|
|
1317
|
+
],
|
|
1318
|
+
});
|
|
1319
|
+
});
|
|
1320
|
+
});
|
|
1321
|
+
//# sourceMappingURL=behavior.test.js.map
|