Skip to content

Commit 83a079f

Browse files
committed
feat: warn on banned syntax when @silvermine/toolbox is installed
Adds a local inline ESLint plugin that warns consumers to prefer toolbox helpers over native JS patterns.
1 parent 3d0c92c commit 83a079f

File tree

2 files changed

+231
-0
lines changed

2 files changed

+231
-0
lines changed

index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const ignores = require('./partials/ignores'),
1717
vueBaseRules = require('./partials/vue/vue-base'),
1818
vue3rules = require('./partials/vue/vue-3'),
1919
typeDefintions = require('./partials/type-definitions'),
20+
toolboxRules = require('./partials/toolbox-rules'),
2021
esLint = require('@eslint/js'),
2122
typescriptESLint = require('typescript-eslint'),
2223
eslintPluginVue = require('eslint-plugin-vue');
@@ -63,4 +64,5 @@ module.exports = [
6364
},
6465
},
6566
},
67+
...toolboxRules,
6668
];

partials/toolbox-rules.js

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
'use strict';
2+
3+
const COMPARISON_OPS = [ '===', '!==', '==', '!=' ],
4+
EQUALITY_OPS = [ '===', '==' ],
5+
INEQUALITY_OPS = [ '!==', '!=' ];
6+
7+
const TYPEOF_HELPERS = {
8+
'string': '`isString`',
9+
'boolean': '`isBoolean`',
10+
'number': '`isNumber`',
11+
'function': '`isFunction`',
12+
'undefined': '`isUndefined`',
13+
'object': '`isObject`, `isStringMap`, or `isStringUnknownMap`',
14+
};
15+
16+
const localPlugin = {
17+
rules: {
18+
'no-in-operator': {
19+
create(context) {
20+
return {
21+
BinaryExpression(node) {
22+
if (node.operator === 'in') {
23+
context.report({
24+
node,
25+
message: 'Use `hasDefined` from `@silvermine/toolbox` instead of the `in` operator.',
26+
});
27+
}
28+
},
29+
};
30+
},
31+
},
32+
33+
'no-typeof-check': {
34+
create(context) {
35+
return {
36+
BinaryExpression(node) {
37+
const isComparisonOp = COMPARISON_OPS.includes(node.operator),
38+
isTypeofLeft = node.left.type === 'UnaryExpression' && node.left.operator === 'typeof',
39+
isTypeofRight = node.right.type === 'UnaryExpression' && node.right.operator === 'typeof',
40+
isLiteralRight = node.right.type === 'Literal',
41+
isLiteralLeft = node.left.type === 'Literal';
42+
43+
if (!isComparisonOp) {
44+
return;
45+
}
46+
47+
let typeValue = null;
48+
49+
if (isTypeofLeft && isLiteralRight) {
50+
typeValue = node.right.value;
51+
} else if (isTypeofRight && isLiteralLeft) {
52+
typeValue = node.left.value;
53+
}
54+
55+
if (!typeValue || !TYPEOF_HELPERS[typeValue]) {
56+
return;
57+
}
58+
59+
context.report({
60+
node,
61+
message: 'Use ' + TYPEOF_HELPERS[typeValue] + ' from `@silvermine/toolbox` instead of `typeof` checks.',
62+
});
63+
},
64+
};
65+
},
66+
},
67+
68+
'prefer-is-empty': {
69+
create(context) {
70+
return {
71+
BinaryExpression(node) {
72+
const isEqualityOp = EQUALITY_OPS.includes(node.operator),
73+
isInequalityOp = INEQUALITY_OPS.includes(node.operator),
74+
leftIsLength = node.left.type === 'MemberExpression' && node.left.property.name === 'length',
75+
rightIsZero = node.right.type === 'Literal' && node.right.value === 0,
76+
rightIsLength = node.right.type === 'MemberExpression' && node.right.property.name === 'length',
77+
leftIsZero = node.left.type === 'Literal' && node.left.value === 0,
78+
isLengthVsZero = (leftIsLength && rightIsZero) || (rightIsLength && leftIsZero);
79+
80+
if (!isLengthVsZero) {
81+
return;
82+
}
83+
84+
if (isEqualityOp) {
85+
context.report({
86+
node,
87+
message: 'Use `isEmpty` from `@silvermine/toolbox` instead of `.length === 0`.',
88+
});
89+
} else if (isInequalityOp) {
90+
context.report({
91+
node,
92+
message: 'Use `!isEmpty(...)` from `@silvermine/toolbox` instead of `.length !== 0`.',
93+
});
94+
}
95+
},
96+
};
97+
},
98+
},
99+
100+
'prefer-compact': {
101+
create(context) {
102+
return {
103+
CallExpression(node) {
104+
const isFilter = node.callee.type === 'MemberExpression'
105+
&& node.callee.property.type === 'Identifier'
106+
&& node.callee.property.name === 'filter';
107+
108+
const hasSingleBooleanArg = node.arguments.length === 1
109+
&& node.arguments[0].type === 'Identifier'
110+
&& node.arguments[0].name === 'Boolean';
111+
112+
if (isFilter && hasSingleBooleanArg) {
113+
context.report({
114+
node,
115+
message: 'Use `compact` from `@silvermine/toolbox` instead of `.filter(Boolean)`.',
116+
});
117+
}
118+
},
119+
};
120+
},
121+
},
122+
123+
'prefer-is-array': {
124+
create(context) {
125+
return {
126+
CallExpression(node) {
127+
const isArrayIsArray = node.callee.type === 'MemberExpression'
128+
&& node.callee.object.type === 'Identifier'
129+
&& node.callee.object.name === 'Array'
130+
&& node.callee.property.type === 'Identifier'
131+
&& node.callee.property.name === 'isArray';
132+
133+
if (isArrayIsArray) {
134+
context.report({
135+
node,
136+
message: 'Use `isArray` from `@silvermine/toolbox` instead of `Array.isArray()`.',
137+
});
138+
}
139+
},
140+
};
141+
},
142+
},
143+
144+
'no-null-check': {
145+
create(context) {
146+
return {
147+
BinaryExpression(node) {
148+
const isStrictOp = node.operator === '===' || node.operator === '!==',
149+
leftIsNull = node.left.type === 'Literal' && node.left.raw === 'null',
150+
rightIsNull = node.right.type === 'Literal' && node.right.raw === 'null';
151+
152+
if (!isStrictOp || (!leftIsNull && !rightIsNull)) {
153+
return;
154+
}
155+
156+
const suggestion = node.operator === '!=='
157+
? '`!isNull`'
158+
: '`isNull`';
159+
160+
context.report({
161+
node,
162+
message: 'Use ' + suggestion + ' from `@silvermine/toolbox` instead of null equality checks.',
163+
});
164+
},
165+
};
166+
},
167+
},
168+
169+
'no-undefined-check': {
170+
create(context) {
171+
return {
172+
BinaryExpression(node) {
173+
const isStrictOp = node.operator === '===' || node.operator === '!==',
174+
leftIsUndefined = node.left.type === 'Identifier' && node.left.name === 'undefined',
175+
rightIsUndefined = node.right.type === 'Identifier' && node.right.name === 'undefined';
176+
177+
if (!isStrictOp || (!leftIsUndefined && !rightIsUndefined)) {
178+
return;
179+
}
180+
181+
const suggestion = node.operator === '!=='
182+
? '`!isUndefined`'
183+
: '`isUndefined`';
184+
185+
context.report({
186+
node,
187+
message: 'Use ' + suggestion + ' from `@silvermine/toolbox` instead of undefined equality checks.',
188+
});
189+
},
190+
};
191+
},
192+
},
193+
},
194+
};
195+
196+
// These rules are enabled by default. Projects that do not use @silvermine/toolbox
197+
// can disable them by overriding the rules in their eslint.config.js, e.g.:
198+
//
199+
// import silvermineConfig from '@silvermine/eslint-config';
200+
//
201+
// export default [
202+
// ...silvermineConfig,
203+
// {
204+
// rules: {
205+
// '@silvermine/toolbox/no-in-operator': 'off',
206+
// '@silvermine/toolbox/no-typeof-check': 'off',
207+
// '@silvermine/toolbox/prefer-is-empty': 'off',
208+
// '@silvermine/toolbox/prefer-compact': 'off',
209+
// '@silvermine/toolbox/prefer-is-array': 'off',
210+
// '@silvermine/toolbox/no-null-check': 'off',
211+
// '@silvermine/toolbox/no-undefined-check': 'off',
212+
// },
213+
// },
214+
// ];
215+
module.exports = [
216+
{
217+
files: [ '**/*.{js,ts,vue}' ],
218+
plugins: { '@silvermine/toolbox': localPlugin },
219+
rules: {
220+
'@silvermine/toolbox/no-in-operator': 'warn',
221+
'@silvermine/toolbox/no-typeof-check': 'warn',
222+
'@silvermine/toolbox/prefer-is-empty': 'warn',
223+
'@silvermine/toolbox/prefer-compact': 'warn',
224+
'@silvermine/toolbox/prefer-is-array': 'warn',
225+
'@silvermine/toolbox/no-null-check': 'warn',
226+
'@silvermine/toolbox/no-undefined-check': 'warn',
227+
},
228+
},
229+
];

0 commit comments

Comments
 (0)