@blumintinc/eslint-plugin-blumint 1.20.21 → 1.20.22

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
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.21',
226
+ version: '1.20.22',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -2,6 +2,7 @@ import { TSESLint } from '@typescript-eslint/utils';
2
2
  type Options = [
3
3
  {
4
4
  sideEffectPatterns?: Array<string | RegExp>;
5
+ ignoreTestFiles?: boolean;
5
6
  }
6
7
  ];
7
8
  export declare const parallelizeAsyncOperations: TSESLint.RuleModule<"parallelizeAsyncOperations", Options, TSESLint.RuleListener>;
@@ -3,8 +3,26 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parallelizeAsyncOperations = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ // Anchored at the end of the path so multi-part suffixes such as
7
+ // `EventRegistry.integration.test.ts` are recognized while production modules
8
+ // that merely contain the word (`testHelpers.ts`, `latest.ts`, `contest/Thing.ts`)
9
+ // keep their enforcement.
10
+ const TEST_FILE_SUFFIX = /\.(test|spec)\.[cm]?[jt]sx?$/;
11
+ // Jest convention directories hold test-only modules regardless of file name.
12
+ const TEST_FILE_DIRECTORY = /(^|\/)(__tests__|__mocks__)\//;
13
+ /**
14
+ * A test suite serves no requests and is not latency-critical, so the rule's
15
+ * rationale — that sequential awaits make network and I/O latency add up — does
16
+ * not apply to it. Its awaits instead encode ordering: `await` an interaction,
17
+ * then `await` an assertion that observes the DOM state the interaction
18
+ * produced. That dependency is a side effect rather than a value, so it is
19
+ * invisible to the syntactic barriers below, and Promise.all would race the
20
+ * assertion against the interaction (issue #1395).
21
+ */
22
+ const isTestFile = (filename) => TEST_FILE_SUFFIX.test(filename) || TEST_FILE_DIRECTORY.test(filename);
6
23
  const defaultOptions = [
7
24
  {
25
+ ignoreTestFiles: true,
8
26
  sideEffectPatterns: [
9
27
  'updatecounter',
10
28
  'setcounter',
@@ -28,6 +46,15 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
28
46
  recommended: 'error',
29
47
  },
30
48
  fixable: 'code',
49
+ // `defaultOptions` above is the single source of truth for defaults; the
50
+ // schema deliberately declares none. ESLint validates rule options with an
51
+ // ajv instance configured `useDefaults: true`, which writes schema defaults
52
+ // INTO the supplied options object before `defaultOptions` are merged. A
53
+ // schema `default: []` on sideEffectPatterns therefore erases the built-in
54
+ // side-effect patterns for any consumer who passes an options object at all
55
+ // -- including one that only sets `ignoreTestFiles` -- so `commit`, `flush`,
56
+ // and the counter patterns stop acting as ordering barriers and the rule
57
+ // reports the very sequences it is meant to leave alone.
31
58
  schema: [
32
59
  {
33
60
  type: 'object',
@@ -40,7 +67,9 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
40
67
  { type: 'object', instanceof: 'RegExp' },
41
68
  ],
42
69
  },
43
- default: [],
70
+ },
71
+ ignoreTestFiles: {
72
+ type: 'boolean',
44
73
  },
45
74
  },
46
75
  additionalProperties: false,
@@ -52,6 +81,14 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
52
81
  },
53
82
  defaultOptions,
