@blumintinc/eslint-plugin-blumint 1.19.17 → 1.19.19
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
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.enforceAssertSafeObjectKey = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
4
8
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
9
|
const createRule_1 = require("../utils/createRule");
|
|
6
10
|
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
@@ -31,13 +35,47 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
31
35
|
create(context, [options]) {
|
|
32
36
|
const importPath = options?.assertSafeImportPath || DEFAULT_IMPORT_PATH;
|
|
33
37
|
let hasAssertSafeImport = false;
|
|
38
|
+
/**
|
|
39
|
+
* Computes the module specifier for the injected assertSafe import.
|
|
40
|
+
*
|
|
41
|
+
* `importPath` is anchored at the repo root (relative to process.cwd(),
|
|
42
|
+
* which is the repo root in real eslint runs), matching how
|
|
43
|
+
* avoid-utils-directory and test-file-location-enforcement treat paths.
|
|
44
|
+
* A bare repo-root specifier such as 'functions/src/util/assertSafe' is
|
|
45
|
+
* unresolvable inside the functions/ TS project, whose baseUrl is
|
|
46
|
+
* functions/: it would resolve to functions/functions/src/util/assertSafe,
|
|
47
|
+
* which does not exist. The specifier is therefore derived relative to the
|
|
48
|
+
* file being fixed so the emitted import resolves from that file's location.
|
|
49
|
+
*/
|
|
50
|
+
const computeImportSpecifier = () => {
|
|
51
|
+
const rawFilename = context.getFilename().replace(/\\/g, '/');
|
|
52
|
+
// Virtual/stdin files (RuleTester default 'file.ts', '<input>', '<text>')
|
|
53
|
+
// are not absolute; we cannot anchor a relative path, so emit the
|
|
54
|
+
// configured path verbatim (preserves the option's literal value for
|
|
55
|
+
// non-file lints).
|
|
56
|
+
if (!path_1.default.isAbsolute(rawFilename)) {
|
|
57
|
+
return importPath;
|
|
58
|
+
}
|
|
59
|
+
const target = importPath
|
|
60
|
+
.replace(/\\/g, '/')
|
|
61
|
+
.replace(/\.(tsx?|jsx?|mts|cts)$/i, '');
|
|
62
|
+
const fileRelToCwd = path_1.default
|
|
63
|
+
.relative(process.cwd(), rawFilename)
|
|
64
|
+
.replace(/\\/g, '/');
|
|
65
|
+
const fileDir = path_1.default.posix.dirname(fileRelToCwd);
|
|
66
|
+
let specifier = path_1.default.posix.relative(fileDir, target);
|
|
67
|
+
if (!specifier.startsWith('.')) {
|
|
68
|
+
specifier = `./${specifier}`;
|
|
69
|
+
}
|
|
70
|
+
return specifier;
|
|
71
|
+
};
|
|
34
72
|
/**
|
|
35
73
|
* Helper function to add assertSafe import if needed
|
|
36
74
|
*/
|
|
37
75
|
const addAssertSafeImport = (fixer) => {
|
|
38
76
|
const program = context.sourceCode.ast;
|
|
39
77
|
const firstImport = program.body.find((node) => node.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
|
|
40
|
-
const importStatement = `import { assertSafe } from '${
|
|
78
|
+
const importStatement = `import { assertSafe } from '${computeImportSpecifier()}';\n`;
|
|
41
79
|
if (firstImport) {
|
|
42
80
|
return fixer.insertTextBefore(firstImport, importStatement);
|
|
43
81
|
}
|
|
@@ -348,6 +348,179 @@ function getStatementRangeWithComments(statement, sourceCode, consumedComments,
|
|
|
348
348
|
trailingCandidates.forEach((comment) => consumedComments?.add(comment));
|
|
349
349
|
return [start, end];
|
|
350
350
|
}
|
|
351
|
+
// Collect every identifier a statement references anywhere in its subtree,
|
|
352
|
+
// including type-annotation positions (e.g. `WidgetProps` inside
|
|
353
|
+
// `FC<WidgetProps>`) and default-parameter values (e.g. `DEFAULT_LABEL` in
|
|
354
|
+
// `{ label = DEFAULT_LABEL }`). Identifiers that are member-access properties or
|
|
355
|
+
// object/type member keys are skipped: `obj.OFFSET` is not a reference to a
|
|
356
|
+
// top-level `OFFSET` binding, so counting it would spuriously constrain the
|
|
357
|
+
// reorder.
|
|
358
|
+
function collectReferencedNames(node) {
|
|
359
|
+
const names = new Set();
|
|
360
|
+
const visit = (current, parent) => {
|
|
361
|
+
if (!current || !ASTHelpers_1.ASTHelpers.isNode(current)) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (current.type === 'Identifier') {
|
|
365
|
+
const isMemberProperty = parent?.type === 'MemberExpression' &&
|
|
366
|
+
parent.property === current &&
|
|
367
|
+
!parent.computed;
|
|
368
|
+
const isObjectKey = parent?.type === 'Property' &&
|
|
369
|
+
parent.key === current &&
|
|
370
|
+
!parent.computed;
|
|
371
|
+
const isMemberKey = (parent?.type === 'TSPropertySignature' ||
|
|
372
|
+
parent?.type === 'TSMethodSignature') &&
|
|
373
|
+
parent.key === current &&
|
|
374
|
+
!parent.computed;
|
|
375
|
+
if (!isMemberProperty && !isObjectKey && !isMemberKey) {
|
|
376
|
+
names.add(current.name);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
Object.values(current).forEach((value) => {
|
|
380
|
+
if (!value || value === current || current.parent === value) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (Array.isArray(value)) {
|
|
384
|
+
value.forEach((child) => {
|
|
385
|
+
if (ASTHelpers_1.ASTHelpers.isNode(child)) {
|
|
386
|
+
visit(child, current);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
else if (ASTHelpers_1.ASTHelpers.isNode(value)) {
|
|
391
|
+
visit(value, current);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
};
|
|
395
|
+
visit(node, null);
|
|
396
|
+
return names;
|
|
397
|
+
}
|
|
398
|
+
// Names bound by an interleaved VALUE declaration (const/let/var) — but not by a
|
|
399
|
+
// reorderable function. Hoisting a function above a value binding it references
|
|
400
|
+
// is a genuine runtime declare-before-use. Type aliases are tracked separately
|
|
401
|
+
// (interleavedTypeAliasNamesOf), because their hoist hazard is not runtime — it
|
|
402
|
+
// is the companion prefer-type-alias-over-typeof-constant rule, which fires only
|
|
403
|
+
// for a const declarator whose own type annotation names a later alias.
|
|
404
|
+
function interleavedValueNamesOf(regionStatements, functionStatements) {
|
|
405
|
+
const names = new Set();
|
|
406
|
+
regionStatements.forEach((statement) => {
|
|
407
|
+
if (functionStatements.has(statement)) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (statement.type === 'VariableDeclaration') {
|
|
411
|
+
statement.declarations.forEach((declarator) => {
|
|
412
|
+
if (declarator.id.type === 'Identifier') {
|
|
413
|
+
names.add(declarator.id.name);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
return names;
|
|
419
|
+
}
|
|
420
|
+
// Names bound by an interleaved `type` alias in the reordered region.
|
|
421
|
+
function interleavedTypeAliasNamesOf(regionStatements, functionStatements) {
|
|
422
|
+
const names = new Set();
|
|
423
|
+
regionStatements.forEach((statement) => {
|
|
424
|
+
if (functionStatements.has(statement)) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (statement.type === 'TSTypeAliasDeclaration') {
|
|
428
|
+
names.add(statement.id.name);
|
|
429
|
+
}
|
|
430
|
+
else if (statement.type === 'ExportNamedDeclaration' &&
|
|
431
|
+
statement.declaration?.type === 'TSTypeAliasDeclaration') {
|
|
432
|
+
names.add(statement.declaration.id.name);
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
return names;
|
|
436
|
+
}
|
|
437
|
+
// Type names referenced in the annotation of a function-holding const's own
|
|
438
|
+
// binding identifier (e.g. `WidgetProps` in `const W: FC<WidgetProps> = ...`).
|
|
439
|
+
// This mirrors exactly what prefer-type-alias-over-typeof-constant's
|
|
440
|
+
// defineTypeBeforeConstant keys on: only a const declarator's `id` annotation.
|
|
441
|
+
// A function *declaration*'s return type (`function f(): Marker`) and an arrow
|
|
442
|
+
// parameter's annotation (`const f = (p: Props) => ...`) are deliberately NOT
|
|
443
|
+
// counted — neither trips the companion rule, so constraining them would decline
|
|
444
|
+
// safe reorders.
|
|
445
|
+
function constAnnotationTypeNamesOf(statementNode) {
|
|
446
|
+
const names = new Set();
|
|
447
|
+
const declaration = statementNode.type === 'ExportNamedDeclaration'
|
|
448
|
+
? statementNode.declaration
|
|
449
|
+
: statementNode;
|
|
450
|
+
if (declaration?.type === 'VariableDeclaration') {
|
|
451
|
+
declaration.declarations.forEach((declarator) => {
|
|
452
|
+
if (declarator.id.type === 'Identifier' && declarator.id.typeAnnotation) {
|
|
453
|
+
collectReferencedNames(declarator.id.typeAnnotation).forEach((name) => names.add(name));
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
return names;
|
|
458
|
+
}
|
|
459
|
+
// Decline (return true) any reorder that would place a function ABOVE an
|
|
460
|
+
// interleaved declaration it depends on. The fixer pins interleaved statements in
|
|
461
|
+
// place and only swaps functions between their own slots, so such a reorder
|
|
462
|
+
// produces a fresh declare-before-use — trading one violation for another instead
|
|
463
|
+
// of converging. Two hazards are guarded:
|
|
464
|
+
// * value binding (const/let/var): a runtime declare-before-use if a function
|
|
465
|
+
// references it and is hoisted above it.
|
|
466
|
+
// * `type` alias: hoisting a function-holding const whose OWN binding
|
|
467
|
+
// annotation names the alias above that alias trips the companion
|
|
468
|
+
// prefer-type-alias-over-typeof-constant rule (defineTypeBeforeConstant).
|
|
469
|
+
// Walks the post-reorder sequence front-to-back: each function slot is filled
|
|
470
|
+
// with the next expected function; interleaved statements keep their slot. A
|
|
471
|
+
// function that references a not-yet-declared interleaved dependency is being
|
|
472
|
+
// hoisted above it.
|
|
473
|
+
function reorderHoistsFunctionAboveDependency(regionStatements, functionStatements, expectedOrderInfos) {
|
|
474
|
+
const interleavedValueNames = interleavedValueNamesOf(regionStatements, functionStatements);
|
|
475
|
+
const interleavedTypeNames = interleavedTypeAliasNamesOf(regionStatements, functionStatements);
|
|
476
|
+
if (interleavedValueNames.size === 0 && interleavedTypeNames.size === 0) {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
const declaredValueNames = new Set();
|
|
480
|
+
const declaredTypeNames = new Set();
|
|
481
|
+
let slotCursor = 0;
|
|
482
|
+
for (const statement of regionStatements) {
|
|
483
|
+
if (functionStatements.has(statement)) {
|
|
484
|
+
const occupant = expectedOrderInfos[slotCursor];
|
|
485
|
+
slotCursor += 1;
|
|
486
|
+
if (!occupant) {
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (interleavedValueNames.size > 0) {
|
|
490
|
+
const referenced = collectReferencedNames(occupant.statementNode);
|
|
491
|
+
for (const name of referenced) {
|
|
492
|
+
if (interleavedValueNames.has(name) &&
|
|
493
|
+
!declaredValueNames.has(name)) {
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (interleavedTypeNames.size > 0) {
|
|
499
|
+
const annotationTypes = constAnnotationTypeNamesOf(occupant.statementNode);
|
|
500
|
+
for (const name of annotationTypes) {
|
|
501
|
+
if (interleavedTypeNames.has(name) && !declaredTypeNames.has(name)) {
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
else if (statement.type === 'VariableDeclaration') {
|
|
508
|
+
statement.declarations.forEach((declarator) => {
|
|
509
|
+
if (declarator.id.type === 'Identifier') {
|
|
510
|
+
declaredValueNames.add(declarator.id.name);
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
else if (statement.type === 'TSTypeAliasDeclaration') {
|
|
515
|
+
declaredTypeNames.add(statement.id.name);
|
|
516
|
+
}
|
|
517
|
+
else if (statement.type === 'ExportNamedDeclaration' &&
|
|
518
|
+
statement.declaration?.type === 'TSTypeAliasDeclaration') {
|
|
519
|
+
declaredTypeNames.add(statement.declaration.id.name);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
351
524
|
exports.verticallyGroupRelatedFunctions = (0, createRule_1.createRule)({
|
|
352
525
|
name: 'vertically-group-related-functions',
|
|
353
526
|
meta: {
|
|
@@ -500,6 +673,16 @@ exports.verticallyGroupRelatedFunctions = (0, createRule_1.createRule)({
|
|
|
500
673
|
}
|
|
501
674
|
const slice = node.body.slice(firstFunctionIndex, lastFunctionIndex + 1);
|
|
502
675
|
const blockContainsOnlyFunctions = slice.every((statement) => functionStatements.has(statement));
|
|
676
|
+
// Decline a reorder that would hoist a function above an interleaved
|
|
677
|
+
// declaration it depends on (a value binding it references, or the
|
|
678
|
+
// `type` alias named in its own const annotation). Applies to both
|
|
679
|
+
// paths, though it can only trigger in the interleaved-statement
|
|
680
|
+
// branch below (Path A reorders a block with no interleaved
|
|
681
|
+
// declarations). The misorderedFunction report still fires; only the
|
|
682
|
+
// harmful, non-converging autofix is suppressed.
|
|
683
|
+
if (reorderHoistsFunctionAboveDependency(slice, functionStatements, expectedOrderInfos)) {
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
503
686
|
if (!blockContainsOnlyFunctions) {
|
|
504
687
|
// Real modules interleave type aliases, consts, and top-level
|
|
505
688
|
// calls (e.g. `void autoRunIfMain();`) between functions. Rather
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.19.19",
|
|
4
|
+
"date": "2026-07-18T17:27:38.611Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-assert-safe-object-key",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1321
|
|
11
|
+
],
|
|
12
|
+
"summary": "compute assertSafe import specifier relative to the fixed file (closes #1321)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.19.18",
|
|
18
|
+
"date": "2026-07-18T13:43:27.635Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "vertically-group-related-functions",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1320
|
|
25
|
+
],
|
|
26
|
+
"summary": "decline reorder that hoists a function above its interleaved type/const dependency (closes #1320)"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.19.17",
|
|
4
32
|
"date": "2026-07-18T07:27:36.853Z",
|