@blumintinc/eslint-plugin-blumint 1.20.36 → 1.20.38
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/lib/index.js
CHANGED
|
@@ -22,21 +22,48 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
22
22
|
return (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
23
23
|
node.type === utils_1.AST_NODE_TYPES.FunctionExpression);
|
|
24
24
|
}
|
|
25
|
+
// Matches both the bare hook (`useMemo(...)`, including the generic form
|
|
26
|
+
// `useCallback<T>(...)`, whose callee is still an identifier) and the
|
|
27
|
+
// namespaced form (`React.useMemo(...)`).
|
|
28
|
+
function isHookCallee(callee, hookName) {
|
|
29
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
30
|
+
return callee.name === hookName;
|
|
31
|
+
}
|
|
32
|
+
return (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
33
|
+
!callee.computed &&
|
|
34
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
35
|
+
callee.property.name === hookName);
|
|
36
|
+
}
|
|
25
37
|
function isInsideUseCallback(node) {
|
|
26
38
|
let current = node.parent;
|
|
27
39
|
while (current) {
|
|
28
|
-
if (current.type === utils_1.AST_NODE_TYPES.CallExpression
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
if (current.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
41
|
+
isHookCallee(current.callee, 'useCallback')) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
current = current.parent;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
// A `useMemo` factory re-runs only when its dependencies change, so every
|
|
49
|
+
// value it produces — including JSX and the inline handlers nested in that
|
|
50
|
+
// JSX — is exactly as referentially stable as the memo itself. Demanding an
|
|
51
|
+
// extra `useCallback` there buys nothing.
|
|
52
|
+
//
|
|
53
|
+
// `useCallback` deliberately gets no such treatment: it memoizes a function
|
|
54
|
+
// that runs on every invocation, so the JSX it returns (and the inline
|
|
55
|
+
// functions in it) is rebuilt each call and stays worth reporting.
|
|
56
|
+
function isInsideUseMemoFactory(node) {
|
|
57
|
+
let child = node;
|
|
58
|
+
let current = node.parent;
|
|
59
|
+
while (current) {
|
|
60
|
+
if (current.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
61
|
+
isHookCallee(current.callee, 'useMemo') &&
|
|
62
|
+
// Only the factory argument is memoized; the dependency array is not.
|
|
63
|
+
current.arguments[0] === child) {
|
|
64
|
+
return true;
|
|
39
65
|
}
|
|
66
|
+
child = current;
|
|
40
67
|
current = current.parent;
|
|
41
68
|
}
|
|
42
69
|
return false;
|
|
@@ -167,6 +194,10 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
167
194
|
node.value.type !== utils_1.AST_NODE_TYPES.JSXExpressionContainer) {
|
|
168
195
|
return;
|
|
169
196
|
}
|
|
197
|
+
// Props of JSX built inside a useMemo factory inherit the memo's stability
|
|
198
|
+
if (isInsideUseMemoFactory(node)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
170
201
|
const { expression } = node.value;
|
|
171
202
|
// Skip if the prop is already wrapped in useCallback or useMemo
|
|
172
203
|
if (expression.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.enforceUniqueCursorHeaders = exports.RULE_NAME = void 0;
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const minimatch_1 = require("minimatch");
|
|
9
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
9
10
|
const createRule_1 = require("../utils/createRule");
|
|
10
11
|
const DEFAULT_OPTIONS = {
|
|
11
12
|
requiredPatterns: ['**/*.{ts,tsx,js,jsx}'],
|
|
@@ -212,6 +213,62 @@ const analyzeHeaderGroups = (candidateGroups, options) => {
|
|
|
212
213
|
});
|
|
213
214
|
return { primaryHeader, duplicateGroups, splitHeaderGroups };
|
|
214
215
|
};
|
|
216
|
+
/**
|
|
217
|
+
* A template made of nothing but comments is safe to prepend to any file;
|
|
218
|
+
* anything else (bare text, trailing code) would be inserted as source and can
|
|
219
|
+
* leave the file unparseable.
|
|
220
|
+
*/
|
|
221
|
+
const isCommentOnly = (template) => template
|
|
222
|
+
.replace(/\/\*[\s\S]*?\*\//gu, '')
|
|
223
|
+
.replace(/\/\/[^\n]*/gu, '')
|
|
224
|
+
.trim().length === 0;
|
|
225
|
+
const toTemplateComment = (template, raw, offset) => {
|
|
226
|
+
const isBlock = raw.startsWith('/*');
|
|
227
|
+
const precedingText = template.slice(0, offset);
|
|
228
|
+
const startLine = precedingText.split('\n').length;
|
|
229
|
+
const startColumn = offset - (precedingText.lastIndexOf('\n') + 1);
|
|
230
|
+
const rawLines = raw.split('\n');
|
|
231
|
+
const lastLine = rawLines[rawLines.length - 1];
|
|
232
|
+
return {
|
|
233
|
+
type: isBlock ? utils_1.AST_TOKEN_TYPES.Block : utils_1.AST_TOKEN_TYPES.Line,
|
|
234
|
+
/** Header detection normalizes the raw comment value, so drop only the delimiters. */
|
|
235
|
+
value: isBlock ? raw.slice(2, -2) : raw.slice(2),
|
|
236
|
+
range: [offset, offset + raw.length],
|
|
237
|
+
loc: {
|
|
238
|
+
start: { line: startLine, column: startColumn },
|
|
239
|
+
end: {
|
|
240
|
+
line: startLine + rawLines.length - 1,
|
|
241
|
+
column: rawLines.length > 1 ? lastLine.length : startColumn + raw.length,
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
const parseTemplateComments = (template) => {
|
|
247
|
+
const commentPattern = /\/\*[\s\S]*?\*\/|\/\/[^\n]*/gu;
|
|
248
|
+
const comments = [];
|
|
249
|
+
for (let match = commentPattern.exec(template); match !== null; match = commentPattern.exec(template)) {
|
|
250
|
+
comments.push(toTemplateComment(template, match[0], match.index));
|
|
251
|
+
}
|
|
252
|
+
return comments;
|
|
253
|
+
};
|
|
254
|
+
/**
|
|
255
|
+
* A fixer must remove its own trigger. Inserting a template the rule itself
|
|
256
|
+
* would not accept as a header leaves `missingHeader` reported, so ESLint's fix
|
|
257
|
+
* loop re-inserts the template on every pass until it hits the pass ceiling.
|
|
258
|
+
*
|
|
259
|
+
* Whether a template is acceptable is not a property of its text alone: the
|
|
260
|
+
* rule groups comments first and demands that a single group carry every tag,
|
|
261
|
+
* so `allowSplitHeaders` and block adjacency decide the answer. Running the
|
|
262
|
+
* template through the same grouping the rule applies to real files keeps the
|
|
263
|
+
* fixer convergent by construction instead of by a second approximation.
|
|
264
|
+
*/
|
|
265
|
+
const canTemplateSatisfyRule = (template, options, excludedAtDirectives) => {
|
|
266
|
+
if (options.requiredTags.length === 0 || !isCommentOnly(template)) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
const candidateGroups = collectHeaderGroups(parseTemplateComments(template), options, excludedAtDirectives);
|
|
270
|
+
return (analyzeHeaderGroups(candidateGroups, options).primaryHeader !== undefined);
|
|
271
|
+
};
|
|
215
272
|
const computeHeaderInsertion = (sourceText, headerTemplate) => {
|
|
216
273
|
const headerText = buildHeaderInsertionText(headerTemplate);
|
|
217
274
|
if (!sourceText.startsWith('#!')) {
|
|
@@ -225,8 +282,7 @@ const computeHeaderInsertion = (sourceText, headerTemplate) => {
|
|
|
225
282
|
/** Insert after shebang line */
|
|
226
283
|
return { index: shebangNewlineIndex + 1, text: headerText };
|
|
227
284
|
};
|
|
228
|
-
const reportMissingHeader = (context, fileName, sourceText, options) => {
|
|
229
|
-
const template = options.headerTemplate;
|
|
285
|
+
const reportMissingHeader = (context, fileName, sourceText, options, template) => {
|
|
230
286
|
context.report({
|
|
231
287
|
loc: { line: 1, column: 0 },
|
|
232
288
|
messageId: 'missingHeader',
|
|
@@ -331,6 +387,10 @@ exports.enforceUniqueCursorHeaders = (0, createRule_1.createRule)({
|
|
|
331
387
|
const fileName = context.getFilename();
|
|
332
388
|
const matchPath = fileName.split(path_1.default.sep).join('/');
|
|
333
389
|
const excludedAtDirectives = new Set(options.excludedAtDirectives);
|
|
390
|
+
const fixableTemplate = options.headerTemplate !== null &&
|
|
391
|
+
canTemplateSatisfyRule(options.headerTemplate, options, excludedAtDirectives)
|
|
392
|
+
? options.headerTemplate
|
|
393
|
+
: null;
|
|
334
394
|
if (fileName === '<input>') {
|
|
335
395
|
return {};
|
|
336
396
|
}
|
|
@@ -353,7 +413,7 @@ exports.enforceUniqueCursorHeaders = (0, createRule_1.createRule)({
|
|
|
353
413
|
const candidateGroups = collectHeaderGroups(topComments, options, excludedAtDirectives);
|
|
354
414
|
const { primaryHeader, duplicateGroups, splitHeaderGroups } = analyzeHeaderGroups(candidateGroups, options);
|
|
355
415
|
if (!primaryHeader) {
|
|
356
|
-
reportMissingHeader(context, fileName, sourceText, options);
|
|
416
|
+
reportMissingHeader(context, fileName, sourceText, options, fixableTemplate);
|
|
357
417
|
return;
|
|
358
418
|
}
|
|
359
419
|
reportDuplicates(context, duplicateGroups, sourceText);
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.38",
|
|
4
|
+
"date": "2026-07-30T20:52:16.921Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-callback-memo",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1465
|
|
11
|
+
],
|
|
12
|
+
"summary": "allow an inline callback inside a useMemo factory (closes #1465)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.20.37",
|
|
18
|
+
"date": "2026-07-30T18:07:02.175Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "enforce-unique-cursor-headers",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1461
|
|
25
|
+
],
|
|
26
|
+
"summary": "only autofix a template that satisfies the rule (closes #1461)"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.20.36",
|
|
4
32
|
"date": "2026-07-30T17:05:52.383Z",
|