@blumintinc/eslint-plugin-blumint 1.20.168 → 1.20.170

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.168',
226
+ version: '1.20.170',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -1,5 +1,8 @@
1
- import { TSESLint, TSESTree } from '@typescript-eslint/utils';
2
- declare const enforceFirebaseImports: TSESLint.RuleModule<"noDynamicImport", never[], {} | {
3
- ImportDeclaration(node: TSESTree.ImportDeclaration): void;
4
- }>;
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ type Options = [
3
+ {
4
+ printWidth?: number;
5
+ }
6
+ ];
7
+ declare const enforceFirebaseImports: TSESLint.RuleModule<"noDynamicImport", Options, TSESLint.RuleListener>;
5
8
  export default enforceFirebaseImports;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const utils_1 = require("@typescript-eslint/utils");
4
4
  const createRule_1 = require("../utils/createRule");
5
+ const replacementSegments_1 = require("../utils/replacementSegments");
5
6
  const isFunctionNode = (node) => node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
6
7
  node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
7
8
  node.type === utils_1.AST_NODE_TYPES.FunctionExpression;
@@ -65,6 +66,236 @@ const TEST_FILE_DIRECTORY = /(^|\/)(__tests__|__mocks__)\//;
65
66
  const isNeverBundled = (filename) => filename.endsWith('.d.ts') ||
66
67
  TEST_FILE_SUFFIX.test(filename) ||
67
68
  TEST_FILE_DIRECTORY.test(filename);
