@blumintinc/eslint-plugin-blumint 0.1.23 → 1.0.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +277 -55
  3. package/assets/logo.svg +26 -0
  4. package/jest.config.js +13 -0
  5. package/package.json +74 -25
  6. package/plugin/README.md +80 -0
  7. package/{docs → plugin/docs}/rules/class-methods-read-top-to-bottom.md +3 -1
  8. package/{docs → plugin/docs}/rules/dynamic-https-errors.md +1 -1
  9. package/{docs → plugin/docs}/rules/require-memo.md +9 -2
  10. package/plugin/package-lock.json +7662 -0
  11. package/plugin/package.json +45 -0
  12. package/tsconfig.json +79 -0
  13. package/lib/index.js +0 -69
  14. package/lib/rules/array-methods-this-context.js +0 -64
  15. package/lib/rules/class-methods-read-top-to-bottom.js +0 -72
  16. package/lib/rules/dynamic-https-errors.js +0 -57
  17. package/lib/rules/export-if-in-doubt.js +0 -69
  18. package/lib/rules/extract-global-constants.js +0 -63
  19. package/lib/rules/generic-starts-with-t.js +0 -35
  20. package/lib/rules/no-async-array-filter.js +0 -40
  21. package/lib/rules/no-async-foreach.js +0 -37
  22. package/lib/rules/no-conditional-literals-in-jsx.js +0 -61
  23. package/lib/rules/no-filter-without-return.js +0 -41
  24. package/lib/rules/no-misused-switch-case.js +0 -35
  25. package/lib/rules/no-unpinned-dependencies.js +0 -65
  26. package/lib/rules/no-useless-fragment.js +0 -46
  27. package/lib/rules/prefer-fragment-shorthand.js +0 -40
  28. package/lib/rules/prefer-type-over-interface.js +0 -45
  29. package/lib/rules/require-memo.js +0 -134
  30. package/lib/utils/ASTHelpers.js +0 -336
  31. package/lib/utils/ClassGraphBuilder.js +0 -80
  32. package/lib/utils/ClassGraphSorter.js +0 -99
  33. package/lib/utils/createRule.js +0 -6
  34. package/lib/utils/graph/ClassGraphBuilder.js +0 -80
  35. package/lib/utils/graph/ClassGraphSorter.js +0 -10
  36. package/lib/utils/graph/ClassGraphSorterReadability.js +0 -94
  37. package/lib/utils/graph.js +0 -109
  38. package/lib/utils/ruleTester.js +0 -23
  39. package/lib/utils/testClass.js +0 -45
  40. /package/{docs → plugin/docs}/rules/array-methods-this-context.md +0 -0
  41. /package/{docs → plugin/docs}/rules/export-if-in-doubt.md +0 -0
  42. /package/{docs → plugin/docs}/rules/extract-global-constants.md +0 -0
  43. /package/{docs → plugin/docs}/rules/generic-starts-with-t.md +0 -0
  44. /package/{docs → plugin/docs}/rules/no-async-array-filter.md +0 -0
  45. /package/{docs → plugin/docs}/rules/no-async-foreach.md +0 -0
  46. /package/{docs → plugin/docs}/rules/no-conditional-literals-in-jsx.md +0 -0
  47. /package/{docs → plugin/docs}/rules/no-filter-without-return.md +0 -0
  48. /package/{docs → plugin/docs}/rules/no-misused-switch-case.md +0 -0
  49. /package/{docs → plugin/docs}/rules/no-unpinned-dependencies.md +0 -0
  50. /package/{docs → plugin/docs}/rules/no-useless-fragment.md +0 -0
  51. /package/{docs → plugin/docs}/rules/prefer-fragment-shorthand.md +0 -0
  52. /package/{docs → plugin/docs}/rules/prefer-type-over-interface.md +0 -0
