@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 +1 -1
- package/lib/rules/enforce-dynamic-firebase-imports.d.ts +7 -4
- package/lib/rules/enforce-dynamic-firebase-imports.js +425 -42
- package/lib/rules/enforce-firestore-rules-get-access.d.ts +7 -1
- package/lib/rules/enforce-firestore-rules-get-access.js +220 -4
- package/lib/rules/enforce-global-constants.d.ts +6 -1
- package/lib/rules/enforce-global-constants.js +548 -14
- package/lib/rules/enforce-querykey-ts.d.ts +6 -1
- package/lib/rules/enforce-querykey-ts.js +162 -4
- package/lib/rules/global-const-style.js +182 -10
- package/lib/rules/jsdoc-above-field.js +33 -8
- package/lib/rules/logical-top-to-bottom-grouping.js +148 -2
- package/lib/rules/memo-compare-deeply-complex-props.d.ts +6 -1
- package/lib/rules/memo-compare-deeply-complex-props.js +272 -18
- package/lib/rules/no-array-length-in-deps.d.ts +1 -0
- package/lib/rules/no-array-length-in-deps.js +83 -7
- package/lib/rules/prefer-global-router-state-key.d.ts +6 -1
- package/lib/rules/prefer-global-router-state-key.js +125 -5
- package/lib/rules/prefer-map-over-conditional-dispatch.d.ts +38 -1
- package/lib/rules/prefer-map-over-conditional-dispatch.js +880 -49
- package/lib/rules/prefer-sx-prop-over-system-props.js +180 -14
- package/lib/rules/require-image-optimized.js +27 -4
- package/lib/rules/require-memo.d.ts +6 -1
- package/lib/rules/require-memo.js +236 -30
- package/lib/rules/use-latest-callback.js +203 -23
- package/package.json +1 -1
- package/release-manifest.json +148 -0
package/lib/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { TSESLint
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
:
|
|
129
|
-
|
|
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
|
|
133
|
-
|
|
391
|
+
const declarations = [
|
|
392
|
+
{
|
|
393
|
+
binding: { kind: 'name', name: nsLocal },
|
|
394
|
+
initializer: { kind: 'import', path: importPath },
|
|
395
|
+
},
|
|
134
396
|
];
|
|
135
397
|
if (defaultSpecifier) {
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
415
|
+
return declarations;
|
|
144
416
|
}
|
|
145
|
-
const
|
|
417
|
+
const destructureProperties = [
|
|
146
418
|
...(defaultSpecifier
|
|
147
|
-
? [
|
|
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
|
|
425
|
+
return destructureProperties.length > 0
|
|
154
426
|
? [
|
|
155
|
-
|
|
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
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
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,
|
|
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
|
|
243
|
-
|
|
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
|
|
250
|
-
if (!target ||
|
|
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,
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
3
|
+
type Options = [
|
|
4
|
+
{
|
|
5
|
+
printWidth?: number;
|
|
6
|
+
}
|
|
7
|
+
];
|
|
8
|
+
export declare const enforceFirestoreRulesGetAccess: TSESLint.RuleModule<MessageIds, Options, TSESLint.RuleListener>;
|
|
3
9
|
export {};
|