eslint-plugin-th-rules 2.7.1 → 3.0.0
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/README.md +171 -11
- package/dist/configs/bundles/recommended-react.d.ts +199 -0
- package/dist/configs/bundles/recommended-react.js +11 -0
- package/dist/configs/bundles/recommended-typescript.d.ts +190 -0
- package/dist/configs/bundles/recommended-typescript.js +6 -0
- package/dist/configs/bundles/recommended.d.ts +190 -0
- package/dist/configs/bundles/recommended.js +8 -0
- package/dist/configs/core/base.d.ts +170 -0
- package/dist/configs/core/base.js +23 -0
- package/dist/configs/core/react.d.ts +6 -0
- package/dist/configs/core/react.js +8 -0
- package/dist/configs/core/typescript.d.ts +2 -0
- package/dist/configs/core/typescript.js +19 -0
- package/dist/configs/externals/base.d.ts +15 -0
- package/dist/configs/externals/base.js +16 -0
- package/dist/configs/externals/opinionated.d.ts +10 -0
- package/dist/configs/externals/opinionated.js +12 -0
- package/dist/index.d.ts +1170 -0
- package/dist/index.js +21 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugin.js +14 -0
- package/dist/rules/no-boolean-coercion.d.ts +5 -0
- package/dist/rules/no-boolean-coercion.js +98 -0
- package/dist/rules/no-comments.d.ts +11 -0
- package/dist/rules/no-comments.js +83 -0
- package/dist/rules/no-default-export.d.ts +5 -0
- package/dist/rules/no-default-export.js +61 -0
- package/dist/rules/no-destructuring.d.ts +8 -0
- package/dist/rules/no-destructuring.js +121 -0
- package/dist/rules/prefer-is-empty.d.ts +5 -0
- package/dist/rules/prefer-is-empty.js +101 -0
- package/dist/rules/schemas-in-schemas-file.d.ts +9 -0
- package/dist/rules/schemas-in-schemas-file.js +141 -0
- package/dist/rules/top-level-functions.d.ts +5 -0
- package/dist/rules/top-level-functions.js +153 -0
- package/dist/rules/types-in-dts.d.ts +8 -0
- package/dist/rules/types-in-dts.js +76 -0
- package/package.json +25 -14
- package/.github/dependabot.yml +0 -15
- package/.github/workflows/codecov.yml +0 -26
- package/.github/workflows/codeql.yml +0 -82
- package/.github/workflows/dependency-review.yml +0 -20
- package/.github/workflows/main.yml +0 -43
- package/.github/workflows/scorecard.yml +0 -72
- package/.github/workflows/snyk-security.yml +0 -67
- package/.releaserc +0 -13
- package/.vscode/settings.json +0 -8
- package/.yarn/releases/yarn-4.12.0.cjs +0 -942
- package/.yarnrc.yml +0 -3
- package/CHANGELOG.md +0 -628
- package/SECURITY.md +0 -48
- package/docs/rules/no-boolean-coercion.md +0 -9
- package/docs/rules/no-comments.md +0 -50
- package/docs/rules/no-default-export.md +0 -26
- package/docs/rules/no-destructuring.md +0 -40
- package/docs/rules/prefer-is-empty.md +0 -9
- package/docs/rules/schemas-in-schemas-file.md +0 -170
- package/docs/rules/top-level-functions.md +0 -48
- package/docs/rules/types-in-dts.md +0 -112
- package/renovate.json +0 -3
- package/scripts/verify.mjs +0 -16
- package/src/index.js +0 -144
- package/src/rules/no-boolean-coercion.js +0 -124
- package/src/rules/no-comments.js +0 -94
- package/src/rules/no-default-export.js +0 -64
- package/src/rules/no-destructuring.js +0 -114
- package/src/rules/prefer-is-empty.js +0 -104
- package/src/rules/schemas-in-schemas-file.js +0 -191
- package/src/rules/top-level-functions.js +0 -200
- package/src/rules/types-in-dts.js +0 -94
- package/tests/no-boolean-coercion.test.ts +0 -83
- package/tests/prefer-is-empty.test.ts +0 -148
- package/tsconfig.json +0 -22
- package/xo.config.ts +0 -2
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { rules } from './plugin.js';
|
|
2
|
+
// Public bundles
|
|
3
|
+
import { recommended } from './configs/bundles/recommended.js';
|
|
4
|
+
import { recommendedReact } from './configs/bundles/recommended-react.js';
|
|
5
|
+
import { recommendedTypescript } from './configs/bundles/recommended-typescript.js';
|
|
6
|
+
// Internal layers (named exports only)
|
|
7
|
+
export { coreBase } from './configs/core/base.js';
|
|
8
|
+
export { coreTypescript } from './configs/core/typescript.js';
|
|
9
|
+
export { coreReact } from './configs/core/react.js';
|
|
10
|
+
export { externalsBase } from './configs/externals/base.js';
|
|
11
|
+
export { externalsOpinionated } from './configs/externals/opinionated.js';
|
|
12
|
+
export const configs = {
|
|
13
|
+
recommended,
|
|
14
|
+
'recommended-react': recommendedReact,
|
|
15
|
+
'recommended-typescript': recommendedTypescript,
|
|
16
|
+
};
|
|
17
|
+
export default {
|
|
18
|
+
rules,
|
|
19
|
+
configs,
|
|
20
|
+
};
|
|
21
|
+
export { rules } from './plugin.js';
|
package/dist/plugin.d.ts
ADDED
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { readdirSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = dirname(__filename);
|
|
6
|
+
const rulesDir = join(__dirname, 'rules');
|
|
7
|
+
const ruleFiles = readdirSync(rulesDir).filter(file => file.endsWith('.js'));
|
|
8
|
+
export const rules = Object.fromEntries(await Promise.all(ruleFiles.map(async (file) => {
|
|
9
|
+
const ruleName = file.replace('.js', '');
|
|
10
|
+
const ruleModule = await import(`./rules/${file}`);
|
|
11
|
+
return [ruleName, ruleModule.default];
|
|
12
|
+
})));
|
|
13
|
+
export const plugin = { rules };
|
|
14
|
+
export default plugin;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { ESLintUtils, } from '@typescript-eslint/utils';
|
|
2
|
+
const createRule = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh2001/eslint-plugin-th-rules/blob/main/docs/rules/no-boolean-coercion.md');
|
|
3
|
+
export default createRule({
|
|
4
|
+
name: 'no-boolean-coercion',
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'problem',
|
|
7
|
+
docs: {
|
|
8
|
+
description: 'Disallow Boolean(value) or !!value. Enforce explicit checks: !_.isNil(value) for scalars and !_.isEmpty(value) for strings, arrays, and objects.',
|
|
9
|
+
},
|
|
10
|
+
hasSuggestions: true,
|
|
11
|
+
schema: [],
|
|
12
|
+
messages: {
|
|
13
|
+
useIsEmpty: 'Boolean coercion is not allowed. Use !_.isEmpty(value) for strings, arrays, and objects.',
|
|
14
|
+
useIsNil: 'Boolean coercion is not allowed. Use !_.isNil(value) for scalar values.',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
defaultOptions: [],
|
|
18
|
+
create(context) {
|
|
19
|
+
const { sourceCode } = context;
|
|
20
|
+
const services = ESLintUtils.getParserServices(context);
|
|
21
|
+
const checker = services?.program?.getTypeChecker?.();
|
|
22
|
+
// --------------------------------------------------------------------
|
|
23
|
+
// Helpers
|
|
24
|
+
// --------------------------------------------------------------------
|
|
25
|
+
function isBooleanCall(node) {
|
|
26
|
+
return (node.type === 'CallExpression'
|
|
27
|
+
&& node.callee.type === 'Identifier'
|
|
28
|
+
&& node.callee.name === 'Boolean'
|
|
29
|
+
&& node.arguments.length === 1);
|
|
30
|
+
}
|
|
31
|
+
function isDoubleNegation(node) {
|
|
32
|
+
return (node.type === 'UnaryExpression'
|
|
33
|
+
&& node.operator === '!'
|
|
34
|
+
&& node.argument.type === 'UnaryExpression'
|
|
35
|
+
&& node.argument.operator === '!');
|
|
36
|
+
}
|
|
37
|
+
function isCollectionLikeByTS(node) {
|
|
38
|
+
if (!checker || !services.esTreeNodeToTSNodeMap) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
|
|
42
|
+
if (!tsNode) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
46
|
+
const typeString = checker.typeToString(type);
|
|
47
|
+
return (typeString.includes('[]')
|
|
48
|
+
|| typeString === 'string'
|
|
49
|
+
|| typeString === 'object'
|
|
50
|
+
|| typeString.startsWith('Array<')
|
|
51
|
+
|| typeString.startsWith('ReadonlyArray<'));
|
|
52
|
+
}
|
|
53
|
+
function isCollectionLikeBySyntax(node) {
|
|
54
|
+
return (node.type === 'ArrayExpression'
|
|
55
|
+
|| node.type === 'ObjectExpression'
|
|
56
|
+
|| (node.type === 'Literal' && typeof node.value === 'string'));
|
|
57
|
+
}
|
|
58
|
+
function report(node, valueNode) {
|
|
59
|
+
const isCollection = isCollectionLikeBySyntax(valueNode)
|
|
60
|
+
|| isCollectionLikeByTS(valueNode);
|
|
61
|
+
const suggestedFn = isCollection ? '_.isEmpty' : '_.isNil';
|
|
62
|
+
const replacement = `!${suggestedFn}(${sourceCode.getText(valueNode)})`;
|
|
63
|
+
context.report({
|
|
64
|
+
node,
|
|
65
|
+
messageId: isCollection ? 'useIsEmpty' : 'useIsNil',
|
|
66
|
+
suggest: [
|
|
67
|
+
{
|
|
68
|
+
messageId: isCollection ? 'useIsEmpty' : 'useIsNil',
|
|
69
|
+
fix(fixer) {
|
|
70
|
+
return fixer.replaceText(node, replacement);
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// --------------------------------------------------------------------
|
|
77
|
+
// Visitors
|
|
78
|
+
// --------------------------------------------------------------------
|
|
79
|
+
return {
|
|
80
|
+
CallExpression(node) {
|
|
81
|
+
if (isBooleanCall(node)) {
|
|
82
|
+
const arg = node.arguments[0];
|
|
83
|
+
if (arg) {
|
|
84
|
+
report(node, arg);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
UnaryExpression(node) {
|
|
89
|
+
if (isDoubleNegation(node)) {
|
|
90
|
+
const valueNode = node.argument.argument;
|
|
91
|
+
if (valueNode) {
|
|
92
|
+
report(node, valueNode);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
type Options = [
|
|
3
|
+
{
|
|
4
|
+
allow?: string[];
|
|
5
|
+
disallow?: string[];
|
|
6
|
+
}?
|
|
7
|
+
];
|
|
8
|
+
declare const _default: ESLintUtils.RuleModule<"commentNotAllowed", Options, unknown, ESLintUtils.RuleListener> & {
|
|
9
|
+
name: string;
|
|
10
|
+
};
|
|
11
|
+
export default _default;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
const DEFAULT_ALLOWED_PATTERNS = [
|
|
3
|
+
/todo/i, // Allow TODO (case-insensitive)
|
|
4
|
+
/warning/i, // Allow WARNING (case-insensitive)
|
|
5
|
+
/error/i, // Allow ERROR (case-insensitive)
|
|
6
|
+
/info/i, // Allow INFO (case-insensitive)
|
|
7
|
+
/^\s*eslint-(disable|enable|env|globals|ignore|directive)/,
|
|
8
|
+
];
|
|
9
|
+
export default ESLintUtils.RuleCreator(() => 'https://github.com/tomerh2001/eslint-plugin-th-rules/blob/main/docs/rules/no-comments.md')({
|
|
10
|
+
name: 'no-comments',
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'problem',
|
|
13
|
+
docs: {
|
|
14
|
+
description: 'Disallow comments except for specified allowed patterns.',
|
|
15
|
+
},
|
|
16
|
+
fixable: 'code',
|
|
17
|
+
schema: [
|
|
18
|
+
{
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
allow: {
|
|
22
|
+
type: 'array',
|
|
23
|
+
items: { type: 'string' },
|
|
24
|
+
description: 'Additional patterns to allow in comments.',
|
|
25
|
+
},
|
|
26
|
+
disallow: {
|
|
27
|
+
type: 'array',
|
|
28
|
+
items: { type: 'string' },
|
|
29
|
+
description: 'Additional patterns to disallow in comments.',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
messages: {
|
|
36
|
+
commentNotAllowed: 'Comment not allowed.',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
defaultOptions: [],
|
|
40
|
+
create(context) {
|
|
41
|
+
const option = context.options[0] ?? {};
|
|
42
|
+
const userAllowedPatterns = (option.allow ?? []).map(pattern => new RegExp(pattern));
|
|
43
|
+
const userDisallowedPatterns = (option.disallow ?? []).map(pattern => new RegExp(pattern));
|
|
44
|
+
function isCommentAllowed(comment) {
|
|
45
|
+
const text = comment.value.trim();
|
|
46
|
+
// Allow JSDoc
|
|
47
|
+
if (comment.type === 'Block' && comment.value.startsWith('*')) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
for (const pattern of [
|
|
51
|
+
...DEFAULT_ALLOWED_PATTERNS,
|
|
52
|
+
...userAllowedPatterns,
|
|
53
|
+
]) {
|
|
54
|
+
if (pattern.test(text)) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const pattern of userDisallowedPatterns) {
|
|
59
|
+
if (pattern.test(text)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
Program() {
|
|
67
|
+
const sourceCode = context.getSourceCode();
|
|
68
|
+
const comments = sourceCode.getAllComments();
|
|
69
|
+
for (const comment of comments) {
|
|
70
|
+
if (!isCommentAllowed(comment)) {
|
|
71
|
+
context.report({
|
|
72
|
+
node: comment,
|
|
73
|
+
messageId: 'commentNotAllowed',
|
|
74
|
+
fix(fixer) {
|
|
75
|
+
return fixer.remove(comment);
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
3
|
+
export default ESLintUtils.RuleCreator(() => 'https://github.com/tomerh2001/eslint-plugin-th-rules/blob/main/docs/rules/no-default-export.md')({
|
|
4
|
+
name: 'no-default-export',
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'problem',
|
|
7
|
+
docs: {
|
|
8
|
+
description: 'Convert unnamed default exports to named default exports based on the file name.',
|
|
9
|
+
},
|
|
10
|
+
fixable: 'code',
|
|
11
|
+
schema: [],
|
|
12
|
+
messages: {
|
|
13
|
+
unnamed: 'Unnamed default export should be named based on the file name.',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultOptions: [],
|
|
17
|
+
create(context) {
|
|
18
|
+
function generateExportNameFromFileName(fileName) {
|
|
19
|
+
// Remove all invalid characters, replace with spaces
|
|
20
|
+
const cleaned = fileName.replaceAll(/[^a-zA-Z\d]+/g, ' ');
|
|
21
|
+
// Split into tokens
|
|
22
|
+
const parts = cleaned
|
|
23
|
+
.trim()
|
|
24
|
+
.split(/\s+/g)
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
if (parts.length === 0) {
|
|
27
|
+
return 'defaultExport';
|
|
28
|
+
}
|
|
29
|
+
// Build camelCase
|
|
30
|
+
const [first, ...rest] = parts;
|
|
31
|
+
return (first.charAt(0).toLowerCase() + first.slice(1)) + rest
|
|
32
|
+
.map(p => p.charAt(0).toUpperCase() + p.slice(1))
|
|
33
|
+
.join('');
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
ExportDefaultDeclaration(node) {
|
|
37
|
+
// 1. skip `export default Foo`
|
|
38
|
+
if (node.declaration.type === 'Identifier') {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// 2. skip named function/class: `export default function Foo() {}`
|
|
42
|
+
if ('id' in node.declaration && node.declaration.id != null) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const fileName = context.getFilename();
|
|
46
|
+
const base = path.basename(fileName, path.extname(fileName));
|
|
47
|
+
const exportName = generateExportNameFromFileName(base);
|
|
48
|
+
context.report({
|
|
49
|
+
node,
|
|
50
|
+
messageId: 'unnamed',
|
|
51
|
+
fix(fixer) {
|
|
52
|
+
const sourceCode = context.getSourceCode();
|
|
53
|
+
const declText = sourceCode.getText(node.declaration);
|
|
54
|
+
const replacement = `const ${exportName} = ${declText};\nexport default ${exportName};`;
|
|
55
|
+
return fixer.replaceText(node, replacement);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
declare const _default: ESLintUtils.RuleModule<"tooDeep" | "tooMany" | "tooLong", [{
|
|
3
|
+
maximumDestructuredVariables: number;
|
|
4
|
+
maximumLineLength: number;
|
|
5
|
+
}], unknown, ESLintUtils.RuleListener> & {
|
|
6
|
+
name: string;
|
|
7
|
+
};
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
const MAX_TAB_COUNT = 3;
|
|
3
|
+
export default ESLintUtils.RuleCreator(() => 'https://github.com/tomerh2001/eslint-plugin-th-rules/blob/main/docs/rules/no-destructuring.md')({
|
|
4
|
+
name: 'no-destructuring',
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'problem',
|
|
7
|
+
docs: {
|
|
8
|
+
description: 'Disallow destructuring that does not meet certain conditions.',
|
|
9
|
+
},
|
|
10
|
+
schema: [
|
|
11
|
+
{
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
maximumDestructuredVariables: { type: 'integer', minimum: 0 },
|
|
15
|
+
maximumLineLength: { type: 'integer', minimum: 0 },
|
|
16
|
+
},
|
|
17
|
+
additionalProperties: false,
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
messages: {
|
|
21
|
+
tooDeep: 'Destructuring at a nesting level above {{max}} is not allowed; found {{actual}} levels of nesting.',
|
|
22
|
+
tooMany: 'Destructuring of more than {{max}} variables is not allowed.',
|
|
23
|
+
tooLong: 'Destructuring spanning a line exceeding {{max}} characters is not allowed.',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
defaultOptions: [
|
|
27
|
+
{
|
|
28
|
+
maximumDestructuredVariables: 2,
|
|
29
|
+
maximumLineLength: 100,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
create(context, [options]) {
|
|
33
|
+
const MAX_VARIABLES = options.maximumDestructuredVariables ?? 2;
|
|
34
|
+
const MAX_LINE_LENGTH = options.maximumLineLength ?? 100;
|
|
35
|
+
const sourceCode = context.getSourceCode();
|
|
36
|
+
function reportIfNeeded(patternNode, reportNode = patternNode) {
|
|
37
|
+
if (patternNode?.type !== 'ObjectPattern'
|
|
38
|
+
|| !patternNode.loc) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const startLine = patternNode.loc.start.line;
|
|
42
|
+
const endLine = patternNode.loc.end.line;
|
|
43
|
+
const lineText = sourceCode.lines[startLine - 1] ?? '';
|
|
44
|
+
const indentCount = lineText.search(/\S|$/);
|
|
45
|
+
const propertyCount = (patternNode).properties
|
|
46
|
+
?.length ?? 0;
|
|
47
|
+
let maxSpannedLineLength = 0;
|
|
48
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
49
|
+
const t = sourceCode.lines[i - 1] ?? '';
|
|
50
|
+
if (t.length > maxSpannedLineLength) {
|
|
51
|
+
maxSpannedLineLength = t.length;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (indentCount > MAX_TAB_COUNT) {
|
|
55
|
+
context.report({
|
|
56
|
+
node: reportNode,
|
|
57
|
+
messageId: 'tooDeep',
|
|
58
|
+
data: {
|
|
59
|
+
max: MAX_TAB_COUNT,
|
|
60
|
+
actual: indentCount,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (propertyCount > MAX_VARIABLES) {
|
|
65
|
+
context.report({
|
|
66
|
+
node: reportNode,
|
|
67
|
+
messageId: 'tooMany',
|
|
68
|
+
data: {
|
|
69
|
+
max: MAX_VARIABLES,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (maxSpannedLineLength > MAX_LINE_LENGTH) {
|
|
74
|
+
context.report({
|
|
75
|
+
node: reportNode,
|
|
76
|
+
messageId: 'tooLong',
|
|
77
|
+
data: {
|
|
78
|
+
max: MAX_LINE_LENGTH,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function checkParameters(parameters) {
|
|
84
|
+
for (const p of parameters || []) {
|
|
85
|
+
if (!p) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// (...args = {}) pattern
|
|
89
|
+
if (p.type === 'AssignmentPattern') {
|
|
90
|
+
reportIfNeeded(p.left, p);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
reportIfNeeded(p, p);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
VariableDeclarator(node) {
|
|
98
|
+
reportIfNeeded(node.id, node);
|
|
99
|
+
},
|
|
100
|
+
FunctionDeclaration(node) {
|
|
101
|
+
checkParameters(node.params);
|
|
102
|
+
},
|
|
103
|
+
FunctionExpression(node) {
|
|
104
|
+
checkParameters(node.params);
|
|
105
|
+
},
|
|
106
|
+
ArrowFunctionExpression(node) {
|
|
107
|
+
checkParameters(node.params);
|
|
108
|
+
},
|
|
109
|
+
MethodDefinition(node) {
|
|
110
|
+
if (node.value?.params) {
|
|
111
|
+
checkParameters(node.value.params);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
TSDeclareFunction(node) {
|
|
115
|
+
if (node.params) {
|
|
116
|
+
checkParameters(node.params);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
+
export default ESLintUtils.RuleCreator(() => "https://github.com/tomerh2001/eslint-plugin-th-rules/blob/main/docs/rules/prefer-is-empty.md")({
|
|
3
|
+
name: "prefer-is-empty",
|
|
4
|
+
meta: {
|
|
5
|
+
type: "problem",
|
|
6
|
+
docs: {
|
|
7
|
+
description: "Require _.isEmpty instead of length comparisons."
|
|
8
|
+
},
|
|
9
|
+
hasSuggestions: true,
|
|
10
|
+
schema: [],
|
|
11
|
+
messages: {
|
|
12
|
+
useIsEmpty: "Use _.isEmpty({{collection}}) instead of checking {{collection}}.length {{operator}} {{value}}."
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
defaultOptions: [],
|
|
16
|
+
create(context) {
|
|
17
|
+
const sourceCode = context.getSourceCode();
|
|
18
|
+
function isLengthAccess(node) {
|
|
19
|
+
return (!!node &&
|
|
20
|
+
node.type === "MemberExpression" &&
|
|
21
|
+
node.property.type === "Identifier" &&
|
|
22
|
+
node.property.name === "length" &&
|
|
23
|
+
node.computed === false);
|
|
24
|
+
}
|
|
25
|
+
function isNumericLiteral(node) {
|
|
26
|
+
return (!!node &&
|
|
27
|
+
node.type === "Literal" &&
|
|
28
|
+
typeof node.value === "number");
|
|
29
|
+
}
|
|
30
|
+
function report(node, collectionNode, operator, value, isEmptyCheck) {
|
|
31
|
+
const collectionText = sourceCode.getText(collectionNode.object);
|
|
32
|
+
const replacement = isEmptyCheck
|
|
33
|
+
? `_.isEmpty(${collectionText})`
|
|
34
|
+
: `!_.isEmpty(${collectionText})`;
|
|
35
|
+
context.report({
|
|
36
|
+
node,
|
|
37
|
+
messageId: "useIsEmpty",
|
|
38
|
+
data: {
|
|
39
|
+
collection: collectionText,
|
|
40
|
+
operator,
|
|
41
|
+
value
|
|
42
|
+
},
|
|
43
|
+
suggest: [
|
|
44
|
+
{
|
|
45
|
+
messageId: "useIsEmpty",
|
|
46
|
+
data: {
|
|
47
|
+
collection: collectionText,
|
|
48
|
+
operator,
|
|
49
|
+
value
|
|
50
|
+
},
|
|
51
|
+
fix(fixer) {
|
|
52
|
+
return fixer.replaceText(node, replacement);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
BinaryExpression(node) {
|
|
60
|
+
const { left, right, operator } = node;
|
|
61
|
+
//
|
|
62
|
+
// Case 1: values.length <op> N
|
|
63
|
+
//
|
|
64
|
+
if (isLengthAccess(left) && isNumericLiteral(right)) {
|
|
65
|
+
const value = right.value;
|
|
66
|
+
// EMPTY checks
|
|
67
|
+
if ((operator === "===" && value === 0) ||
|
|
68
|
+
(operator === "<=" && value === 0) ||
|
|
69
|
+
(operator === "<" && value === 1)) {
|
|
70
|
+
report(node, left, operator, value, true);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// NOT EMPTY checks
|
|
74
|
+
if ((operator === ">" && value === 0) ||
|
|
75
|
+
(operator === ">=" && value === 1) ||
|
|
76
|
+
((operator === "!=" || operator === "!==") && value === 0)) {
|
|
77
|
+
report(node, left, operator, value, false);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//
|
|
81
|
+
// Case 2: N <op> values.length (reverse order)
|
|
82
|
+
//
|
|
83
|
+
if (isNumericLiteral(left) && isLengthAccess(right)) {
|
|
84
|
+
const value = left.value;
|
|
85
|
+
// EMPTY checks
|
|
86
|
+
if ((operator === "===" && value === 0) ||
|
|
87
|
+
(operator === ">=" && value === 0) ||
|
|
88
|
+
(operator === ">" && value === 0)) {
|
|
89
|
+
report(node, right, operator, value, true);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// NOT EMPTY checks
|
|
93
|
+
if ((operator === "<" && value === 1) ||
|
|
94
|
+
(operator === "<=" && value === 0)) {
|
|
95
|
+
report(node, right, operator, value, false);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
declare const _default: ESLintUtils.RuleModule<"moveSchema", [{
|
|
3
|
+
allowedSuffixes: string[];
|
|
4
|
+
onlyWhenAssigned: boolean;
|
|
5
|
+
allowInTests: boolean;
|
|
6
|
+
}], unknown, ESLintUtils.RuleListener> & {
|
|
7
|
+
name: string;
|
|
8
|
+
};
|
|
9
|
+
export default _default;
|