@@ -1,61 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.noConditionalLiteralsInJsx = void 0;
4
- /* eslint-disable @blumintinc/blumint/extract-global-constants */
5
- // import { ASTHelpers } from '../utils/ASTHelpers';
6
- const createRule_1 = require("../utils/createRule");
7
- const utils_1 = require("@typescript-eslint/utils");
8
- exports.noConditionalLiteralsInJsx = (0, createRule_1.createRule)({
9
- name: 'no-conditional-literals-in-jsx',
10
- meta: {
11
- type: 'problem',
12
- docs: {
13
- description: 'Disallow use of conditional literals in JSX code',
14
- recommended: 'error',
15
- },
16
- schema: [],
17
- messages: {
18
- unexpected: 'Conditional expression is a sibling of raw text and must be wrapped in <div> or <span>',
19
- },
20
- },
21
- defaultOptions: [],
22
- create(context) {
23
- return {
24
- // Imagine evaluating <div>text {conditional && 'string'}</div>
25
- JSXExpressionContainer(node) {
26
- // We start at the expression {conditional && 'string'}
27
- if (node.expression.type !== 'LogicalExpression') {
28
- return;
29
- }
30
- const parentChildren = node.parent && 'children' in node.parent ? node.parent.children : [];
31
- // "text" is one of the siblingTextNodes.
32
- const siblingTextNodes = parentChildren.filter((n) => {
33
- if (n.type === utils_1.TSESTree.AST_NODE_TYPES.Literal ||
34
- n.type === utils_1.TSESTree.AST_NODE_TYPES.JSXText) {
35
- return !!('value' in n && !!n.value ? `${n.value}`.trim() : false);
36
- }
37
- return false;
38
- });
39
- // If we were evaluating
40
- // <div>{property} {conditional && 'string'}</div>
41
- // Then {property} would be one of the siblingExpressionNodes
42
- const siblingExpressionNodes = parentChildren.filter((n) => n.type === 'JSXExpressionContainer' &&
43
- 'expression' in n &&
44
- (n.expression.type === 'Identifier' ||
45
- n.expression.type === 'MemberExpression'));
46
- // Operands of {conditional && 'string'} -- the conditional and the
47
- // literal. We want to make sure we have a text literal, otherwise we'd
48
- // trigger this rule on the (safe) {conditional && <div>string</div>}.
49
- const expressionOperandTypes = [
50
- node.expression.left.type,
51
- node.expression.right.type,
52
- ];
53
- if (siblingTextNodes.concat(siblingExpressionNodes).length > 0 &&
54
- expressionOperandTypes.includes(utils_1.TSESTree.AST_NODE_TYPES.Literal)) {
55
- context.report({ node, messageId: 'unexpected' });
56
- }
57
- },
58
- };
59
- },
60
- });
61
- //# sourceMappingURL=no-conditional-literals-in-jsx.js.map
@@ -1,41 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.noFilterWithoutReturn = void 0;
4
- const ASTHelpers_1 = require("../utils/ASTHelpers");
5
- const createRule_1 = require("../utils/createRule");
6
- exports.noFilterWithoutReturn = (0, createRule_1.createRule)({
7
- create(context) {
8
- return {
9
- 'CallExpression[callee.property.name="filter"]'(node) {
10
- const callback = node.arguments[0];
11
- if (callback && callback.type === 'ArrowFunctionExpression') {
12
- const { body } = callback;
13
- if (body.type !== 'BlockStatement') {
14
- // If the body is not a block statement, it's a direct return
15
- return;
16
- }
17
- if (!ASTHelpers_1.ASTHelpers.hasReturnStatement(body)) {
18
- context.report({
19
- node,
20
- messageId: 'unexpected',
21
- });
22
- }
23
- }
24
- },
25
- };
26
- },
27
- name: 'no-filter-without-return',
28
- meta: {
29
- type: 'problem',
30
- docs: {
31
- description: 'Disallow Array.filter callbacks without an explicit return (if part of a block statement)',
32
- recommended: 'error',
33
- },
34
- schema: [],
35
- messages: {
36
- unexpected: 'An array filter callback with a block statement must contain a return statement',
37
- },
38
- },
39
- defaultOptions: [],
40
- });
41
- //# sourceMappingURL=no-filter-without-return.js.map
@@ -1,35 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.noMisusedSwitchCase = void 0;
4
- const createRule_1 = require("../utils/createRule");
5
- exports.noMisusedSwitchCase = (0, createRule_1.createRule)({
6
- name: 'no-misused-switch-case',
7
- meta: {
8
- type: 'problem',
9
- docs: {
10
- description: 'Prevent misuse of logical OR in switch case statements',
11
- recommended: 'error',
12
- },
13
- schema: [],
14
- messages: {
15
- noMisusedSwitchCase: 'Avoid using logical OR in switch case. Use cascading cases instead.',
16
- },
17
- },
18
- defaultOptions: [],
19
- create(context) {
20
- return {
21
- SwitchStatement(node) {
22
- for (const switchCase of node.cases) {
23
- if (switchCase.test?.type === 'LogicalExpression' &&
24
- switchCase.test.operator === '||') {
25
- context.report({
26
- node: switchCase,
27
- messageId: 'noMisusedSwitchCase',
28
- });
29
- }
30
- }
31
- },
32
- };
33
- },
34
- });
35
- //# sourceMappingURL=no-misused-switch-case.js.map
@@ -1,65 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.noUnpinnedDependencies = void 0;
4
- const createRule_1 = require("../utils/createRule");
5
- exports.noUnpinnedDependencies = (0, createRule_1.createRule)({
6
- meta: {
7
- type: 'problem',
8
- docs: {
9
- description: 'Enforces pinned dependencies',
10
- recommended: 'error',
11
- },
12
- fixable: 'code',
13
- schema: [],
14
- messages: {
15
- unexpected: "Dependency '{{ propertyName }}' should be pinned to a specific version, but '{{ version }}' was found.",
16
- },
17
- },
18
- defaultOptions: [],
19
- name: 'no-unpinned-dependencies',
20
- create(context) {
21
- return {
22
- JSONLiteral(node) {
23
- const property = node?.parent;
24
- // const property = node.parent;
25
- const configSection = node?.parent?.parent?.parent;
26
- if (!property || !configSection) {
27
- return;
28
- }
29
- const configKey = configSection?.key;
30
- if (!configKey) {
31
- return;
32
- }
33
- // Check if we're in the "dependencies" or "devDependencies" section of package.json
34
- if (node.type === 'JSONLiteral' &&
35
- property?.type === 'JSONProperty' &&
36
- (configKey.name === 'devDependencies' ||
37
- configKey.value === 'devDependencies' ||
38
- configKey.name === 'dependencies' ||
39
- configKey.value === 'dependencies')) {
40
- // Get the version string
41
- const version = node.value;
42
- const propertyName = property.key.name ||
43
- property.key.value;
44
- // Check if the version string starts with a caret (^) or tilde (~), indicating a non-pinned version
45
- if (typeof version === 'string' &&
46
- (version.includes('^') || version.includes('~'))) {
47
- context.report({
48
- node: node,
49
- messageId: 'unexpected',
50
- data: {
51
- propertyName,
52
- version,
53
- },
54
- fix: function (fixer) {
55
- const fixed = version.replace('^', '').replace('~', '');
56
- return fixer.replaceTextRange(node.range, `"${fixed}"`);
57
- },
58
- });
59
- }
60
- }
61
- },
62
- };
63
- },
64
- });
65
- //# sourceMappingURL=no-unpinned-dependencies.js.map
@@ -1,46 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.noUselessFragment = void 0;
4
- exports.noUselessFragment = {
5
- create(context) {
6
- return {
7
- JSXFragment(node) {
8
- if (node.children.length === 1) {
9
- context.report({
10
- node,
11
- messageId: 'noUselessFragment',
12
- fix(fixer) {
13
- const sourceCode = context.getSourceCode();
14
- const openingFragment = sourceCode.getFirstToken(node);
15
- const closingFragment = sourceCode.getLastToken(node);
16
- return [
17
- fixer.removeRange([
18
- openingFragment.range[0],
19
- openingFragment.range[0] + 2,
20
- ]),
21
- fixer.removeRange([
22
- closingFragment.range[0] - 3,
23
- closingFragment.range[0],
24
- ]),
25
- ];
26
- },
27
- });
28
- }
29
- },
30
- };
31
- },
32
- meta: {
33
- type: 'suggestion',
34
- docs: {
35
- description: 'Prevent unnecessary use of React fragments',
36
- recommended: 'warn',
37
- },
38
- messages: {
39
- noUselessFragment: 'React fragment is unnecessary when wrapping a single child',
40
- },
41
- schema: [],
42
- fixable: 'code',
43
- },
44
- defaultOptions: [],
45
- };
46
- //# sourceMappingURL=no-useless-fragment.js.map
@@ -1,40 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.preferFragmentShorthand = void 0;
4
- exports.preferFragmentShorthand = {
5
- create(context) {
6
- return {
7
- JSXElement(node) {
8
- const openingElement = node.openingElement;
9
- if (openingElement.name.type === 'JSXMemberExpression' &&
10
- openingElement.name.object.type === 'JSXIdentifier' &&
11
- openingElement.name.object.name === 'React' &&
12
- openingElement.name.property.type === 'JSXIdentifier' &&
13
- openingElement.name.property.name === 'Fragment') {
14
- context.report({
15
- node,
16
- messageId: 'preferShorthand',
17
- fix: (fixer) => [
18
- fixer.replaceTextRange(openingElement.range, '<>'),
19
- fixer.replaceTextRange(node.closingElement.range, '</>'),
20
- ],
21
- });
22
- }
23
- },
24
- };
25
- },
26
- meta: {
27
- type: 'suggestion',
28
- docs: {
29
- description: 'Prefer <> shorthand for <React.Fragment>',
30
- recommended: 'warn',
31
- },
32
- messages: {
33
- preferShorthand: 'Use <> shorthand for <React.Fragment>, unless a key is required for an iterator',
34
- },
35
- schema: [],
36
- fixable: 'code',
37
- },
38
- defaultOptions: [],
39
- };
40
- //# sourceMappingURL=prefer-fragment-shorthand.js.map
@@ -1,45 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.preferTypeOverInterface = void 0;
4
- const createRule_1 = require("../utils/createRule");
5
- exports.preferTypeOverInterface = (0, createRule_1.createRule)({
6
- name: 'prefer-type-over-interface',
7
- meta: {
8
- type: 'suggestion',
9
- docs: {
10
- description: 'Prefer using type alias over interface',
11
- recommended: 'warn',
12
- },
13
- schema: [],
14
- messages: {
15
- preferType: 'Prefer using type alias over interface.',
16
- },
17
- fixable: 'code',
18
- },
19
- defaultOptions: [],
20
- create(context) {
21
- return {
22
- TSInterfaceDeclaration(node) {
23
- context.report({
24
- node,
25
- messageId: 'preferType',
26
- fix(fixer) {
27
- const sourceCode = context.getSourceCode();
28
- const openingBrace = sourceCode.getTokenAfter(node.id, {
29
- filter: (token) => token.value === '{',
30
- });
31
- const fixes = [
32
- fixer.replaceTextRange([node.range[0], node.id.range[1]], `type ${node.id.name} =`),
33
- ];
34
- if (node.extends && node.extends.length > 0 && openingBrace) {
35
- const extendsKeyword = sourceCode.getFirstTokenBetween(node.id, openingBrace, { filter: (token) => token.value === 'extends' });
36
- fixes.push(fixer.remove(extendsKeyword), fixer.insertTextBefore(openingBrace, '& '));
37
- }
38
- return fixes;
39
- },
40
- });
41
- },
42
- };
43
- },
44
- });
45
- //# sourceMappingURL=prefer-type-over-interface.js.map
@@ -1,134 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.requireMemo = void 0;
4
- const utils_1 = require("@typescript-eslint/utils");
5
- const ASTHelpers_1 = require("../utils/ASTHelpers");
6
- const isComponentExplicitlyUnmemoized = (componentName) => componentName.toLowerCase().includes('unmemoized');
7
- function isFunction(node) {
8
- return (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
9
- node.type === utils_1.AST_NODE_TYPES.FunctionExpression);
10
- }
11
- function isHigherOrderFunctionReturningJSX(node) {
12
- if (isFunction(node)) {
13
- // Check if function takes another function as an argument
14
- const hasFunctionParam = 'params' in node && node.params.some(isFunction);
15
- if (node.body && node.body.type === 'BlockStatement') {
16
- for (const statement of node.body.body) {
17
- if (statement.type === 'ReturnStatement' && statement.argument) {
18
- const returnsJSX = ASTHelpers_1.ASTHelpers.returnsJSX(statement.argument);
19
- const returnsFunction = isFunction(statement.argument);
20
- return (hasFunctionParam || returnsFunction) && returnsJSX;
21
- }
22
- }
23
- }
24
- }
25
- return false;
26
- }
27
- const isUnmemoizedArrowFunction = (parentNode) => {
28
- return (parentNode.type === 'VariableDeclarator' &&
29
- parentNode.id.type === 'Identifier' &&
30
- !isComponentExplicitlyUnmemoized(parentNode.id.name));
31
- };
32
- const isUnmemoizedFunctionComponent = (parentNode, node) => {
33
- return (node.type === 'FunctionDeclaration' &&
34
- parentNode.type === 'Program' &&
35
- node.id &&
36
- !isComponentExplicitlyUnmemoized(node.id.name));
37
- };
38
- const isUnmemoizedExportedFunctionComponent = (parentNode, node) => {
39
- return (node.type === 'FunctionDeclaration' &&
40
- parentNode.type === 'ExportNamedDeclaration' &&
41
- node.id &&
42
- !isComponentExplicitlyUnmemoized(node.id.name));
43
- };
44
- function checkFunction(context, node) {
45
- const fileName = context.getFilename();
46
- if (!fileName.endsWith('.tsx')) {
47
- return;
48
- }
49
- if (isHigherOrderFunctionReturningJSX(node)) {
50
- return;
51
- }
52
- const parentNode = node.parent;
53
- if (node.parent.type === 'CallExpression') {
54
- return;
55
- }
56
- if (ASTHelpers_1.ASTHelpers.returnsJSX(node.body) && ASTHelpers_1.ASTHelpers.hasParameters(node)) {
57
- const results = [
58
- isUnmemoizedArrowFunction,
59
- isUnmemoizedFunctionComponent,
60
- isUnmemoizedExportedFunctionComponent,
61
- ].map((fn) => fn(parentNode, node));
62
- if (results.some((result) => !!result)) {
63
- context.report({
64
- node,
65
- messageId: 'requireMemo',
66
- fix: results[2] || results[1]
67
- ? function fix(fixer) {
68
- const sourceCode = context.getSourceCode();
69
- let importFix = null;
70
- // Search for React import statement
71
- const importDeclarations = sourceCode.ast.body.filter((node) => node.type === 'ImportDeclaration');
72
- const reactImport = importDeclarations.find((importDeclaration) => importDeclaration.source.value === 'react');
73
- if (reactImport) {
74
- // Check if memo is already imported
75
- if (!reactImport.specifiers.some((specifier) => specifier.local.name === 'memo')) {
76
- // Add memo to existing import statement
77
- const lastSpecifier = reactImport.specifiers[reactImport.specifiers.length - 1];
78
- importFix = fixer.insertTextAfter(lastSpecifier, ', memo');
79
- }
80
- }
81
- else {
82
- // Add new import statement for memo
83
- const importStatement = "import { memo } from 'react';\n";
84
- importFix = fixer.insertTextBeforeRange([sourceCode.ast.range[0], sourceCode.ast.range[0]], importStatement);
85
- }
86
- const functionKeywordRange = [
87
- node.range[0],
88
- node.range[0] + 'function'.length,
89
- ];
90
- const functionKeywordReplacement = `const ${node.id.name} = memo(`;
91
- // Step 3: Rename function
92
- const functionNameReplacement = `function ${node.id.name}Unmemoized`;
93
- const fixes = [
94
- fixer.replaceTextRange(functionKeywordRange, functionKeywordReplacement),
95
- fixer.insertTextAfterRange([node.range[1], node.range[1]], ')'),
96
- fixer.replaceTextRange([node.id.range[0] - 1, node.id.range[1]], functionNameReplacement),
97
- ];
98
- if (importFix) {
99
- fixes.push(importFix);
100
- }
101
- return fixes;
102
- }
103
- : undefined,
104
- });
105
- }
106
- }
107
- }
108
- exports.requireMemo = {
109
- create: (context) => ({
110
- ArrowFunctionExpression(node) {
111
- checkFunction(context, node);
112
- },
113
- FunctionDeclaration(node) {
114
- checkFunction(context, node);
115
- },
116
- FunctionExpression(node) {
117
- checkFunction(context, node);
118
- },
119
- }),
120
- meta: {
121
- type: 'problem',
122
- docs: {
123
- description: 'React components must be memoized',
124
- recommended: 'error',
125
- },
126
- messages: {
127
- requireMemo: 'Component definition not wrapped in React.memo()',
128
- },
129
- schema: [],
130
- fixable: 'code',
131
- },
132
- defaultOptions: [],
133
- };
134
- //# sourceMappingURL=require-memo.js.map