54
83
  create(context, [options]) {
84
+ // Normalize Windows backslash separators so the forward-slash directory
85
+ // check matches on every platform. Without this, `getFilename()` returns
86
+ // `C:\repo\src\__tests__\Foo.ts` on Windows and the exemption silently
87
+ // fails there.
88
+ const filename = context.getFilename().replace(/\\/g, '/');
89
+ if ((options?.ignoreTestFiles ?? true) && isTestFile(filename)) {
90
+ return {};
91
+ }
55
92
  const sourceCode = context.sourceCode;
56
93
  const sideEffectMatchers = (options?.sideEffectPatterns ?? []).map((pattern) => typeof pattern === 'string' ? new RegExp(pattern, 'i') : pattern);
57
94
  const reportedRanges = new Set();
@@ -1,51 +1,11 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.preferCloneDeep = void 0;
7
- const path_1 = __importDefault(require("path"));
8
4
  const utils_1 = require("@typescript-eslint/utils");
9
5
  const createRule_1 = require("../utils/createRule");
10
6
  const CLONE_DEEP_NAME = 'cloneDeep';
11
7
  const CLONE_DEEP_MODULE = 'functions/src/util/cloneDeep';
12
- const CLONE_DEEP_TARGET = 'src/util/cloneDeep';
13
- const FUNCTIONS_TIER_SEGMENT = '/functions/src/';
14
- const FUNCTIONS_ROOT_SEGMENT = '/functions/';
15
8
  const INDENT_STEP = ' ';
16
- const toPosixPath = (filePath) => filePath.replace(/\\/g, '/');
17
- const ensureRelativeSpecifier = (specifier) => specifier.startsWith('.') ? specifier : `./${specifier}`;
18
- const isWindowsDrivePath = (filePath) => /^[A-Za-z]:[\\/]/.test(filePath);
19
- const isValidRelativePath = (relativePath) => relativePath !== '' &&
20
- !path_1.default.isAbsolute(relativePath) &&
21
- !isWindowsDrivePath(relativePath);
22
- /**
23
- * The helper lives in one place but the two TypeScript tiers reach it
24
- * differently: the root tsconfig maps `functions/*` through `paths`, so files
25
- * outside `functions/` resolve the bare specifier, while `functions/tsconfig.json`
26
- * is rooted at `functions/` and declares no `paths`, leaving backend files able
27
- * to reach a sibling util only by relative path. A single hardcoded specifier
28
- * therefore emits an unresolvable import for every backend fix (#1389).
29
- *
30
- * Returns null when no correct specifier exists, which makes the caller decline
31
- * the fix rather than write an import that cannot resolve.
32
- */
33
- function buildCloneDeepSpecifier(sourceFilePath, cwd) {
34
- const absoluteFilename = toPosixPath(path_1.default.isAbsolute(sourceFilePath)
35
- ? sourceFilePath
36
- : path_1.default.join(cwd, sourceFilePath));
37
- const tierIndex = absoluteFilename.indexOf(FUNCTIONS_TIER_SEGMENT);
38
- if (tierIndex === -1) {
39
- return CLONE_DEEP_MODULE;
40
- }
41
- const functionsRoot = absoluteFilename.slice(0, tierIndex + FUNCTIONS_ROOT_SEGMENT.length);
42
- const targetPath = path_1.default.join(functionsRoot, CLONE_DEEP_TARGET);
43
- const relativePath = path_1.default.relative(path_1.default.dirname(absoluteFilename), targetPath);
44
- if (!isValidRelativePath(relativePath)) {
45
- return null;
46
- }
47
- return ensureRelativeSpecifier(toPosixPath(relativePath));
48
- }
49
9
  /**
50
10
  * Only BluMint's own `cloneDeep` accepts an overrides argument, so an existing
51
11
  * binding coming from anywhere else (notably `lodash`) must not be reused by the
@@ -84,8 +44,6 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
84
44
  // Track processed nodes to avoid duplicate reports
85
45
  const processedNodes = new Set();
86
46
  const sourceCode = context.sourceCode;
87
- const cwd = typeof context.getCwd === 'function' ? context.getCwd() : process.cwd();
88
- const cloneDeepSpecifier = buildCloneDeepSpecifier(context.getFilename(), cwd);
89
47
  function normalizedTextOf(node) {
90
48
  return sourceCode.getText(node).replace(/\s+/g, '');
91
49
  }
@@ -364,56 +322,45 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
364
322
  return targets;
365
323
  }
366
324
  /**
367
- * Returns the fixes required for `cloneDeep` to resolve, an empty list when
368
- * it already does, or null when a conflicting binding of that name exists —
369
- * shadowing it would silently call something else — or when no import
370
- * specifier that resolves from this file can be derived.
325
+ * Whether the rewritten `cloneDeep(...)` call resolves in this file, which is
326
+ * true only where the file already imports the helper as a value.
327
+ *
328
+ * The specifier that reaches the helper is a property of the consuming
329
+ * project, not of this rule: the module is absent from some consumers
330
+ * entirely, and where it exists the two TypeScript tiers reach it by
331
+ * different forms. Writing an import from a guessed specifier therefore
332
+ * trades working code for a build error (#1396), so an existing import is
333
+ * demanded as proof of a path that resolves here — the same policy
334
+ * `enforce-querykey-ts` applies to its own import.
335
+ *
336
+ * A binding of the name from anywhere else is not proof and must not be
337
+ * reused: `lodash`'s `cloneDeep` accepts no overrides argument, a local
338
+ * declaration would shadow the helper, and a namespace or type-only import
339
+ * supplies no callable value.
371
340
  */
372
- function buildImportFixes(fixer, scope) {
341
+ function bindsCloneDeepHelper(scope) {
373
342
  const existing = utils_1.ASTUtils.findVariable(scope, CLONE_DEEP_NAME);
374
- if (existing) {
375
- const [definition] = existing.defs;
376
- if (!definition) {
377
- return null;
378
- }
379
- const definitionNode = definition.node;
380
- if (definitionNode.type !== utils_1.AST_NODE_TYPES.ImportSpecifier &&
381
- definitionNode.type !== utils_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
382
- return null;
383
- }
384
- if (definitionNode.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
385
- definitionNode.importKind === 'type') {
386
- return null;
387
- }
388
- const declaration = definitionNode.parent;
389
- if (!declaration ||
390
- declaration.type !== utils_1.AST_NODE_TYPES.ImportDeclaration ||
391
- declaration.importKind === 'type' ||
392
- !isCloneDeepModule(String(declaration.source.value))) {
393
- return null;
394
- }
395
- return [];
343
+ if (!existing) {
344
+ return false;
396
345
  }
397
- const importDeclarations = sourceCode.ast.body.filter((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
398
- const reusable = importDeclarations.find((declaration) => declaration.importKind !== 'type' &&
399
- isCloneDeepModule(String(declaration.source.value)) &&
400
- declaration.specifiers.some((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier));
401
- if (reusable) {
402
- const namedSpecifiers = reusable.specifiers.filter((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier);
403
- const lastSpecifier = namedSpecifiers[namedSpecifiers.length - 1];
404
- return [fixer.insertTextAfter(lastSpecifier, `, ${CLONE_DEEP_NAME}`)];
346
+ const [definition] = existing.defs;
347
+ if (!definition) {
348
+ return false;
405
349
  }
406
- // Reusing an existing import needs no specifier of its own, so only a
407
- // freshly written import depends on one being derivable.
408
- if (cloneDeepSpecifier === null) {
409
- return null;
350
+ const definitionNode = definition.node;
351
+ if (definitionNode.type !== utils_1.AST_NODE_TYPES.ImportSpecifier &&
352
+ definitionNode.type !== utils_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
353
+ return false;
410
354
  }
411
- const importText = `import { ${CLONE_DEEP_NAME} } from '${cloneDeepSpecifier}';\n`;
412
- const [firstImport] = importDeclarations;
413
- if (firstImport) {
414
- return [fixer.insertTextBefore(firstImport, importText)];
355
+ if (definitionNode.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
356
+ definitionNode.importKind === 'type') {
357
+ return false;
415
358
  }
416
- return [fixer.insertTextBeforeRange([0, 0], importText)];
359
+ const declaration = definitionNode.parent;
360
+ return (!!declaration &&
361
+ declaration.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
362
+ declaration.importKind !== 'type' &&
363
+ isCloneDeepModule(String(declaration.source.value)));
417
364
  }
418
365
  // Find the outermost object expression that needs cloneDeep
419
366
  function findOutermostPartialDeepCopy(node) {
@@ -468,6 +415,9 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
468
415
  node,
469
416
  messageId: 'preferCloneDeep',
470
417
  fix(fixer) {
418
+ if (!bindsCloneDeepHelper(scope)) {
419
+ return null;
420
+ }
471
421
  const rewrites = [];
472
422
  const targets = startsWithSpread(node)
473
423
  ? [node]
@@ -482,11 +432,7 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
482
432
  }
483
433
  rewrites.push(fixer.replaceText(target, call));
484
434
  }
485
- const importFixes = buildImportFixes(fixer, scope);
486
- if (importFixes === null) {
487
- return null;
488
- }
489
- return [...importFixes, ...rewrites];
435
+ return rewrites;
490
436
  },
491
437
  });
492
438
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.21",
3
+ "version": "1.20.22",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,26 @@
1
1
  [
2
+ {
3
+ "version": "1.20.22",
4
+ "date": "2026-07-30T00:39:18.760Z",
5
+ "rules": [
6
+ {
7
+ "name": "parallelize-async-operations",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1395
11
+ ],
12
+ "summary": "exempt test files and stop schema defaults erasing barriers (closes #1395)"
13
+ },
14
+ {
15
+ "name": "prefer-clone-deep",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 1396
19
+ ],
20
+ "summary": "only autofix when the file already imports the helper (closes #1396)"
21
+ }
22
+ ]
23
+ },
2
24
  {
3
25
  "version": "1.20.21",
4
26
  "date": "2026-07-29T23:50:58.620Z",