@blumintinc/eslint-plugin-blumint 1.0.2 → 1.0.4
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 +62 -283
- package/lib/index.d.ts +1 -0
- package/lib/index.js +69 -0
- package/lib/rules/array-methods-this-context.d.ts +3 -0
- package/lib/rules/array-methods-this-context.js +64 -0
- package/lib/rules/class-methods-read-top-to-bottom.d.ts +2 -0
- package/lib/rules/class-methods-read-top-to-bottom.js +72 -0
- package/lib/rules/dynamic-https-errors.d.ts +2 -0
- package/lib/rules/dynamic-https-errors.js +57 -0
- package/lib/rules/export-if-in-doubt.d.ts +2 -0
- package/lib/rules/export-if-in-doubt.js +69 -0
- package/lib/rules/extract-global-constants.d.ts +2 -0
- package/lib/rules/extract-global-constants.js +63 -0
- package/lib/rules/generic-starts-with-t.d.ts +2 -0
- package/lib/rules/generic-starts-with-t.js +35 -0
- package/lib/rules/no-async-array-filter.d.ts +2 -0
- package/lib/rules/no-async-array-filter.js +40 -0
- package/lib/rules/no-async-foreach.d.ts +2 -0
- package/lib/rules/no-async-foreach.js +37 -0
- package/lib/rules/no-conditional-literals-in-jsx.d.ts +2 -0
- package/lib/rules/no-conditional-literals-in-jsx.js +61 -0
- package/lib/rules/no-filter-without-return.d.ts +2 -0
- package/lib/rules/no-filter-without-return.js +41 -0
- package/lib/rules/no-misused-switch-case.d.ts +2 -0
- package/lib/rules/no-misused-switch-case.js +35 -0
- package/lib/rules/no-unpinned-dependencies.d.ts +2 -0
- package/lib/rules/no-unpinned-dependencies.js +65 -0
- package/lib/rules/no-useless-fragment.d.ts +2 -0
- package/lib/rules/no-useless-fragment.js +46 -0
- package/lib/rules/prefer-fragment-shorthand.d.ts +3 -0
- package/lib/rules/prefer-fragment-shorthand.js +40 -0
- package/lib/rules/prefer-type-over-interface.d.ts +2 -0
- package/lib/rules/prefer-type-over-interface.js +45 -0
- package/lib/rules/require-memo.d.ts +6 -0
- package/lib/rules/require-memo.js +166 -0
- package/lib/utils/ASTHelpers.d.ts +12 -0
- package/lib/utils/ASTHelpers.js +336 -0
- package/lib/utils/createRule.d.ts +2 -0
- package/lib/utils/createRule.js +6 -0
- package/lib/utils/graph/ClassGraphBuilder.d.ts +29 -0
- package/lib/utils/graph/ClassGraphBuilder.js +80 -0
- package/lib/utils/graph/ClassGraphSorter.d.ts +7 -0
- package/lib/utils/graph/ClassGraphSorter.js +10 -0
- package/lib/utils/graph/ClassGraphSorterReadability.d.ts +16 -0
- package/lib/utils/graph/ClassGraphSorterReadability.js +94 -0
- package/lib/utils/ruleTester.d.ts +5 -0
- package/lib/utils/ruleTester.js +23 -0
- package/package.json +33 -13
- package/CHANGELOG.md +0 -63
- package/assets/logo.svg +0 -26
- package/jest.config.js +0 -13
- package/plugin/README.md +0 -80
- package/plugin/docs/rules/array-methods-this-context.md +0 -30
- package/plugin/docs/rules/class-methods-read-top-to-bottom.md +0 -46
- package/plugin/docs/rules/dynamic-https-errors.md +0 -23
- package/plugin/docs/rules/export-if-in-doubt.md +0 -26
- package/plugin/docs/rules/extract-global-constants.md +0 -33
- package/plugin/docs/rules/generic-starts-with-t.md +0 -33
- package/plugin/docs/rules/no-async-array-filter.md +0 -32
- package/plugin/docs/rules/no-async-foreach.md +0 -41
- package/plugin/docs/rules/no-conditional-literals-in-jsx.md +0 -27
- package/plugin/docs/rules/no-filter-without-return.md +0 -43
- package/plugin/docs/rules/no-misused-switch-case.md +0 -38
- package/plugin/docs/rules/no-unpinned-dependencies.md +0 -34
- package/plugin/docs/rules/no-useless-fragment.md +0 -25
- package/plugin/docs/rules/prefer-fragment-shorthand.md +0 -25
- package/plugin/docs/rules/prefer-type-over-interface.md +0 -25
- package/plugin/docs/rules/require-memo.md +0 -36
- package/plugin/package-lock.json +0 -7662
- package/plugin/package.json +0 -45
- package/tsconfig.json +0 -79
|
@@ -0,0 +1,166 @@
|
|
|
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 isMemoImport(importPath) {
|
|
45
|
+
// Match both absolute and relative paths ending with util/memo
|
|
46
|
+
return /(?:^|\/)util\/memo$/.test(importPath);
|
|
47
|
+
}
|
|
48
|
+
function checkFunction(context, node) {
|
|
49
|
+
const fileName = context.getFilename();
|
|
50
|
+
if (!fileName.endsWith('.tsx')) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (isHigherOrderFunctionReturningJSX(node)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const parentNode = node.parent;
|
|
57
|
+
if (node.parent.type === 'CallExpression') {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (ASTHelpers_1.ASTHelpers.returnsJSX(node.body) && ASTHelpers_1.ASTHelpers.hasParameters(node)) {
|
|
61
|
+
const results = [
|
|
62
|
+
isUnmemoizedArrowFunction,
|
|
63
|
+
isUnmemoizedFunctionComponent,
|
|
64
|
+
isUnmemoizedExportedFunctionComponent,
|
|
65
|
+
].map((fn) => fn(parentNode, node));
|
|
66
|
+
if (results.some((result) => !!result)) {
|
|
67
|
+
context.report({
|
|
68
|
+
node,
|
|
69
|
+
messageId: 'requireMemo',
|
|
70
|
+
fix: results[2] || results[1]
|
|
71
|
+
? function fix(fixer) {
|
|
72
|
+
const sourceCode = context.getSourceCode();
|
|
73
|
+
let importFix = null;
|
|
74
|
+
// Search for memo import statement
|
|
75
|
+
const importDeclarations = sourceCode.ast.body.filter((node) => node.type === 'ImportDeclaration');
|
|
76
|
+
const memoImport = importDeclarations.find((importDeclaration) => isMemoImport(importDeclaration.source.value));
|
|
77
|
+
if (memoImport) {
|
|
78
|
+
// Check if memo is already imported
|
|
79
|
+
if (!memoImport.specifiers.some((specifier) => specifier.local.name === 'memo')) {
|
|
80
|
+
// Add memo to existing import statement
|
|
81
|
+
const lastSpecifier = memoImport.specifiers[memoImport.specifiers.length - 1];
|
|
82
|
+
importFix = fixer.insertTextAfter(lastSpecifier, ', memo');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Calculate relative path based on current file location
|
|
87
|
+
const currentFilePath = context.getFilename();
|
|
88
|
+
const importPath = calculateImportPath(currentFilePath);
|
|
89
|
+
// Find the first import statement to insert after
|
|
90
|
+
const firstImport = importDeclarations[0];
|
|
91
|
+
// Add new import statement for memo
|
|
92
|
+
const importStatement = `import { memo } from '${importPath}';\n`;
|
|
93
|
+
if (firstImport) {
|
|
94
|
+
// Insert after the first import with a single newline
|
|
95
|
+
importFix = fixer.insertTextAfter(firstImport, '\n' + importStatement.trim());
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// Insert at the start of the file
|
|
99
|
+
importFix = fixer.insertTextBeforeRange([sourceCode.ast.range[0], sourceCode.ast.range[0]], importStatement.trim() + '\n');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const functionKeywordRange = [
|
|
103
|
+
node.range[0],
|
|
104
|
+
node.range[0] + 'function'.length,
|
|
105
|
+
];
|
|
106
|
+
const functionKeywordReplacement = `const ${node.id.name} = memo(`;
|
|
107
|
+
// Step 3: Rename function
|
|
108
|
+
const functionNameReplacement = `function ${node.id.name}Unmemoized`;
|
|
109
|
+
const fixes = [
|
|
110
|
+
fixer.replaceTextRange(functionKeywordRange, functionKeywordReplacement),
|
|
111
|
+
fixer.insertTextAfterRange([node.range[1], node.range[1]], ')'),
|
|
112
|
+
fixer.replaceTextRange([node.id.range[0] - 1, node.id.range[1]], functionNameReplacement),
|
|
113
|
+
];
|
|
114
|
+
if (importFix) {
|
|
115
|
+
fixes.push(importFix);
|
|
116
|
+
}
|
|
117
|
+
return fixes;
|
|
118
|
+
}
|
|
119
|
+
: undefined,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function calculateImportPath(currentFilePath) {
|
|
125
|
+
// Default to absolute path if we can't calculate relative path
|
|
126
|
+
if (!currentFilePath)
|
|
127
|
+
return 'src/util/memo';
|
|
128
|
+
// Split the current file path into parts and normalize
|
|
129
|
+
const parts = currentFilePath.split(/[\\/]/); // Handle both Unix and Windows paths
|
|
130
|
+
const srcIndex = parts.indexOf('src');
|
|
131
|
+
if (srcIndex === -1) {
|
|
132
|
+
// If we're not in a src directory, use absolute path
|
|
133
|
+
return 'src/util/memo';
|
|
134
|
+
}
|
|
135
|
+
// Calculate relative path based on current file depth from src
|
|
136
|
+
// Subtract 1 from depth to exclude the filename itself
|
|
137
|
+
const depth = parts.length - (srcIndex + 1) - 1;
|
|
138
|
+
return '../'.repeat(depth) + 'util/memo';
|
|
139
|
+
}
|
|
140
|
+
exports.requireMemo = {
|
|
141
|
+
create: (context) => ({
|
|
142
|
+
ArrowFunctionExpression(node) {
|
|
143
|
+
checkFunction(context, node);
|
|
144
|
+
},
|
|
145
|
+
FunctionDeclaration(node) {
|
|
146
|
+
checkFunction(context, node);
|
|
147
|
+
},
|
|
148
|
+
FunctionExpression(node) {
|
|
149
|
+
checkFunction(context, node);
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
meta: {
|
|
153
|
+
type: 'problem',
|
|
154
|
+
docs: {
|
|
155
|
+
description: 'React components must be memoized',
|
|
156
|
+
recommended: 'error',
|
|
157
|
+
},
|
|
158
|
+
messages: {
|
|
159
|
+
requireMemo: 'Component definition not wrapped in React.memo()',
|
|
160
|
+
},
|
|
161
|
+
schema: [],
|
|
162
|
+
fixable: 'code',
|
|
163
|
+
},
|
|
164
|
+
defaultOptions: [],
|
|
165
|
+
};
|
|
166
|
+
//# sourceMappingURL=require-memo.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { Graph } from './graph/ClassGraphBuilder';
|
|
3
|
+
export declare class ASTHelpers {
|
|
4
|
+
static blockIncludesIdentifier(block: TSESTree.BlockStatement): boolean;
|
|
5
|
+
static declarationIncludesIdentifier(node: TSESTree.Node | null): boolean;
|
|
6
|
+
static classMethodDependenciesOf(node: TSESTree.Node | null, graph: Graph, className: string): string[];
|
|
7
|
+
static isNode(value: unknown): value is TSESTree.Node;
|
|
8
|
+
static hasReturnStatement(node: TSESTree.Node): boolean;
|
|
9
|
+
static isNodeExported(node: TSESTree.Node): boolean;
|
|
10
|
+
static returnsJSX(node: TSESTree.Node): boolean;
|
|
11
|
+
static hasParameters(node: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | TSESTree.FunctionDeclaration): boolean;
|
|
12
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ASTHelpers = void 0;
|
|
4
|
+
class ASTHelpers {
|
|
5
|
+
static blockIncludesIdentifier(block) {
|
|
6
|
+
for (const statement of block.body) {
|
|
7
|
+
if (ASTHelpers.declarationIncludesIdentifier(statement)) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
static declarationIncludesIdentifier(node) {
|
|
14
|
+
if (!node) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
switch (node.type) {
|
|
18
|
+
case 'TSNonNullExpression':
|
|
19
|
+
return this.declarationIncludesIdentifier(node.expression);
|
|
20
|
+
case 'ArrayPattern':
|
|
21
|
+
return node.elements.some((element) => ASTHelpers.declarationIncludesIdentifier(element));
|
|
22
|
+
case 'ObjectPattern':
|
|
23
|
+
return node.properties.some((property) => this.declarationIncludesIdentifier(property.value || null));
|
|
24
|
+
case 'AssignmentPattern':
|
|
25
|
+
return this.declarationIncludesIdentifier(node.left);
|
|
26
|
+
case 'RestElement':
|
|
27
|
+
return this.declarationIncludesIdentifier(node.argument);
|
|
28
|
+
case 'AwaitExpression':
|
|
29
|
+
return this.declarationIncludesIdentifier(node.argument);
|
|
30
|
+
case 'AssignmentExpression':
|
|
31
|
+
return (this.declarationIncludesIdentifier(node.left) ||
|
|
32
|
+
this.declarationIncludesIdentifier(node.right));
|
|
33
|
+
case 'BlockStatement':
|
|
34
|
+
return node.body.some((statement) => statement.type === 'BlockStatement' &&
|
|
35
|
+
ASTHelpers.blockIncludesIdentifier(statement));
|
|
36
|
+
case 'IfStatement':
|
|
37
|
+
return (this.declarationIncludesIdentifier(node.test) ||
|
|
38
|
+
this.declarationIncludesIdentifier(node.consequent) ||
|
|
39
|
+
this.declarationIncludesIdentifier(node.alternate));
|
|
40
|
+
case 'TSTypeAssertion':
|
|
41
|
+
return this.declarationIncludesIdentifier(node.expression);
|
|
42
|
+
case 'Identifier':
|
|
43
|
+
return true;
|
|
44
|
+
case 'SpreadElement':
|
|
45
|
+
return ASTHelpers.declarationIncludesIdentifier(node.argument);
|
|
46
|
+
case 'ChainExpression':
|
|
47
|
+
return ASTHelpers.declarationIncludesIdentifier(node.expression);
|
|
48
|
+
case 'ArrayExpression':
|
|
49
|
+
return node.elements.some((element) => element &&
|
|
50
|
+
(element.type === 'SpreadElement'
|
|
51
|
+
? ASTHelpers.declarationIncludesIdentifier(element.argument)
|
|
52
|
+
: ASTHelpers.declarationIncludesIdentifier(element)));
|
|
53
|
+
case 'ObjectExpression':
|
|
54
|
+
return node.properties.some((property) => {
|
|
55
|
+
if (property.type === 'Property') {
|
|
56
|
+
return ASTHelpers.declarationIncludesIdentifier(property.value);
|
|
57
|
+
}
|
|
58
|
+
else if (property.type === 'SpreadElement') {
|
|
59
|
+
return ASTHelpers.declarationIncludesIdentifier(property.argument);
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
});
|
|
63
|
+
case 'Property':
|
|
64
|
+
return this.declarationIncludesIdentifier(node.value);
|
|
65
|
+
case 'BinaryExpression':
|
|
66
|
+
case 'LogicalExpression':
|
|
67
|
+
return (this.declarationIncludesIdentifier(node.left) ||
|
|
68
|
+
this.declarationIncludesIdentifier(node.right));
|
|
69
|
+
case 'UnaryExpression':
|
|
70
|
+
case 'UpdateExpression':
|
|
71
|
+
return this.declarationIncludesIdentifier(node.argument);
|
|
72
|
+
case 'MemberExpression':
|
|
73
|
+
if (node.object.type === 'ThisExpression') {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return (this.declarationIncludesIdentifier(node.object) ||
|
|
77
|
+
this.declarationIncludesIdentifier(node.property));
|
|
78
|
+
case 'CallExpression':
|
|
79
|
+
case 'NewExpression':
|
|
80
|
+
// For function and constructor calls, we care about both the callee and the arguments.
|
|
81
|
+
return (this.declarationIncludesIdentifier(node.callee) ||
|
|
82
|
+
node.arguments.some((arg) => this.declarationIncludesIdentifier(arg)));
|
|
83
|
+
case 'ConditionalExpression':
|
|
84
|
+
return (this.declarationIncludesIdentifier(node.test) ||
|
|
85
|
+
this.declarationIncludesIdentifier(node.consequent) ||
|
|
86
|
+
this.declarationIncludesIdentifier(node.alternate));
|
|
87
|
+
case 'TSAsExpression':
|
|
88
|
+
return this.declarationIncludesIdentifier(node.expression);
|
|
89
|
+
default:
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
static classMethodDependenciesOf(node, graph, className) {
|
|
94
|
+
const dependencies = [];
|
|
95
|
+
if (!node) {
|
|
96
|
+
return dependencies;
|
|
97
|
+
}
|
|
98
|
+
switch (node.type) {
|
|
99
|
+
case 'MethodDefinition':
|
|
100
|
+
const functionBody = node.value.body;
|
|
101
|
+
return (functionBody?.body || [])
|
|
102
|
+
.map((statement) => ASTHelpers.classMethodDependenciesOf(statement, graph, className))
|
|
103
|
+
.flat();
|
|
104
|
+
case 'Identifier':
|
|
105
|
+
dependencies.push(node.name);
|
|
106
|
+
break;
|
|
107
|
+
case 'ExpressionStatement':
|
|
108
|
+
return ASTHelpers.classMethodDependenciesOf(node.expression, graph, className);
|
|
109
|
+
case 'MemberExpression':
|
|
110
|
+
if ((node.object.type === 'ThisExpression' &&
|
|
111
|
+
node.property.type === 'Identifier') ||
|
|
112
|
+
(node.object.type === 'Identifier' &&
|
|
113
|
+
node.object.name === className &&
|
|
114
|
+
node.property.type === 'Identifier')) {
|
|
115
|
+
dependencies.push(node.property.name);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
return [
|
|
119
|
+
...ASTHelpers.classMethodDependenciesOf(node.object, graph, className),
|
|
120
|
+
...ASTHelpers.classMethodDependenciesOf(node.property, graph, className),
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
case 'TSNonNullExpression':
|
|
125
|
+
return ASTHelpers.classMethodDependenciesOf(node.expression, graph, className);
|
|
126
|
+
case 'ArrayPattern':
|
|
127
|
+
return node.elements
|
|
128
|
+
.map((element) => ASTHelpers.classMethodDependenciesOf(element, graph, className))
|
|
129
|
+
.flat();
|
|
130
|
+
case 'ObjectPattern':
|
|
131
|
+
return node.properties
|
|
132
|
+
.map((property) => ASTHelpers.classMethodDependenciesOf(property.value || null, graph, className))
|
|
133
|
+
.flat();
|
|
134
|
+
case 'AssignmentPattern':
|
|
135
|
+
return ASTHelpers.classMethodDependenciesOf(node.left, graph, className);
|
|
136
|
+
case 'RestElement':
|
|
137
|
+
return ASTHelpers.classMethodDependenciesOf(node.argument, graph, className);
|
|
138
|
+
case 'AwaitExpression':
|
|
139
|
+
return ASTHelpers.classMethodDependenciesOf(node.argument, graph, className);
|
|
140
|
+
case 'AssignmentExpression':
|
|
141
|
+
return [
|
|
142
|
+
...ASTHelpers.classMethodDependenciesOf(node.left, graph, className),
|
|
143
|
+
...ASTHelpers.classMethodDependenciesOf(node.right, graph, className),
|
|
144
|
+
];
|
|
145
|
+
case 'BlockStatement':
|
|
146
|
+
return node.body
|
|
147
|
+
.map((statement) => ASTHelpers.classMethodDependenciesOf(statement, graph, className))
|
|
148
|
+
.flat()
|
|
149
|
+
.filter(Boolean);
|
|
150
|
+
case 'IfStatement':
|
|
151
|
+
return [
|
|
152
|
+
...ASTHelpers.classMethodDependenciesOf(node.test, graph, className),
|
|
153
|
+
...ASTHelpers.classMethodDependenciesOf(node.consequent, graph, className),
|
|
154
|
+
...ASTHelpers.classMethodDependenciesOf(node.alternate, graph, className),
|
|
155
|
+
];
|
|
156
|
+
case 'TSTypeAssertion':
|
|
157
|
+
return ASTHelpers.classMethodDependenciesOf(node.expression, graph, className);
|
|
158
|
+
case 'Identifier':
|
|
159
|
+
return dependencies;
|
|
160
|
+
case 'SpreadElement':
|
|
161
|
+
return ASTHelpers.classMethodDependenciesOf(node.argument, graph, className);
|
|
162
|
+
case 'ChainExpression':
|
|
163
|
+
return ASTHelpers.classMethodDependenciesOf(node.expression, graph, className);
|
|
164
|
+
case 'ArrayExpression':
|
|
165
|
+
return node.elements
|
|
166
|
+
.map((element) => element &&
|
|
167
|
+
(element.type === 'SpreadElement'
|
|
168
|
+
? ASTHelpers.classMethodDependenciesOf(element.argument, graph, className)
|
|
169
|
+
: ASTHelpers.classMethodDependenciesOf(element, graph, className)))
|
|
170
|
+
.flat()
|
|
171
|
+
.filter(Boolean);
|
|
172
|
+
case 'ObjectExpression':
|
|
173
|
+
return node.properties
|
|
174
|
+
.map((property) => {
|
|
175
|
+
if (property.type === 'Property') {
|
|
176
|
+
return ASTHelpers.classMethodDependenciesOf(property.value, graph, className);
|
|
177
|
+
}
|
|
178
|
+
else if (property.type === 'SpreadElement') {
|
|
179
|
+
return ASTHelpers.classMethodDependenciesOf(property.argument, graph, className);
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
})
|
|
183
|
+
.flat()
|
|
184
|
+
.filter(Boolean);
|
|
185
|
+
case 'Property':
|
|
186
|
+
return ASTHelpers.classMethodDependenciesOf(node.value, graph, className);
|
|
187
|
+
case 'BinaryExpression':
|
|
188
|
+
case 'LogicalExpression':
|
|
189
|
+
return [
|
|
190
|
+
...ASTHelpers.classMethodDependenciesOf(node.left, graph, className),
|
|
191
|
+
...ASTHelpers.classMethodDependenciesOf(node.right, graph, className),
|
|
192
|
+
];
|
|
193
|
+
case 'UnaryExpression':
|
|
194
|
+
case 'UpdateExpression':
|
|
195
|
+
return ASTHelpers.classMethodDependenciesOf(node.argument, graph, className);
|
|
196
|
+
case 'CallExpression':
|
|
197
|
+
case 'NewExpression':
|
|
198
|
+
// For function and constructor calls, we care about both the callee and the arguments.
|
|
199
|
+
return [
|
|
200
|
+
...ASTHelpers.classMethodDependenciesOf(node.callee, graph, className),
|
|
201
|
+
...node.arguments
|
|
202
|
+
.map((arg) => ASTHelpers.classMethodDependenciesOf(arg, graph, className))
|
|
203
|
+
.flat(),
|
|
204
|
+
];
|
|
205
|
+
case 'ConditionalExpression':
|
|
206
|
+
return [
|
|
207
|
+
...ASTHelpers.classMethodDependenciesOf(node.test, graph, className),
|
|
208
|
+
...ASTHelpers.classMethodDependenciesOf(node.consequent, graph, className),
|
|
209
|
+
...ASTHelpers.classMethodDependenciesOf(node.alternate, graph, className),
|
|
210
|
+
];
|
|
211
|
+
case 'TSAsExpression':
|
|
212
|
+
return ASTHelpers.classMethodDependenciesOf(node.expression, graph, className);
|
|
213
|
+
case 'VariableDeclaration':
|
|
214
|
+
return node.declarations
|
|
215
|
+
.map((declaration) => ASTHelpers.classMethodDependenciesOf(declaration, graph, className))
|
|
216
|
+
.flat()
|
|
217
|
+
.filter(Boolean);
|
|
218
|
+
case 'VariableDeclarator':
|
|
219
|
+
return ASTHelpers.classMethodDependenciesOf(node.init, graph, className);
|
|
220
|
+
case 'ForOfStatement':
|
|
221
|
+
return [
|
|
222
|
+
...ASTHelpers.classMethodDependenciesOf(node.left, graph, className),
|
|
223
|
+
...ASTHelpers.classMethodDependenciesOf(node.body, graph, className),
|
|
224
|
+
...ASTHelpers.classMethodDependenciesOf(node.right, graph, className),
|
|
225
|
+
];
|
|
226
|
+
case 'ForStatement':
|
|
227
|
+
return [node.body, node.init, node.test, node.update]
|
|
228
|
+
.map((node) => ASTHelpers.classMethodDependenciesOf(node, graph, className))
|
|
229
|
+
.flat();
|
|
230
|
+
case 'ThrowStatement':
|
|
231
|
+
return ASTHelpers.classMethodDependenciesOf(node.argument, graph, className);
|
|
232
|
+
case 'TemplateLiteral':
|
|
233
|
+
return node.expressions
|
|
234
|
+
.map((expression) => ASTHelpers.classMethodDependenciesOf(expression, graph, className))
|
|
235
|
+
.flat();
|
|
236
|
+
case 'ReturnStatement':
|
|
237
|
+
return ASTHelpers.classMethodDependenciesOf(node.argument, graph, className);
|
|
238
|
+
case 'ArrowFunctionExpression':
|
|
239
|
+
return [
|
|
240
|
+
...node.params.flatMap((param) => ASTHelpers.classMethodDependenciesOf(param, graph, className)),
|
|
241
|
+
...ASTHelpers.classMethodDependenciesOf(node.body, graph, className),
|
|
242
|
+
];
|
|
243
|
+
default:
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
// Removing duplicates
|
|
247
|
+
return [
|
|
248
|
+
...new Set(dependencies.filter((dep) => graph?.[dep]?.type !== 'property')),
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
static isNode(value) {
|
|
252
|
+
return typeof value === 'object' && value !== null && 'type' in value;
|
|
253
|
+
}
|
|
254
|
+
static hasReturnStatement(node) {
|
|
255
|
+
if (node.type === 'ReturnStatement') {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
if (node.type === 'IfStatement') {
|
|
259
|
+
const consequentHasReturn = ASTHelpers.hasReturnStatement(node.consequent);
|
|
260
|
+
const alternateHasReturn = !!node.alternate && ASTHelpers.hasReturnStatement(node.alternate);
|
|
261
|
+
return consequentHasReturn && alternateHasReturn;
|
|
262
|
+
}
|
|
263
|
+
if (node.type === 'BlockStatement') {
|
|
264
|
+
for (const statement of node.body) {
|
|
265
|
+
if (ASTHelpers.hasReturnStatement(statement)) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
for (const key in node) {
|
|
271
|
+
if (key === 'parent') {
|
|
272
|
+
continue; // Ignore the parent property
|
|
273
|
+
}
|
|
274
|
+
const value = node[key];
|
|
275
|
+
if (ASTHelpers.isNode(value)) {
|
|
276
|
+
if (ASTHelpers.hasReturnStatement(value)) {
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
static isNodeExported(node) {
|
|
284
|
+
// Checking if the node is exported as a named export.
|
|
285
|
+
if (node.parent && node.parent.type === 'ExportNamedDeclaration') {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
// Checking if the node is exported as default.
|
|
289
|
+
if (node.parent &&
|
|
290
|
+
node.parent.parent &&
|
|
291
|
+
node.parent.parent.type === 'ExportDefaultDeclaration') {
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
// Checking if the node is exported in a list of exports.
|
|
295
|
+
if (node.parent &&
|
|
296
|
+
node.parent.parent &&
|
|
297
|
+
node.parent.parent.type === 'ExportSpecifier' &&
|
|
298
|
+
node.parent.parent.exported.name === node.name) {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
static returnsJSX(node) {
|
|
304
|
+
if (node.type === 'JSXElement' || node.type === 'JSXFragment') {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
if (node.type === 'BlockStatement') {
|
|
308
|
+
for (const statement of node.body) {
|
|
309
|
+
if (statement.type === 'ReturnStatement' &&
|
|
310
|
+
(statement.argument?.type === 'JSXElement' ||
|
|
311
|
+
statement.argument?.type === 'JSXFragment')) {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
// Handle conditional returns
|
|
315
|
+
if (statement.type === 'ReturnStatement' &&
|
|
316
|
+
statement.argument?.type === 'ConditionalExpression') {
|
|
317
|
+
const conditionalExpr = statement.argument;
|
|
318
|
+
if (ASTHelpers.returnsJSX(conditionalExpr.consequent) ||
|
|
319
|
+
ASTHelpers.returnsJSX(conditionalExpr.alternate)) {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (node.type === 'ConditionalExpression') {
|
|
326
|
+
return (ASTHelpers.returnsJSX(node.consequent) ||
|
|
327
|
+
ASTHelpers.returnsJSX(node.alternate));
|
|
328
|
+
}
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
static hasParameters(node) {
|
|
332
|
+
return node.params && node.params.length > 0;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
exports.ASTHelpers = ASTHelpers;
|
|
336
|
+
//# sourceMappingURL=ASTHelpers.js.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
export declare const createRule: <TOptions extends readonly unknown[], TMessageIds extends string, TRuleListener extends import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener = import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>({ name, meta, ...rule }: Readonly<ESLintUtils.RuleWithMetaAndName<TOptions, TMessageIds, TRuleListener>>) => import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<TMessageIds, TOptions, TRuleListener>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRule = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
exports.createRule = utils_1.ESLintUtils.RuleCreator((name) => `https://github.com/BluMintInc/eslint-custom-rules/plugin/docs/rules/${name}.md`);
|
|
6
|
+
//# sourceMappingURL=createRule.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
export type GraphNode = {
|
|
3
|
+
name: string;
|
|
4
|
+
type: 'method' | 'property' | 'constructor';
|
|
5
|
+
accessibility?: TSESTree.Accessibility;
|
|
6
|
+
isStatic: boolean;
|
|
7
|
+
dependencies: string[];
|
|
8
|
+
};
|
|
9
|
+
export type Graph = Record<string, GraphNode>;
|
|
10
|
+
/**
|
|
11
|
+
* Builds a graph of class methods and properties with their dependencies from a class declaration.
|
|
12
|
+
* A dependency in this case is the name of another class method.
|
|
13
|
+
*/
|
|
14
|
+
export declare class ClassGraphBuilder {
|
|
15
|
+
private className;
|
|
16
|
+
private classBody;
|
|
17
|
+
graph: Graph;
|
|
18
|
+
private sorter;
|
|
19
|
+
constructor(className: string, classBody: TSESTree.ClassBody);
|
|
20
|
+
private buildGraph;
|
|
21
|
+
private static isClassMember;
|
|
22
|
+
private addMemberToGraph;
|
|
23
|
+
private static nodeTypeOf;
|
|
24
|
+
private static createGraphNode;
|
|
25
|
+
private static isNamedClassMethod;
|
|
26
|
+
private addDependencies;
|
|
27
|
+
get graphSorted(): GraphNode[];
|
|
28
|
+
get memberNamesSorted(): string[];
|
|
29
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ClassGraphBuilder = void 0;
|
|
4
|
+
/* eslint-disable security/detect-object-injection */
|
|
5
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
6
|
+
const ClassGraphSorterReadability_1 = require("./ClassGraphSorterReadability");
|
|
7
|
+
const ASTHelpers_1 = require("../ASTHelpers");
|
|
8
|
+
/**
|
|
9
|
+
* Builds a graph of class methods and properties with their dependencies from a class declaration.
|
|
10
|
+
* A dependency in this case is the name of another class method.
|
|
11
|
+
*/
|
|
12
|
+
class ClassGraphBuilder {
|
|
13
|
+
constructor(className, classBody) {
|
|
14
|
+
this.className = className;
|
|
15
|
+
this.classBody = classBody;
|
|
16
|
+
this.graph = {};
|
|
17
|
+
this.buildGraph();
|
|
18
|
+
// Note: extension requires injection of other sorters
|
|
19
|
+
this.sorter = new ClassGraphSorterReadability_1.ClassGraphSorterReadability(this.graph);
|
|
20
|
+
}
|
|
21
|
+
buildGraph() {
|
|
22
|
+
// NOTE: these need to be run sequentially for each member,
|
|
23
|
+
// since we need to know the class members before we can search
|
|
24
|
+
// methods for dependencies
|
|
25
|
+
this.classBody.body.forEach((member) => {
|
|
26
|
+
if (ClassGraphBuilder.isClassMember(member)) {
|
|
27
|
+
this.addMemberToGraph(member);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
this.classBody.body.forEach((member) => {
|
|
31
|
+
if (ClassGraphBuilder.isNamedClassMethod(member)) {
|
|
32
|
+
const { key } = member;
|
|
33
|
+
if (utils_1.ASTUtils.isIdentifier(key)) {
|
|
34
|
+
this.addDependencies(member, key.name);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
static isClassMember(node) {
|
|
40
|
+
return (node.type === 'MethodDefinition' || node.type === 'PropertyDefinition');
|
|
41
|
+
}
|
|
42
|
+
addMemberToGraph(member) {
|
|
43
|
+
const name = member.key.name;
|
|
44
|
+
const type = ClassGraphBuilder.nodeTypeOf(member);
|
|
45
|
+
const node = ClassGraphBuilder.createGraphNode(name, type, member.accessibility, member.static);
|
|
46
|
+
this.graph[name] = node;
|
|
47
|
+
}
|
|
48
|
+
static nodeTypeOf(member) {
|
|
49
|
+
if (member.type === 'MethodDefinition') {
|
|
50
|
+
return member.kind === 'constructor' ? 'constructor' : 'method';
|
|
51
|
+
}
|
|
52
|
+
return 'property';
|
|
53
|
+
}
|
|
54
|
+
static createGraphNode(name, type, accessibility, isStatic = false) {
|
|
55
|
+
return {
|
|
56
|
+
name,
|
|
57
|
+
type,
|
|
58
|
+
accessibility,
|
|
59
|
+
isStatic,
|
|
60
|
+
dependencies: [],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
static isNamedClassMethod(node) {
|
|
64
|
+
return node.type === 'MethodDefinition';
|
|
65
|
+
}
|
|
66
|
+
addDependencies(node, methodName) {
|
|
67
|
+
const newDependencies = ASTHelpers_1.ASTHelpers.classMethodDependenciesOf(node, this.graph, this.className).filter((name) => !!this.graph[name] && name !== methodName);
|
|
68
|
+
if (this.graph[methodName]) {
|
|
69
|
+
this.graph[methodName].dependencies.push(...newDependencies);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
get graphSorted() {
|
|
73
|
+
return this.sorter.nodesSorted;
|
|
74
|
+
}
|
|
75
|
+
get memberNamesSorted() {
|
|
76
|
+
return this.sorter.nodeNamesSorted;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.ClassGraphBuilder = ClassGraphBuilder;
|
|
80
|
+
//# sourceMappingURL=ClassGraphBuilder.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ClassGraphSorter = void 0;
|
|
4
|
+
class ClassGraphSorter {
|
|
5
|
+
constructor(graph) {
|
|
6
|
+
this.graph = graph;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
exports.ClassGraphSorter = ClassGraphSorter;
|
|
10
|
+
//# sourceMappingURL=ClassGraphSorter.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Graph, GraphNode } from './ClassGraphBuilder';
|
|
2
|
+
import { ClassGraphSorter } from './ClassGraphSorter';
|
|
3
|
+
type GraphNodeModifier = 'isStatic' | 'accessibility';
|
|
4
|
+
export declare class ClassGraphSorterReadability extends ClassGraphSorter {
|
|
5
|
+
graph: Graph;
|
|
6
|
+
static readonly MODIFIER_PRIORITY_MAP: Record<GraphNodeModifier, any[]>;
|
|
7
|
+
private static readonly SEARCH_NODE_PRIORITY_FUNCTIONS;
|
|
8
|
+
constructor(graph: Graph);
|
|
9
|
+
get nodeNamesSorted(): string[];
|
|
10
|
+
get nodesSorted(): GraphNode[];
|
|
11
|
+
sortNodes(): GraphNode[];
|
|
12
|
+
private groupNodesByType;
|
|
13
|
+
private static sortMembersForReadability;
|
|
14
|
+
private dependencyDfs;
|
|
15
|
+
}
|
|
16
|
+
export {};
|