@blumintinc/eslint-plugin-blumint 0.1.14 → 0.1.16

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 CHANGED
@@ -64,7 +64,7 @@ Or use the recommended config:
64
64
  | [generic-starts-with-t](docs/rules/generic-starts-with-t.md) | Enforce TypeScript generic types to start with T | | ✅ | |
65
65
  | [no-async-array-filter](docs/rules/no-async-array-filter.md) | Disallow async callbacks for Array.filter | ✅ | | |
66
66
  | [no-async-foreach](docs/rules/no-async-foreach.md) | Disallow Array.forEach with an async callback function | ✅ | | |
67
- | [no-conditional-literals-in-jsx](docs/rules/no-conditional-literals-in-jsx.md) | Disallow use of conditional literals in JSX code | | | |
67
+ | [no-conditional-literals-in-jsx](docs/rules/no-conditional-literals-in-jsx.md) | Disallow use of conditional literals in JSX code || | |
68
68
  | [no-filter-without-return](docs/rules/no-filter-without-return.md) | Disallow Array.filter callbacks without an explicit return (if part of a block statement) | ✅ | | |
69
69
  | [no-misused-switch-case](docs/rules/no-misused-switch-case.md) | Prevent misuse of logical OR in switch case statements | ✅ | | |
70
70
  | [no-unpinned-dependencies](docs/rules/no-unpinned-dependencies.md) | Enforces pinned dependencies | ✅ | | 🔧 |
@@ -1,5 +1,7 @@
1
1
  # Disallow use of conditional literals in JSX code (`@blumintinc/blumint/no-conditional-literals-in-jsx`)
2
2
 
3
+ 💼 This rule is enabled in the ✅ `recommended` config.
4
+
3
5
  <!-- end auto-generated rule header -->
4
6
 
5
7
  This rule disallows the use of conditional literals in JSX.
@@ -0,0 +1,29 @@
1
+ # Requires memoization of react components (`@blumintinc/blumint/require-memo`)
2
+
3
+ <!-- end auto-generated rule header -->
4
+
5
+ This rule enforces that React components (that are not pure) to be memoized. If the component name includes "Unmemoized", then this rule is ignored.
6
+
7
+ ## Rule Details
8
+
9
+ Examples of **incorrect** code for this rule:
10
+
11
+ ```jsx
12
+ const Component = ({foo, bar}) => return (
13
+ <SomeOtherComponent>{foo}{bar}</SomeOtherComponent>
14
+ )
15
+ ```
16
+
17
+ Examples of **correct** code for this rule:
18
+
19
+ ```jsx
20
+ const MemoizedComponent = memo(function BigComponent({foo, bar}) {
21
+ return (
22
+ <SomeOtherComponent>{foo}{bar}</SomeOtherComponent>
23
+ )
24
+ })
25
+ const ComponentUnmemoized = ({foo, bar}) => return (
26
+ <SomeOtherComponent>{foo}{bar}</SomeOtherComponent>
27
+ )
28
+ ```
29
+
package/lib/index.js CHANGED
@@ -13,10 +13,11 @@ const no_unpinned_dependencies_1 = require("./rules/no-unpinned-dependencies");
13
13
  const no_useless_fragment_1 = require("./rules/no-useless-fragment");
14
14
  const prefer_fragment_shorthand_1 = require("./rules/prefer-fragment-shorthand");
15
15
  const prefer_type_over_interface_1 = require("./rules/prefer-type-over-interface");
16
+ const require_memo_1 = require("./rules/require-memo");
16
17
  module.exports = {
17
18
  meta: {
18
19
  name: '@blumintinc/eslint-plugin-blumint',
19
- version: '0.1.14',
20
+ version: '0.1.16',
20
21
  },
21
22
  parseOptions: {
22
23
  ecmaVersion: 2020,
@@ -38,6 +39,7 @@ module.exports = {
38
39
  '@blumintinc/blumint/no-useless-fragment': 'warn',
39
40
  '@blumintinc/blumint/prefer-fragment-shorthand': 'warn',
40
41
  '@blumintinc/blumint/prefer-type-over-interface': 'warn',
42
+ '@blumintinc/blumint/require-memo': 'error',
41
43
  },
42
44
  },
43
45
  },
