Skip to content

Commit 843eaff

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. Rules are only active when @silvermine/toolbox is resolvable in the consuming project.
1 parent bff56ad commit 843eaff

File tree

2 files changed

+221
-0
lines changed

2 files changed

+221
-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');
@@ -47,4 +48,5 @@ module.exports = [
4748
...vue3rules,
4849
},
4950
},
51+
...toolboxRules,
5052
];

partials/toolbox-rules.js

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

0 commit comments

Comments
 (0)