Skip to content

Commit 0fbb7ad

Browse files
denisbalykoawinogradov
authored andcommitted
feat(eslint-plugin): added new plugin
Helps not to write complex constructs and function calls inside className.
1 parent 08234c8 commit 0fbb7ad

4 files changed

Lines changed: 190 additions & 0 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# @bem-react/no-classname-runtime
2+
3+
Do not use the @bem-react/classname function in runtime code
4+
5+
## Rule Details
6+
7+
The classname method from @bem-react/classname is often used and called in the hottest places, sometimes you can take it to the import level, then the result can be passed as a string literal directly to className= " x"
8+
9+
Examples of **incorrect** code for this rule:
10+
11+
```typescript jsx
12+
//
13+
render() {
14+
return (
15+
{items.map((item, idx, arr) => (
16+
<div className={cn('Items')}>
17+
<div className={cn('Item')}>
18+
ItemText
19+
</div>
20+
</div>
21+
)}
22+
)
23+
}
24+
```
25+
26+
Examples of **correct** code for this rule:
27+
28+
```typescript jsx
29+
const cnItems = cn('Items');
30+
const cnItem = cn('Item');
31+
32+
//
33+
render() {
34+
return (
35+
{items.map((item, idx, arr) => (
36+
<div className={cnItems}>
37+
<div className={cnItem}>
38+
ItemText
39+
</div>
40+
</div>
41+
)}
42+
)
43+
}
44+
```

packages/eslint-plugin/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module.exports = {
22
rules: {
3+
'no-classname-runtime': require('./lib/rules/no-classname-runtime'),
34
'whitelist-levels-imports': require('./lib/rules/whitelist-levels-imports'),
45
},
56
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
'use strict'
2+
3+
module.exports = {
4+
meta: {
5+
docs: {
6+
description: 'Do not use the @bem-react/classname function in runtime code',
7+
category: 'perf',
8+
recommended: true,
9+
},
10+
},
11+
12+
create: function(context) {
13+
function isCanBeSimplified(arg) {
14+
if (arg && (arg.type === 'Literal' || arg.type === 'Identifier')) return true
15+
16+
if (arg.type === 'MemberExpression') {
17+
// Exception for this.props.x or this.state.x
18+
if (
19+
arg.object.type === 'ThisExpression' &&
20+
(arg.property.name === 'props' || arg.property.name === 'state')
21+
)
22+
return false
23+
24+
return isCanBeSimplified(arg.object) && isCanBeSimplified(arg.property)
25+
}
26+
27+
if (arg.type === 'Property') {
28+
return isCanBeSimplified(arg.key) && isCanBeSimplified(arg.value)
29+
}
30+
31+
if (arg.type === 'ObjectExpression') {
32+
return isCanBeSlow(arg.properties)
33+
}
34+
35+
if (arg.type === 'ArrayExpression') {
36+
return isCanBeSlow(arg.elements)
37+
}
38+
39+
if (arg.type === 'ConditionalExpression') {
40+
return isCanBeSlow([arg.consequent, arg.alternate])
41+
}
42+
43+
if (arg.type === 'BinaryExpression' || arg.type === 'LogicalExpression') {
44+
return isCanBeSlow([arg.left, arg.right])
45+
}
46+
47+
return false
48+
}
49+
50+
function isCanBeSlow(args, dbg) {
51+
return [].concat(args).every(isCanBeSimplified)
52+
}
53+
54+
function isCallExpression(node) {
55+
if (node.type === 'CallExpression' && isCanBeSlow(node.arguments)) {
56+
return true
57+
}
58+
59+
return false
60+
}
61+
62+
return {
63+
JSXAttribute: function(node) {
64+
const isEvaluatedClassName =
65+
node.name.type === 'JSXIdentifier' &&
66+
node.name.name === 'className' &&
67+
node.value.type === 'JSXExpressionContainer'
68+
69+
if (isEvaluatedClassName) {
70+
const expression = node.value.expression
71+
if (
72+
// className={cn('x')}
73+
isCallExpression(expression) ||
74+
// className={isA ? cn('A') : cn('B')}
75+
(expression.type === 'ConditionalExpression' &&
76+
(isCallExpression(expression.consequent) ||
77+
isCallExpression(expression.alternate))) ||
78+
// className={isA && cn('A') || cn('B')} || className={isA ? cn('A') : cn('B')} ||
79+
((expression.type === 'BinaryExpression' || expression.type === 'LogicalExpression') &&
80+
(isCallExpression(expression.left) || isCallExpression(expression.right)))
81+
) {
82+
context.report({
83+
node: node.parent,
84+
message: "You can speed up your code if you don't call the function on every render",
85+
})
86+
}
87+
}
88+
},
89+
}
90+
},
91+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use strict'
2+
3+
const rule = require('../../../lib/rules/no-classname-runtime')
4+
const { RuleTester } = require('eslint')
5+
6+
RuleTester.setDefaultConfig({
7+
parserOptions: {
8+
ecmaVersion: 2015,
9+
sourceType: 'module',
10+
ecmaFeatures: {
11+
jsx: true,
12+
},
13+
},
14+
})
15+
16+
const ruleTester = new RuleTester()
17+
18+
ruleTester.run('no-classname-runtime', rule, {
19+
valid: [
20+
'<Block className="Time"/>',
21+
'<Block className={cnTest}/>',
22+
'<Block className={this.cnTest}/>',
23+
'<Block className={isVisible ? "Visible" : Hidden}/>',
24+
'<Block className={cnTest(this.props.className)}/>',
25+
'<Block className={cnTest(undefined, [this.props.className])}/>',
26+
].map((code) => ({
27+
code,
28+
})),
29+
30+
invalid: [
31+
'<Block className={cnTest()}/>',
32+
'<Block className={cnTest("Test")}/>',
33+
'<Block className={cnTest("Test", "TestElem")}/>',
34+
'<Block className={cnTest({ theme })}/>',
35+
'<Block className={cnTest({ [props.type]: true })}/>',
36+
'<Block className={cnTest({ [props.type]: a.b.c.d })}/>',
37+
'<Block className={cnTest({ theme, colored })}/>',
38+
'<Block className={cnTest("Test", { theme })}/>',
39+
'<Block className={cnTest("Test", { params }, [mixClassName])}/>',
40+
'<Block className={cnTest("Test", null, [mixClassName])}/>',
41+
'<Block className={cnTest(null, [mixClassName])}/>',
42+
'<Block className={cnTest({ isVisible: a > b })}/>',
43+
'<Block className={isVisible ? cnTest("Visible") : cnTest("Hidden")}/>',
44+
'<Block className={isVisible && cnTest("Visible") || cnTest("Hidden")}/>',
45+
'<Block className={isVisible || cnTest("Hidden")} />',
46+
].map((code) => ({
47+
code,
48+
errors: [
49+
{
50+
message: "You can speed up your code if you don't call the function on every render",
51+
},
52+
],
53+
})),
54+
})

0 commit comments

Comments
 (0)