@@ -55,6 +57,7 @@ module.exports = {
55
57
  'no-useless-fragment': no_useless_fragment_1.noUselessFragment,
56
58
  'prefer-fragment-shorthand': prefer_fragment_shorthand_1.preferFragmentShorthand,
57
59
  'prefer-type-over-interface': prefer_type_over_interface_1.preferTypeOverInterface,
60
+ 'require-memo': require_memo_1.requireMemo,
58
61
  },
59
62
  };
60
63
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requireMemo = void 0;
4
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
5
+ const isComponentExplicitlyUnmemoized = (componentName) => componentName.toLowerCase().includes('unmemoized');
6
+ function isMemoCallExpression(node) {
7
+ if (node.type !== 'CallExpression')
8
+ return false;
9
+ if (node.callee.type === 'MemberExpression') {
10
+ const { callee: { object, property }, } = node;
11
+ if (object.type === 'Identifier' &&
12
+ property.type === 'Identifier' &&
13
+ object.name === 'React' &&
14
+ property.name === 'memo') {
15
+ return true;
16
+ }
17
+ }
18
+ else if (node.callee.type === 'Identifier' && node.callee.name === 'memo') {
19
+ return true;
20
+ }
21
+ return false;
22
+ }
23
+ function isHigherOrderFunctionReturningJSX(node) {
24
+ if (node.type === 'ArrowFunctionExpression' ||
25
+ node.type === 'FunctionExpression') {
26
+ if (node.body.type === 'ArrowFunctionExpression' ||
27
+ node.body.type === 'FunctionExpression') {
28
+ return ASTHelpers_1.ASTHelpers.returnsJSX(node.body.body);
29
+ }
30
+ }
31
+ return false;
32
+ }
33
+ function checkFunction(context, node) {
34
+ const fileName = context.getFilename();
35
+ if (!fileName.endsWith('.tsx')) {
36
+ return;
37
+ }
38
+ if (isHigherOrderFunctionReturningJSX(node)) {
39
+ return;
40
+ }
41
+ let currentNode = node.parent;
42
+ while (currentNode.type === 'CallExpression') {
43
+ if (isMemoCallExpression(currentNode)) {
44
+ return;
45
+ }
46
+ currentNode = currentNode.parent;
47
+ }
48
+ if (ASTHelpers_1.ASTHelpers.returnsJSX(node.body) && ASTHelpers_1.ASTHelpers.hasParameters(node)) {
49
+ if (currentNode.type === 'VariableDeclarator' &&
50
+ currentNode.id.type === 'Identifier' &&
51
+ !isComponentExplicitlyUnmemoized(currentNode.id.name)) {
52
+ context.report({ node, messageId: 'requireMemo' });
53
+ }
54
+ else if (node.type === 'FunctionDeclaration' &&
55
+ currentNode.type === 'Program' &&
56
+ node.id &&
57
+ !isComponentExplicitlyUnmemoized(node.id.name)) {
58
+ context.report({ node, messageId: 'requireMemo' });
59
+ }
60
+ }
61
+ }
62
+ exports.requireMemo = {
63
+ create: (context) => ({
64
+ ArrowFunctionExpression(node) {
65
+ checkFunction(context, node);
66
+ },
67
+ FunctionDeclaration(node) {
68
+ checkFunction(context, node);
69
+ },
70
+ FunctionExpression(node) {
71
+ checkFunction(context, node);
72
+ },
73
+ }),
74
+ meta: {
75
+ type: 'problem',
76
+ docs: {
77
+ description: 'React components must be memoized',
78
+ recommended: 'error',
79
+ },
80
+ messages: {
81
+ requireMemo: 'Component definition not wrapped in React.memo()',
82
+ },
83
+ schema: [],
84
+ fixable: 'code',
85
+ },
86
+ defaultOptions: [],
87
+ };
88
+ //# sourceMappingURL=require-memo.js.map
@@ -142,6 +142,23 @@ class ASTHelpers {
142
142
  }