69
+ /**
70
+ * Prettier's own default. The fixer authors a whole statement that a formatter
71
+ * owns, so a line it emits past this width is rewritten on the next
72
+ * `prettier --write` — and fails `prettier --check` in the meantime.
73
+ */
74
+ const DEFAULT_PRINT_WIDTH = 80;
75
+ /**
76
+ * The file's own nesting step, taken as the most common indentation increase
77
+ * between consecutive lines. Reading it from the source keeps emitted code in
78
+ * the author's units instead of assuming a two-space, space-indented file.
79
+ */
80
+ const indentUnitOf = (sourceCode) => {
81
+ const blockComments = sourceCode
82
+ .getAllComments()
83
+ .filter((comment) => comment.type === utils_1.AST_TOKEN_TYPES.Block)
84
+ .map((comment) => comment.range);
85
+ // A block comment's interior lines carry the comment's own alignment, which
86
+ // is not a nesting step of the file; counting them makes a JSDoc-heavy file
87
+ // look 1-space indented.
88
+ const continuesBlockComment = (offset) => blockComments.some(([start, end]) => start < offset && offset < end);
89
+ const frequencies = new Map();
90
+ let previous = '';
91
+ let offset = 0;
92
+ for (const line of sourceCode.getText().split('\n')) {
93
+ const lineStart = offset;
94
+ offset += line.length + 1;
95
+ if (line.trim() === '' || continuesBlockComment(lineStart)) {
96
+ continue;
97
+ }
98
+ const indent = /^[ \t]*/.exec(line)?.[0] ?? '';
99
+ if (indent.length > previous.length && indent.startsWith(previous)) {
100
+ const delta = indent.slice(previous.length);
101
+ frequencies.set(delta, (frequencies.get(delta) ?? 0) + 1);
102
+ }
103
+ previous = indent;
104
+ }
105
+ let unit = ' ';
106
+ let best = 0;
107
+ for (const [delta, count] of frequencies) {
108
+ if (count > best) {
109
+ unit = delta;
110
+ best = count;
111
+ }
112
+ }
113
+ return unit;
114
+ };
115
+ /**
116
+ * A per-line transform moving text written at `fromIndent` to `toIndent`, or
117
+ * null when neither indentation is a prefix of the other (tabs against spaces),
118
+ * where no delta can be applied without corrupting the layout.
119
+ */
120
+ const lineShifterBetween = (fromIndent, toIndent) => {
121
+ if (fromIndent === toIndent) {
122
+ return (line) => line;
123
+ }
124
+ if (fromIndent.startsWith(toIndent)) {
125
+ const removed = fromIndent.slice(toIndent.length);
126
+ return (line) => line.startsWith(removed) ? line.slice(removed.length) : line;
127
+ }
128
+ if (toIndent.startsWith(fromIndent)) {
129
+ const added = toIndent.slice(fromIndent.length);
130
+ return (line) => `${added}${line}`;
131
+ }
132
+ return null;
133
+ };
134
+ /**
135
+ * Ranges whose interior line breaks carry string data rather than formatting.
136
+ * A multi-line template literal evaluates to the whitespace written inside it,
137
+ * so shifting those lines would silently change the value the code produces.
138
+ */
139
+ const stringDataRangesOf = (sourceCode, node) => sourceCode
140
+ .getTokens(node)
141
+ .filter((token) => (token.type === utils_1.AST_TOKEN_TYPES.Template ||
142
+ token.type === utils_1.AST_TOKEN_TYPES.String) &&
143
+ token.loc.start.line !== token.loc.end.line)
144
+ .map((token) => token.range);
145
+ /**
146
+ * The source spanned by `range` with its continuation lines moved from the
147
+ * depth they were written at to `toIndent`, or null when that move is not
148
+ * expressible.
149
+ *
150
+ * An expression spliced out of a concise body lands one nesting level deeper
151
+ * once the arrow gains a block, so every line after the first would otherwise
152
+ * keep the column it had at the shallower depth (issue #2057). The first line
153
+ * is excluded because it is spliced in directly after `return `, where it has
154
+ * no indentation of its own left to adjust.
155
+ */
156
+ const reindentedRange = (sourceCode, range, stringData, fromIndent, toIndent) => {
157
+ const text = sourceCode.getText().slice(range[0], range[1]);
158
+ if (!text.includes('\n')) {
159
+ return text;
160
+ }
161
+ const shiftLine = lineShifterBetween(fromIndent, toIndent);
162
+ if (!shiftLine) {
163
+ return null;
164
+ }
165
+ const carriesStringData = (offset) => stringData.some(([start, end]) => start < offset && offset < end);
166
+ let offset = range[0];
167
+ return text
168
+ .split('\n')
169
+ .map((line, index) => {
170
+ const lineStart = offset;
171
+ offset += line.length + 1;
172
+ if (index === 0 || line.trim() === '' || carriesStringData(lineStart)) {
173
+ return line;
174
+ }
175
+ return shiftLine(line);
176
+ })
177
+ .join('\n');
178
+ };
179
+ /**
180
+ * Type syntax that wraps an expression without changing where its parentheses
181
+ * are needed, so an object literal behind one is still an object literal for
182
+ * the purposes of {@link wrapsObjectLiteral}.
183
+ */
184
+ const TYPE_WRAPPER_EXPRESSIONS = new Set([
185
+ utils_1.AST_NODE_TYPES.TSAsExpression,
186
+ utils_1.AST_NODE_TYPES.TSNonNullExpression,
187
+ utils_1.AST_NODE_TYPES.TSSatisfiesExpression,
188
+ utils_1.AST_NODE_TYPES.TSTypeAssertion,
189
+ ]);
190
+ /**
191
+ * Whether the parentheses around a concise body exist only to keep its leading
192
+ * brace from parsing as a block.
193
+ *
194
+ * Those are the parentheses `return` makes dead, since a return argument is
195
+ * already an expression position. Every other parenthesized body keeps them:
196
+ * a broken-open binary expression or a multi-line JSX element is printed
197
+ * parenthesized in return position too, so dropping them there would trade one
198
+ * formatter rewrite for another.
199
+ */
200
+ const wrapsObjectLiteral = (node) => {
201
+ let current = node;
202
+ while (TYPE_WRAPPER_EXPRESSIONS.has(current.type)) {
203
+ current = current
204
+ .expression;
205
+ }
206
+ return current.type === utils_1.AST_NODE_TYPES.ObjectExpression;
207
+ };
208
+ const propertyText = (property) => property.value === undefined
209
+ ? property.key
210
+ : `${property.key}: ${property.value}`;
211
+ /** Everything a broken-open `await import(...)` keeps on the line before its argument. */
212
+ const IMPORT_CALL_HEAD = 'await import(';
213
+ /**
214
+ * Prettier expands an object pattern of more than two properties as soon as one
215
+ * of them is renamed, whatever the line would otherwise measure
216
+ * (`isComplexDestructuring`). A renamed specifier and the `default:` entry are
217
+ * both non-shorthand, so the emitted pattern has to answer that rule as well as
218
+ * the width to survive `prettier --check`.
219
+ */
220
+ const isComplexPattern = (binding) => binding.kind === 'pattern' &&
221
+ binding.properties.length > 2 &&
222
+ binding.properties.some((property) => property.value !== undefined);
223
+ /**
224
+ * One property of an expanded pattern. A renamed binding whose own line
225
+ * overflows breaks after its `:`, which is the last break point the pattern
226
+ * has; a shorthand one has none, so it is printed as is — exactly what Prettier
227
+ * does with a name too long for the line it lands on.
228
+ */
229
+ const printProperty = (property, indent, indentUnit, printWidth) => {
230
+ const inline = `${indent}${propertyText(property)},`;
231
+ if (property.value === undefined || inline.length <= printWidth) {
232
+ return inline;
233
+ }
234
+ return `${indent}${property.key}:\n${indent}${indentUnit}${property.value},`;
235
+ };
236
+ const inlineBindingOf = (binding) => binding.kind === 'name'
237
+ ? binding.name
238
+ : `{ ${binding.properties.map(propertyText).join(', ')} }`;
239
+ const inlineInitializerOf = (initializer) => initializer.kind === 'import'
240
+ ? `${IMPORT_CALL_HEAD}'${initializer.path}')`
241
+ : initializer.text;
242
+ /** The whole declaration on one line, with no break opportunity taken. */
243
+ const printInline = (declaration) => `const ${inlineBindingOf(declaration.binding)} = ${inlineInitializerOf(declaration.initializer)};`;
244
+ /**
245
+ * Prints the declaration in the shape Prettier prints it at `indent`.
246
+ *
247
+ * The specifier list and the module path both come from the source import, so
248
+ * the one-line form has no length bound and overflows on ordinary firebaseCloud
249
+ * paths. Wrapping unconditionally is the mirror failure: Prettier collapses an
250
+ * expanded destructuring pattern, argument list or assignment back onto one line
251
+ * as soon as it fits, so the width — not the shape of the input — decides.
252
+ *
253
+ * Every branch below is a shape Prettier itself emits, so there is no
254
+ * precondition that can fail and no line the measurement rejects yet still gets
255
+ * printed.
256
+ */
257
+ const printDeclaration = (declaration, indent, indentUnit, printWidth) => {
258
+ const { binding, initializer } = declaration;
259
+ const oneLine = printInline(declaration);
260
+ if (indent.length + oneLine.length <= printWidth) {
261
+ return oneLine;
262
+ }
263
+ const inlineHead = `const ${inlineBindingOf(binding)} =`;
264
+ // An expanded pattern moves the `=` onto the closing brace's line, so it is
265
+ // that line — not the head — the initializer is measured against.
266
+ const expanded = binding.kind === 'pattern' &&
267
+ (isComplexPattern(binding) ||
268
+ indent.length + inlineHead.length > printWidth)
269
+ ? {
270
+ text: `const {\n${binding.properties
271
+ .map((property) => printProperty(property, `${indent}${indentUnit}`, indentUnit, printWidth))
272
+ .join('\n')}\n${indent}} =`,
273
+ tail: '} =',
274
+ }
275
+ : { text: inlineHead, tail: inlineHead };
276
+ const inlineInitializer = inlineInitializerOf(initializer);
277
+ const tailColumn = indent.length + expanded.tail.length;
278
+ // The initializer still fits after an expanded pattern's `} =`.
279
+ if (tailColumn + inlineInitializer.length + 2 <= printWidth) {
280
+ return `${expanded.text} ${inlineInitializer};`;
281
+ }
282
+ // Prettier breaks the call's argument before it breaks after the `=`, so long
283
+ // as the call head still fits on the line the `=` sits on.
284
+ const rhsIndent = `${indent}${indentUnit}`;
285
+ if (initializer.kind === 'import' &&
286
+ tailColumn + IMPORT_CALL_HEAD.length + 1 <= printWidth) {
287
+ return `${expanded.text} ${IMPORT_CALL_HEAD}\n${rhsIndent}'${initializer.path}'\n${indent});`;
288
+ }
289
+ // Nothing fits beside the `=`: the initializer takes the next line, and
290
+ // breaks its own argument there when even that line overflows. A module path
291
+ // wider than the line it lands on is emitted as is — Prettier cannot break a
292
+ // string literal either, so that is already its output.
293
+ if (initializer.kind === 'import' &&
294
+ rhsIndent.length + inlineInitializer.length + 1 > printWidth) {
295
+ return `${expanded.text}\n${rhsIndent}${IMPORT_CALL_HEAD}\n${rhsIndent}${indentUnit}'${initializer.path}'\n${rhsIndent});`;
296
+ }
297
+ return `${expanded.text}\n${rhsIndent}${inlineInitializer};`;
298
+ };
68
299
  const enforceFirebaseImports = (0, createRule_1.createRule)({
69
300
  name: 'enforce-dynamic-firebase-imports',
70
301
  meta: {
@@ -75,14 +306,37 @@ const enforceFirebaseImports = (0, createRule_1.createRule)({
75
306
  },
76
307
  fixable: 'code',
77
308
  hasSuggestions: true,
78
- schema: [],
309
+ schema: [
310
+ {
311
+ type: 'object',
312
+ properties: {
313
+ printWidth: {
314
+ type: 'number',
315
+ minimum: 1,
316
+ },
317
+ },
318
+ additionalProperties: false,
319
+ },
320
+ ],
79
321
  messages: {
80
322
  noDynamicImport: 'Static import from firebaseCloud path "{{importPath}}" eagerly bundles Firebase code into the initial client chunk, which inflates startup time and prevents lazy loading. Load it at the call site instead, inside an async function body (e.g., `const { export } = await import(\'{{importPath}}\')`). Keep it out of module scope: a top-level `await import(...)` defers nothing and does not parse once the module is compiled to CommonJS.',
81
323
  },
82
324
  },
83
- defaultOptions: [],
84
- create(context) {
325
+ defaultOptions: [{}],
326
+ create(context, [options]) {
85
327
  const sourceCode = context.getSourceCode();
328
+ const printWidth = typeof options.printWidth === 'number' && options.printWidth > 0
329
+ ? options.printWidth
330
+ : DEFAULT_PRINT_WIDTH;
331
+ // Derived once per file rather than per fix: every fix in a file shares the
332
+ // author's nesting step.
333
+ let cachedIndentUnit = null;
334
+ const fileIndentUnit = () => {
335
+ if (cachedIndentUnit === null) {
336
+ cachedIndentUnit = indentUnitOf(sourceCode);
337
+ }
338
+ return cachedIndentUnit;
339
+ };
86
340
  // Normalize Windows backslash separators so the forward-slash directory
87
341
  // checks match on every platform. Without this, `getFilename()` returns
88
342
  // `C:\repo\src\hooks\__tests__\Foo.ts` on Windows and the exemption
@@ -124,38 +378,63 @@ const enforceFirebaseImports = (0, createRule_1.createRule)({
124
378
  : `${spec.imported.name} as ${spec.local.name}`)
125
379
  .join(', ');
126
380
  const destructureEntry = (spec) => spec.imported.name === spec.local.name
127
- ? spec.local.name
128
- : `${spec.imported.name}: ${spec.local.name}`;
129
- const buildValueStatements = () => {
381
+ ? { key: spec.local.name }
382
+ : { key: spec.imported.name, value: spec.local.name };
383
+ /**
384
+ * The declarations to relocate, as structure rather than text: the line
385
+ * they land on is only known at the insertion site, and its width is
386
+ * what decides their printed shape.
387
+ */
388
+ const buildDeclarations = () => {
130
389
  if (namespaceSpecifier) {
131
390
  const nsLocal = namespaceSpecifier.local.name;
132
- const statements = [
133
- `const ${nsLocal} = await import('${importPath}');`,
391
+ const declarations = [
392
+ {
393
+ binding: { kind: 'name', name: nsLocal },
394
+ initializer: { kind: 'import', path: importPath },
395
+ },
134
396
  ];
135
397
  if (defaultSpecifier) {
136
- statements.push(`const ${defaultSpecifier.local.name} = ${nsLocal}.default;`);
398
+ declarations.push({
399
+ binding: { kind: 'name', name: defaultSpecifier.local.name },
400
+ initializer: {
401
+ kind: 'expression',
402
+ text: `${nsLocal}.default`,
403
+ },
404
+ });
137
405
  }
138
406
  if (namedSpecifiers.length > 0) {
139
- statements.push(`const { ${namedSpecifiers
140
- .map(destructureEntry)
141
- .join(', ')} } = ${nsLocal};`);
407
+ declarations.push({
408
+ binding: {
409
+ kind: 'pattern',
410
+ properties: namedSpecifiers.map(destructureEntry),
411
+ },
412
+ initializer: { kind: 'expression', text: nsLocal },
413
+ });
142
414
  }
143
- return statements;
415
+ return declarations;
144
416
  }
145
- const destructureParts = [
417
+ const destructureProperties = [
146
418
  ...(defaultSpecifier
147
- ? [`default: ${defaultSpecifier.local.name}`]
419
+ ? [{ key: 'default', value: defaultSpecifier.local.name }]
148
420
  : []),
149
421
  ...namedSpecifiers.map(destructureEntry),
150
422
  ];
151
423
  // A side-effect import binds nothing, so there is no declaration to
152
424
  // relocate — the awaited call would have to stay at module scope.
153
- return destructureParts.length > 0
425
+ return destructureProperties.length > 0
154
426
  ? [
155
- `const { ${destructureParts.join(', ')} } = await import('${importPath}');`,
427
+ {
428
+ binding: {
429
+ kind: 'pattern',
430
+ properties: destructureProperties,
431
+ },
432
+ initializer: { kind: 'import', path: importPath },
433
+ },
156
434
  ]
157
435
  : [];
158
436
  };
437
+ const printAt = (declarations, indent) => declarations.map((declaration) => printDeclaration(declaration, indent, fileIndentUnit(), printWidth));
159
438
  /**
160
439
  * An `ImportDeclaration` only ever sits at module scope, so rewriting
161
440
  * it in place can only ever produce a module-scope `await import(...)`
@@ -194,6 +473,34 @@ const enforceFirebaseImports = (0, createRule_1.createRule)({
194
473
  return target;
195
474
  };
196
475
  const indentationAt = (line) => /^[ \t]*/.exec(sourceCode.lines[line - 1] ?? '')?.[0] ?? '';
476
+ const commentSegment = (comment) => ({
477
+ text: sourceCode.getText().slice(comment.range[0], comment.range[1]),
478
+ breakAfter: (0, replacementSegments_1.requiresLineBreakAfter)(comment),
479
+ });
480
+ /**
481
+ * The comments written inside the import, as one run of text to
482
+ * re-emit ahead of the relocated declaration.
483
+ *
484
+ * The declaration is removed — or, in the type-only branch, re-authored
485
+ * from its parts — wholesale, so a comment inside it has no anchor in
486
+ * the replacement. Its subject survives, though: every value specifier
487
+ * reappears in the emitted destructuring pattern, so the comment has
488
+ * somewhere to go and dropping it would be the fixer writing text it
489
+ * does not own. Declining instead would only let a comment decide
490
+ * whether the rewrite happens at all, which is the same violation seen
491
+ * from the other side (#1877), so the comments are carried (#2056).
492
+ *
493
+ * A `//` comment swallows whatever follows it on its line, so the run
494
+ * breaks wherever `requiresLineBreakAfter` says it must, and the
495
+ * declaration always begins on the line after it.
496
+ */
497
+ const carriedImportComments = (indent) => {
498
+ const comments = sourceCode.getCommentsInside(node);
499
+ if (comments.length === 0) {
500
+ return null;
501
+ }
502
+ return (0, replacementSegments_1.joinSegmentBody)(comments.map(commentSegment), indent);
503
+ };
197
504
  /**
198
505
  * Consumes the import's own trailing whitespace, and its line break
199
506
  * when the import owns the line, so the removal strands neither a blank
@@ -216,38 +523,100 @@ const enforceFirebaseImports = (0, createRule_1.createRule)({
216
523
  }
217
524
  return cursor;
218
525
  };
526
+ /**
527
+ * The span of source the concise body's `return` takes as its argument.
528
+ *
529
+ * It is copied out of the file rather than reprinted from the AST,
530
+ * because the parentheses around an object literal are not part of the
531
+ * literal's node and a broken-open expression's own line breaks are not
532
+ * recoverable from it. The one thing left behind is a parenthesis pair
533
+ * that exists solely to keep a leading brace from parsing as a block:
534
+ * `return` already supplies an expression position, so those are dead
535
+ * and a formatter strips them (#2057). Any other parenthesized body
536
+ * keeps its parentheses — a broken-open binary expression and a
537
+ * multi-line JSX element are printed parenthesized in return position
538
+ * too.
539
+ */
540
+ const returnedExpressionRange = (arrow, arrowToken) => {
541
+ const first = sourceCode.getTokenAfter(arrowToken);
542
+ const last = sourceCode.getLastToken(arrow);
543
+ const disambiguatingParens = !!first &&
544
+ !!last &&
545
+ first.type === utils_1.AST_TOKEN_TYPES.Punctuator &&
546
+ first.value === '(' &&
547
+ last.type === utils_1.AST_TOKEN_TYPES.Punctuator &&
548
+ last.value === ')' &&
549
+ first.range[1] <= arrow.body.range[0] &&
550
+ last.range[0] >= arrow.body.range[1] &&
551
+ wrapsObjectLiteral(arrow.body);
552
+ return disambiguatingParens
553
+ ? [arrow.body.range[0], arrow.body.range[1]]
554
+ : [first ? first.range[0] : arrowToken.range[1], arrow.range[1]];
555
+ };
219
556
  /**
220
557
  * Gives a concise-bodied arrow the block its declaration needs, turning
221
558
  * the returned expression into an explicit `return`.
222
559
  *
223
- * The expression is spliced verbatim out of the source rather than
224
- * reprinted from the AST: the parentheses around an object literal are
225
- * not part of its node, and a comment sitting between `=>` and the
226
- * expression belongs to neither, so both survive only by copying the
227
- * text the arrow already owns.
560
+ * The expression moves one nesting level deeper on the way in, so its
561
+ * continuation lines are shifted by that delta rather than spliced at
562
+ * the column they were written at everything a multi-line concise
563
+ * body holds would otherwise land under-indented inside the block it
564
+ * gains (#2057). Lines whose breaks belong to a template literal are
565
+ * left alone: their whitespace is the string's own value.
566
+ *
567
+ * A comment written between `=>` and the expression annotates neither
568
+ * node, so it is carried across explicitly. One that cannot share a
569
+ * line with what follows takes a line of its own ABOVE the `return`:
570
+ * after `return` a line break — or a block comment carrying one, which
571
+ * the grammar reads as a line terminator — triggers ASI and silently
572
+ * replaces the returned value with `undefined` (#1963).
228
573
  */
229
- const blockifyConciseBody = (fixer, arrow, statements) => {
574
+ const blockifyConciseBody = (fixer, arrow, declarations) => {
230
575
  const arrowToken = sourceCode.getTokenBefore(arrow.body, {
231
576
  filter: (token) => token.type === utils_1.AST_TOKEN_TYPES.Punctuator && token.value === '=>',
232
577
  });
233
578
  if (!arrowToken) {
234
579
  return null;
235
580
  }
236
- const expression = sourceCode
237
- .getText()
238
- .slice(arrowToken.range[1], arrow.range[1])
239
- .trim();
240
581
  const indent = indentationAt(arrow.loc.start.line);
241
- const bodyIndent = `${indent} `;
242
- const lines = [...statements, `return ${expression};`]
243
- .map((statement) => `\n${bodyIndent}${statement}`)
582
+ const bodyIndent = `${indent}${fileIndentUnit()}`;
583
+ const range = returnedExpressionRange(arrow, arrowToken);
584
+ const expression = reindentedRange(sourceCode, range, stringDataRangesOf(sourceCode, arrow), indentationAt(sourceCode.getLocFromIndex(range[0]).line), bodyIndent);
585
+ // Tab-indented source under a space-indented block, or the reverse,
586
+ // admits no expressible delta; the body would land mangled, so the
587
+ // fix is withheld and the report stands.
588
+ if (expression === null) {
589
+ return null;
590
+ }
591
+ const outer = sourceCode
592
+ .getCommentsInside(arrow)
593
+ .filter((comment) => comment.range[0] >= arrowToken.range[1] &&
594
+ (comment.range[1] <= range[0] || comment.range[0] >= range[1]));
595
+ const leading = outer.filter((comment) => comment.range[1] <= range[0]);
596
+ const trailing = outer.filter((comment) => comment.range[0] >= range[1]);
597
+ const hoisted = leading.filter(replacementSegments_1.requiresOwnLine);
598
+ const statement = `return ${(0, replacementSegments_1.joinSegmentBody)([
599
+ ...leading
600
+ .filter((comment) => !(0, replacementSegments_1.requiresOwnLine)(comment))
601
+ .map(commentSegment),
602
+ { text: `${expression};`, breakAfter: false },
603
+ ...trailing.map(commentSegment),
604
+ ], bodyIndent)}`;
605
+ const carried = carriedImportComments(bodyIndent);
606
+ const lines = [
607
+ ...(carried === null ? [] : [carried]),
608
+ ...printAt(declarations, bodyIndent),
609
+ ...hoisted.map((comment) => commentSegment(comment).text),
610
+ statement,
611
+ ]
612
+ .map((emitted) => `\n${bodyIndent}${emitted}`)
244
613
  .join('');
245
614
  return fixer.replaceTextRange([arrowToken.range[1], arrow.range[1]], ` {${lines}\n${indent}}`);
246
615
  };
247
616
  const buildFix = (fixer) => {
248
617
  const target = findRelocationTarget();
249
- const statements = buildValueStatements();
250
- if (!target || statements.length === 0) {
618
+ const declarations = buildDeclarations();
619
+ if (!target || declarations.length === 0) {
251
620
  return null;
252
621
  }
253
622
  // Type-only specifiers are erased at compile time, so they stay where
@@ -257,7 +626,7 @@ const enforceFirebaseImports = (0, createRule_1.createRule)({
257
626
  : fixer.removeRange([node.range[0], removalEnd()]);
258
627
  if (target.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
259
628
  target.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
260
- const blockified = blockifyConciseBody(fixer, target, statements);
629
+ const blockified = blockifyConciseBody(fixer, target, declarations);
261
630
  return blockified ? [importEdit, blockified] : null;
262
631
  }
263
632
  const body = target.body;
@@ -277,15 +646,29 @@ const enforceFirebaseImports = (0, createRule_1.createRule)({
277
646
  const neighbour = following ?? lastDirective;
278
647
  // A body written on one line keeps its shape; a multi-line body gets
279
648
  // the declaration on its own line at the body's own indentation.
280
- const insertion = following && following.loc.start.line === anchorLine
281
- ? ` ${statements.join(' ')}`
282
- : statements
283
- .map((statement) => {
284
- const indent = neighbour
285
- ? indentationAt(neighbour.loc.start.line)
286
- : `${indentationAt(target.loc.start.line)} `;
287
- return `\n${indent}${statement}`;
288
- })
649
+ //
650
+ // The one-line body is the single emission the print width does not
651
+ // govern: a block body holding statements is a shape no formatter
652
+ // prints on one line at all, so there is no width at which the
653
+ // author's layout survives and no wrapped form that would restore it.
654
+ // Breaking the declaration open there would abandon that layout
655
+ // without buying anything. Every other emission lands on a fresh line
656
+ // whose column is known, and is printed against it.
657
+ const inlineBody = Boolean(following && following.loc.start.line === anchorLine);
658
+ const bodyIndent = neighbour && !inlineBody
659
+ ? indentationAt(neighbour.loc.start.line)
660
+ : `${indentationAt(target.loc.start.line)}${fileIndentUnit()}`;
661
+ // A carried comment forces the multi-line form even for a one-line
662
+ // body: a `//` comment appended to that line would swallow the rest
663
+ // of it, and the comment-free emission is unchanged either way.
664
+ const carried = carriedImportComments(bodyIndent);
665
+ const insertion = inlineBody && carried === null
666
+ ? ` ${declarations.map(printInline).join(' ')}`
667
+ : [
668
+ ...(carried === null ? [] : [carried]),
669
+ ...printAt(declarations, bodyIndent),
670
+ ]
671
+ .map((statement) => `\n${bodyIndent}${statement}`)
289
672
  .join('');
290
673
  return [
291
674
  importEdit,
@@ -1,3 +1,9 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
1
2
  type MessageIds = 'useGetAccess' | 'requireGetDefault';
2
- export declare const enforceFirestoreRulesGetAccess: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
3
+ type Options = [
4
+ {
5
+ printWidth?: number;
6
+ }
7
+ ];
8
+ export declare const enforceFirestoreRulesGetAccess: TSESLint.RuleModule<MessageIds, Options, TSESLint.RuleListener>;
3
9
  export {};