143
143
  return false;
144
144
  }
145
+ static returnsJSX(node) {
146
+ if (node.type === 'JSXElement') {
147
+ return true;
148
+ }
149
+ if (node.type === 'BlockStatement') {
150
+ for (const statement of node.body) {
151
+ if (statement.type === 'ReturnStatement' &&
152
+ statement.argument?.type === 'JSXElement') {
153
+ return true;
154
+ }
155
+ }
156
+ }
157
+ return false;
158
+ }
159
+ static hasParameters(node) {
160
+ return node.params && node.params.length > 0;
161
+ }
145
162
  }
146
163
  exports.ASTHelpers = ASTHelpers;
147
164
  //# sourceMappingURL=ASTHelpers.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "Custom eslint rules for use at BluMint",
5
5
  "keywords": [
6
6
  "eslint",
@@ -1,59 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.noConditionalLiterals = void 0;
4
- /* eslint-disable @blumintinc/blumint/extract-global-constants */
5
- // import { ASTHelpers } from '../utils/ASTHelpers';
6
- const utils_1 = require("@typescript-eslint/utils");
7
- exports.noConditionalLiterals = {
8
- create(context) {
9
- return {
10
- // Imagine evaluating <div>text {conditional && 'string'}</div>
11
- JSXExpressionContainer(node) {
12
- // We start at the expression {conditional && 'string'}
13
- if (node.expression.type !== 'LogicalExpression') {
14
- return;
15
- }
16
- const parentChildren = node.parent && 'children' in node.parent ? node.parent.children : [];
17
- // "text" is one of the siblingTextNodes.
18
- const siblingTextNodes = parentChildren.filter((n) => {
19
- if (n.type === utils_1.TSESTree.AST_NODE_TYPES.Literal ||
20
- n.type === utils_1.TSESTree.AST_NODE_TYPES.JSXText) {
21
- return !!('value' in n && !!n.value ? `${n.value}`.trim() : false);
22
- }
23
- return false;
24
- });
25
- // If we were evaluating
26
- // <div>{property} {conditional && 'string'}</div>
27
- // Then {property} would be one of the siblingExpressionNodes
28
- const siblingExpressionNodes = parentChildren.filter((n) => n.type === 'JSXExpressionContainer' &&
29
- 'expression' in n &&
30
- (n.expression.type === 'Identifier' ||
31
- n.expression.type === 'MemberExpression'));
32
- // Operands of {conditional && 'string'} -- the conditional and the
33
- // literal. We want to make sure we have a text literal, otherwise we'd
34
- // trigger this rule on the (safe) {conditional && <div>string</div>}.
35
- const expressionOperandTypes = [
36
- node.expression.left.type,
37
- node.expression.right.type,
38
- ];
39
- if (siblingTextNodes.concat(siblingExpressionNodes).length > 0 &&
40
- expressionOperandTypes.includes(utils_1.TSESTree.AST_NODE_TYPES.Literal)) {
41
- context.report({ node, messageId: 'unexpected' });
42
- }
43
- },
44
- };
45
- },
46
- meta: {
47
- type: 'problem',
48
- docs: {
49
- description: 'Disallow use of conditional literals in JSX code',
50
- recommended: 'error',
51
- },
52
- messages: {
53
- unexpected: 'Conditional expression is a sibling of raw text and must be wrapped in <div> or <span>',
54
- },
55
- schema: [],
56
- },
57
- defaultOptions: [],
58
- };
59
- //# sourceMappingURL=no-conditional-literals.js.map