@discourse/lint-configs 2.44.0 → 2.45.0
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/eslint-rules/i18n-import-location.mjs +9 -9
- package/eslint-rules/no-computed-macros/computed-macros-analysis.mjs +903 -0
- package/eslint-rules/no-computed-macros/computed-macros-fixer.mjs +612 -0
- package/eslint-rules/no-computed-macros/macro-transforms.mjs +645 -0
- package/eslint-rules/no-computed-macros.mjs +436 -0
- package/eslint-rules/no-discourse-computed.mjs +21 -21
- package/eslint-rules/truth-helpers-imports.mjs +9 -5
- package/eslint-rules/utils/fix-import.mjs +32 -12
- package/eslint.mjs +4 -8
- package/package.json +3 -3
- package/stylelint.mjs +6 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Fixer logic for the `no-computed-macros` ESLint rule.
|
|
3
|
+
*
|
|
4
|
+
* Generates a combined ESLint fixer function per class that:
|
|
5
|
+
* 1. Removes each macro PropertyDefinition
|
|
6
|
+
* 2. Collects @tracked declarations and inserts them at the correct
|
|
7
|
+
* sort-class-members section:
|
|
8
|
+
* - Regular tracked → [tracked-properties]
|
|
9
|
+
* - Override fields (e.g. oneWay `_propOverride`) → [private-properties]
|
|
10
|
+
* 3. Inserts all generated getters at the correct class body position
|
|
11
|
+
* (the [everything-else] section, after all property-like members)
|
|
12
|
+
*
|
|
13
|
+
* This "remove + insert elsewhere" approach produces correct
|
|
14
|
+
* sort-class-members order in a single --fix pass, avoiding the messy
|
|
15
|
+
* intermediate state of in-place replacement.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// Names of lifecycle methods that have their own sort-class-members slot
|
|
19
|
+
// (they come before [everything-else] in the sort order).
|
|
20
|
+
const LIFECYCLE_METHODS = new Set(["constructor", "init", "willDestroy"]);
|
|
21
|
+
|
|
22
|
+
// Decorators that make a property reactive (equivalent to @tracked).
|
|
23
|
+
// Members with these decorators do not need @tracked added.
|
|
24
|
+
const TRACKED_DECORATORS = new Set([
|
|
25
|
+
"tracked",
|
|
26
|
+
"trackedArray",
|
|
27
|
+
"dedupeTracked",
|
|
28
|
+
"resettableTracked",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Public API
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a combined fixer function for ALL fixable macro usages in one class.
|
|
37
|
+
*
|
|
38
|
+
* The returned function produces an array of Fix objects that:
|
|
39
|
+
* - Remove each macro PropertyDefinition
|
|
40
|
+
* - Collect and insert @tracked declarations at the tracked section
|
|
41
|
+
* - Insert all generated getters at the correct position
|
|
42
|
+
*
|
|
43
|
+
* @param {import('./computed-macros-analysis.mjs').MacroUsage[]} classUsages
|
|
44
|
+
* @param {Set<import('estree').Node>} existingNodesToDecorate
|
|
45
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
46
|
+
* @returns {(fixer: import('eslint').Rule.RuleFixer) => import('eslint').Rule.Fix[]}
|
|
47
|
+
*/
|
|
48
|
+
export function createClassFix(
|
|
49
|
+
classUsages,
|
|
50
|
+
existingNodesToDecorate,
|
|
51
|
+
sourceCode
|
|
52
|
+
) {
|
|
53
|
+
return function (fixer) {
|
|
54
|
+
const fixes = [];
|
|
55
|
+
const text = sourceCode.getText();
|
|
56
|
+
const classBody = classUsages[0].propertyNode.parent;
|
|
57
|
+
const indent = detectIndent(classUsages[0].propertyNode, sourceCode);
|
|
58
|
+
const macroNodeSet = new Set(classUsages.map((u) => u.propertyNode));
|
|
59
|
+
|
|
60
|
+
// ---- 1. Remove all macro PropertyDefinitions ----
|
|
61
|
+
for (const usage of classUsages) {
|
|
62
|
+
let start = getNodeStart(usage, sourceCode);
|
|
63
|
+
let end = getNodeEnd(usage.propertyNode, sourceCode);
|
|
64
|
+
// Consume trailing newline for clean line removal
|
|
65
|
+
if (end < text.length && text[end] === "\n") {
|
|
66
|
+
end++;
|
|
67
|
+
}
|
|
68
|
+
// Consume one preceding blank line to avoid orphaned double blank
|
|
69
|
+
// lines after the macro is removed
|
|
70
|
+
if (start >= 2 && text[start - 1] === "\n" && text[start - 2] === "\n") {
|
|
71
|
+
start--;
|
|
72
|
+
}
|
|
73
|
+
fixes.push(fixer.replaceTextRange([start, end], ""));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---- 2. Collect @tracked declarations ----
|
|
77
|
+
// Regular tracked (from trackedDeps / existingNodesToDecorate) go to
|
|
78
|
+
// [tracked-properties]; override fields (from overrideTrackedFields,
|
|
79
|
+
// e.g. oneWay's `_propOverride`) go to [private-properties] because
|
|
80
|
+
// their `_` prefix matches the sort-class-members private pattern.
|
|
81
|
+
const trackedLines = [];
|
|
82
|
+
const privateTrackedLines = [];
|
|
83
|
+
const seenTrackedDeps = new Set();
|
|
84
|
+
const processedNodes = new Set();
|
|
85
|
+
|
|
86
|
+
for (const usage of classUsages) {
|
|
87
|
+
// Override tracked fields → [private-properties] section
|
|
88
|
+
if (usage.transform.overrideTrackedFields) {
|
|
89
|
+
for (const field of usage.transform.overrideTrackedFields({
|
|
90
|
+
propName: usage.propName,
|
|
91
|
+
})) {
|
|
92
|
+
const line =
|
|
93
|
+
field.initializer != null
|
|
94
|
+
? `${indent}@tracked ${field.name} = ${field.initializer};\n`
|
|
95
|
+
: `${indent}@tracked ${field.name};\n`;
|
|
96
|
+
privateTrackedLines.push(line);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// New @tracked declarations from trackedDeps → [tracked-properties]
|
|
101
|
+
if (usage.allLocal && usage.trackedDeps?.length > 0) {
|
|
102
|
+
for (const dep of usage.trackedDeps) {
|
|
103
|
+
if (!seenTrackedDeps.has(dep)) {
|
|
104
|
+
seenTrackedDeps.add(dep);
|
|
105
|
+
trackedLines.push(`${indent}@tracked ${dep};\n`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Move existing members with @tracked prepended → [tracked-properties]
|
|
111
|
+
if (usage.existingNodesToDecorate) {
|
|
112
|
+
for (const memberNode of usage.existingNodesToDecorate) {
|
|
113
|
+
if (processedNodes.has(memberNode)) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
processedNodes.add(memberNode);
|
|
117
|
+
|
|
118
|
+
// Capture source text (key through trailing semicolons)
|
|
119
|
+
let endPos = memberNode.range[1];
|
|
120
|
+
while (endPos < text.length && text[endPos] === ";") {
|
|
121
|
+
endPos++;
|
|
122
|
+
}
|
|
123
|
+
const memberSource = text.slice(memberNode.range[0], endPos);
|
|
124
|
+
trackedLines.push(`${indent}@tracked ${memberSource}\n`);
|
|
125
|
+
|
|
126
|
+
// Remove from original position (full line)
|
|
127
|
+
const removeStart = lineStartOf(memberNode, text);
|
|
128
|
+
let removeEnd = endPos;
|
|
129
|
+
if (removeEnd < text.length && text[removeEnd] === "\n") {
|
|
130
|
+
removeEnd++;
|
|
131
|
+
}
|
|
132
|
+
fixes.push(fixer.replaceTextRange([removeStart, removeEnd], ""));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Compute insertion positions
|
|
138
|
+
const trackedInsertPos =
|
|
139
|
+
trackedLines.length > 0
|
|
140
|
+
? findTrackedInsertionPoint(
|
|
141
|
+
classBody,
|
|
142
|
+
macroNodeSet,
|
|
143
|
+
existingNodesToDecorate,
|
|
144
|
+
text
|
|
145
|
+
)
|
|
146
|
+
: null;
|
|
147
|
+
|
|
148
|
+
const privateInsertPos =
|
|
149
|
+
privateTrackedLines.length > 0
|
|
150
|
+
? findPrivatePropertyInsertionPoint(
|
|
151
|
+
classBody,
|
|
152
|
+
macroNodeSet,
|
|
153
|
+
existingNodesToDecorate,
|
|
154
|
+
text
|
|
155
|
+
)
|
|
156
|
+
: null;
|
|
157
|
+
|
|
158
|
+
// ESLint rejects overlapping fixes at the same position, so when both
|
|
159
|
+
// insertions target the same point (e.g. class with only macro members),
|
|
160
|
+
// combine them into a single insertion: tracked first, then private.
|
|
161
|
+
if (
|
|
162
|
+
trackedInsertPos !== null &&
|
|
163
|
+
privateInsertPos !== null &&
|
|
164
|
+
trackedInsertPos === privateInsertPos
|
|
165
|
+
) {
|
|
166
|
+
fixes.push(
|
|
167
|
+
fixer.insertTextBeforeRange(
|
|
168
|
+
[trackedInsertPos, trackedInsertPos],
|
|
169
|
+
trackedLines.join("") + "\n" + privateTrackedLines.join("")
|
|
170
|
+
)
|
|
171
|
+
);
|
|
172
|
+
} else {
|
|
173
|
+
if (trackedInsertPos !== null) {
|
|
174
|
+
fixes.push(
|
|
175
|
+
fixer.insertTextBeforeRange(
|
|
176
|
+
[trackedInsertPos, trackedInsertPos],
|
|
177
|
+
trackedLines.join("")
|
|
178
|
+
)
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Insert override fields at [private-properties] section (after all
|
|
183
|
+
// other PropertyDefinitions, before methods/getters).
|
|
184
|
+
if (privateInsertPos !== null) {
|
|
185
|
+
// Blank line separator before the private section when there are
|
|
186
|
+
// other property-like members above
|
|
187
|
+
const hasPropertiesAbove = classBody.body.some(
|
|
188
|
+
(m) =>
|
|
189
|
+
!macroNodeSet.has(m) &&
|
|
190
|
+
!existingNodesToDecorate.has(m) &&
|
|
191
|
+
m.type === "PropertyDefinition" &&
|
|
192
|
+
m.range[1] <= privateInsertPos
|
|
193
|
+
);
|
|
194
|
+
const privatePrefix = hasPropertiesAbove ? "\n" : "";
|
|
195
|
+
fixes.push(
|
|
196
|
+
fixer.insertTextBeforeRange(
|
|
197
|
+
[privateInsertPos, privateInsertPos],
|
|
198
|
+
privatePrefix + privateTrackedLines.join("")
|
|
199
|
+
)
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---- 3. Insert all getters at the correct position ----
|
|
205
|
+
const getterTexts = classUsages.map((u) =>
|
|
206
|
+
buildGetterCode(u, indent, sourceCode)
|
|
207
|
+
);
|
|
208
|
+
const allGettersText = getterTexts.join("\n");
|
|
209
|
+
|
|
210
|
+
const insertPos = findGetterInsertionPoint(
|
|
211
|
+
classBody,
|
|
212
|
+
macroNodeSet,
|
|
213
|
+
sourceCode
|
|
214
|
+
);
|
|
215
|
+
const hasContent = hasContentBeforeInsertion(
|
|
216
|
+
classUsages,
|
|
217
|
+
classBody,
|
|
218
|
+
macroNodeSet,
|
|
219
|
+
insertPos
|
|
220
|
+
);
|
|
221
|
+
// Blank line before getters when there's content above (field → method)
|
|
222
|
+
const prefix = hasContent ? "\n" : "";
|
|
223
|
+
|
|
224
|
+
fixes.push(
|
|
225
|
+
fixer.insertTextBeforeRange(
|
|
226
|
+
[insertPos, insertPos],
|
|
227
|
+
prefix + allGettersText + "\n"
|
|
228
|
+
)
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
return fixes;
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Getter code generation
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Build the full getter code string including decorator.
|
|
241
|
+
*
|
|
242
|
+
* @param {import('./computed-macros-analysis.mjs').MacroUsage} usage
|
|
243
|
+
* @param {string} indent
|
|
244
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
245
|
+
* @returns {string}
|
|
246
|
+
*/
|
|
247
|
+
function buildGetterCode(usage, indent, sourceCode) {
|
|
248
|
+
const {
|
|
249
|
+
transform,
|
|
250
|
+
propName,
|
|
251
|
+
allLocal,
|
|
252
|
+
dependentKeys,
|
|
253
|
+
literalArgs,
|
|
254
|
+
argNodes,
|
|
255
|
+
} = usage;
|
|
256
|
+
const transformArgs = { literalArgs, argNodes, propName, sourceCode };
|
|
257
|
+
const bodyRaw = transform.toGetterBody(transformArgs);
|
|
258
|
+
|
|
259
|
+
// Build decorator line
|
|
260
|
+
let decoratorLine;
|
|
261
|
+
if (allLocal) {
|
|
262
|
+
decoratorLine = `${indent}@dependentKeyCompat`;
|
|
263
|
+
} else {
|
|
264
|
+
const keys = dependentKeys.map((k) => JSON.stringify(k)).join(", ");
|
|
265
|
+
decoratorLine = `${indent}@computed(${keys})`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Build getter body — handle multi-line bodies (e.g. sort)
|
|
269
|
+
const bodyLines = bodyRaw.split("\n");
|
|
270
|
+
const bodyIndent = `${indent} `;
|
|
271
|
+
const formattedBody = bodyLines
|
|
272
|
+
.map((line) => `${bodyIndent}${line}`)
|
|
273
|
+
.join("\n");
|
|
274
|
+
|
|
275
|
+
const parts = [
|
|
276
|
+
decoratorLine,
|
|
277
|
+
`${indent}get ${propName}() {`,
|
|
278
|
+
formattedBody,
|
|
279
|
+
`${indent}}`,
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
// Append setter for bidirectional macros (e.g. alias)
|
|
283
|
+
if (transform.toSetterBody) {
|
|
284
|
+
const setterBody = transform.toSetterBody({
|
|
285
|
+
...transformArgs,
|
|
286
|
+
useEmberSet: !allLocal,
|
|
287
|
+
});
|
|
288
|
+
const setterLines = setterBody.split("\n");
|
|
289
|
+
const formattedSetterBody = setterLines
|
|
290
|
+
.map((line) => `${bodyIndent}${line}`)
|
|
291
|
+
.join("\n");
|
|
292
|
+
parts.push(
|
|
293
|
+
`${indent}set ${propName}(value) {`,
|
|
294
|
+
formattedSetterBody,
|
|
295
|
+
`${indent}}`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return parts.join("\n");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
// Insertion point logic
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Find the character position where generated getters should be inserted.
|
|
308
|
+
*
|
|
309
|
+
* Strategy (matches sort-class-members order):
|
|
310
|
+
* 1. If there's a non-static, non-macro instance MethodDefinition that is NOT
|
|
311
|
+
* a lifecycle method, insert before it (start of [everything-else] section).
|
|
312
|
+
* Static methods come before all instance members in the sort order, so
|
|
313
|
+
* they must be skipped.
|
|
314
|
+
* 2. Otherwise, if there's a last non-macro member, insert after it.
|
|
315
|
+
* 3. Fallback: insert before the closing `}` of the class body.
|
|
316
|
+
*
|
|
317
|
+
* @param {import('estree').ClassBody} classBody
|
|
318
|
+
* @param {Set<import('estree').Node>} macroNodeSet
|
|
319
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
320
|
+
* @returns {number} Character position at the start of a line
|
|
321
|
+
*/
|
|
322
|
+
function findGetterInsertionPoint(classBody, macroNodeSet, sourceCode) {
|
|
323
|
+
const text = sourceCode.getText();
|
|
324
|
+
const members = classBody.body;
|
|
325
|
+
|
|
326
|
+
// Look for the first non-static, non-macro instance MethodDefinition
|
|
327
|
+
// that isn't a lifecycle method. Static methods come before all instance
|
|
328
|
+
// members in the sort order, so inserting among them would be wrong.
|
|
329
|
+
for (const member of members) {
|
|
330
|
+
if (macroNodeSet.has(member) || member.static) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (member.type === "MethodDefinition") {
|
|
334
|
+
const name = member.key?.name;
|
|
335
|
+
if (!LIFECYCLE_METHODS.has(name)) {
|
|
336
|
+
// Insert before this method's line
|
|
337
|
+
return lineStartOf(member, text);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// No existing instance method found. Insert after the last non-macro member
|
|
343
|
+
// (including static members — they precede [everything-else] in sort order).
|
|
344
|
+
let lastNonMacro = null;
|
|
345
|
+
for (const member of members) {
|
|
346
|
+
if (!macroNodeSet.has(member)) {
|
|
347
|
+
lastNonMacro = member;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (lastNonMacro) {
|
|
352
|
+
// Position after this member's line (past semicolons and newline)
|
|
353
|
+
let pos = lastNonMacro.range[1];
|
|
354
|
+
while (pos < text.length && text[pos] === ";") {
|
|
355
|
+
pos++;
|
|
356
|
+
}
|
|
357
|
+
if (pos < text.length && text[pos] === "\n") {
|
|
358
|
+
pos++;
|
|
359
|
+
}
|
|
360
|
+
return pos;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Fallback: before the closing `}` of the class body
|
|
364
|
+
return lineStartOf({ range: [classBody.range[1] - 1] }, text);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Find the position to insert @tracked declarations.
|
|
369
|
+
*
|
|
370
|
+
* Strategy: insert right after the last existing @tracked property line
|
|
371
|
+
* (skipping macro nodes and members being moved). Falls back to the start
|
|
372
|
+
* of the class body (after `{\n`) if no @tracked properties exist.
|
|
373
|
+
*
|
|
374
|
+
* @param {import('estree').ClassBody} classBody
|
|
375
|
+
* @param {Set<import('estree').Node>} macroNodeSet
|
|
376
|
+
* @param {Set<import('estree').Node>} existingNodesToDecorate
|
|
377
|
+
* @param {string} text
|
|
378
|
+
* @returns {number}
|
|
379
|
+
*/
|
|
380
|
+
function findTrackedInsertionPoint(
|
|
381
|
+
classBody,
|
|
382
|
+
macroNodeSet,
|
|
383
|
+
existingNodesToDecorate,
|
|
384
|
+
text
|
|
385
|
+
) {
|
|
386
|
+
let lastTrackedEnd = null;
|
|
387
|
+
|
|
388
|
+
for (const member of classBody.body) {
|
|
389
|
+
if (macroNodeSet.has(member) || existingNodesToDecorate.has(member)) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (member.type !== "PropertyDefinition") {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (hasTrackedLikeDecorator(member)) {
|
|
397
|
+
let pos = member.range[1];
|
|
398
|
+
while (pos < text.length && text[pos] === ";") {
|
|
399
|
+
pos++;
|
|
400
|
+
}
|
|
401
|
+
if (pos < text.length && text[pos] === "\n") {
|
|
402
|
+
pos++;
|
|
403
|
+
}
|
|
404
|
+
lastTrackedEnd = pos;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (lastTrackedEnd !== null) {
|
|
409
|
+
return lastTrackedEnd;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// No tracked properties found — insert at start of class body
|
|
413
|
+
let pos = classBody.range[0] + 1;
|
|
414
|
+
if (pos < text.length && text[pos] === "\n") {
|
|
415
|
+
pos++;
|
|
416
|
+
}
|
|
417
|
+
return pos;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Find the position to insert override tracked fields (e.g. oneWay's
|
|
422
|
+
* `_propOverride`). These use a `_` prefix, so sort-class-members classifies
|
|
423
|
+
* them as [private-properties], which comes after [properties] and before
|
|
424
|
+
* lifecycle methods / [everything-else].
|
|
425
|
+
*
|
|
426
|
+
* Strategy: insert after the last non-macro, non-moved PropertyDefinition.
|
|
427
|
+
* This places private tracked fields after all other property-like members.
|
|
428
|
+
*
|
|
429
|
+
* @param {import('estree').ClassBody} classBody
|
|
430
|
+
* @param {Set<import('estree').Node>} macroNodeSet
|
|
431
|
+
* @param {Set<import('estree').Node>} existingNodesToDecorate
|
|
432
|
+
* @param {string} text
|
|
433
|
+
* @returns {number}
|
|
434
|
+
*/
|
|
435
|
+
function findPrivatePropertyInsertionPoint(
|
|
436
|
+
classBody,
|
|
437
|
+
macroNodeSet,
|
|
438
|
+
existingNodesToDecorate,
|
|
439
|
+
text
|
|
440
|
+
) {
|
|
441
|
+
let lastPropertyEnd = null;
|
|
442
|
+
|
|
443
|
+
for (const member of classBody.body) {
|
|
444
|
+
if (macroNodeSet.has(member) || existingNodesToDecorate.has(member)) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (member.type !== "PropertyDefinition") {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
let pos = member.range[1];
|
|
452
|
+
while (pos < text.length && text[pos] === ";") {
|
|
453
|
+
pos++;
|
|
454
|
+
}
|
|
455
|
+
if (pos < text.length && text[pos] === "\n") {
|
|
456
|
+
pos++;
|
|
457
|
+
}
|
|
458
|
+
lastPropertyEnd = pos;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (lastPropertyEnd !== null) {
|
|
462
|
+
return lastPropertyEnd;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// No properties found — insert at start of class body
|
|
466
|
+
let pos = classBody.range[0] + 1;
|
|
467
|
+
if (pos < text.length && text[pos] === "\n") {
|
|
468
|
+
pos++;
|
|
469
|
+
}
|
|
470
|
+
return pos;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Check whether a class member has a tracked-like decorator.
|
|
475
|
+
*
|
|
476
|
+
* @param {import('estree').Node} member
|
|
477
|
+
* @returns {boolean}
|
|
478
|
+
*/
|
|
479
|
+
function hasTrackedLikeDecorator(member) {
|
|
480
|
+
return (
|
|
481
|
+
member.decorators?.some((d) => {
|
|
482
|
+
const expr = d.expression;
|
|
483
|
+
return (
|
|
484
|
+
(expr.type === "Identifier" && TRACKED_DECORATORS.has(expr.name)) ||
|
|
485
|
+
(expr.type === "CallExpression" &&
|
|
486
|
+
expr.callee?.type === "Identifier" &&
|
|
487
|
+
TRACKED_DECORATORS.has(expr.callee.name))
|
|
488
|
+
);
|
|
489
|
+
}) ?? false
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Get the start-of-line position for a node (walks back past whitespace).
|
|
495
|
+
*
|
|
496
|
+
* @param {{ range?: number[], decorators?: Array<{ range: number[] }> }} node
|
|
497
|
+
* @param {string} text
|
|
498
|
+
* @returns {number}
|
|
499
|
+
*/
|
|
500
|
+
function lineStartOf(node, text) {
|
|
501
|
+
let pos =
|
|
502
|
+
node.decorators?.length > 0 ? node.decorators[0].range[0] : node.range[0];
|
|
503
|
+
|
|
504
|
+
while (pos > 0 && text[pos - 1] !== "\n") {
|
|
505
|
+
pos--;
|
|
506
|
+
}
|
|
507
|
+
return pos;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Check whether there will be visible content before the getter insertion
|
|
512
|
+
* point after fixes are applied. Used to decide whether to add a blank-line
|
|
513
|
+
* prefix (`\n`) before the getters.
|
|
514
|
+
*
|
|
515
|
+
* @param {import('./computed-macros-analysis.mjs').MacroUsage[]} classUsages
|
|
516
|
+
* @param {import('estree').ClassBody} classBody
|
|
517
|
+
* @param {Set<import('estree').Node>} macroNodeSet
|
|
518
|
+
* @param {number} insertPos Character position where getters will be inserted
|
|
519
|
+
* @returns {boolean}
|
|
520
|
+
*/
|
|
521
|
+
function hasContentBeforeInsertion(
|
|
522
|
+
classUsages,
|
|
523
|
+
classBody,
|
|
524
|
+
macroNodeSet,
|
|
525
|
+
insertPos
|
|
526
|
+
) {
|
|
527
|
+
// True if any macro produces @tracked declarations (placed at the
|
|
528
|
+
// tracked section, which is always before the getter insertion point)
|
|
529
|
+
const hasTrackedContent = classUsages.some(
|
|
530
|
+
(u) =>
|
|
531
|
+
(u.allLocal && u.trackedDeps?.length > 0) ||
|
|
532
|
+
u.transform.overrideTrackedFields
|
|
533
|
+
);
|
|
534
|
+
if (hasTrackedContent) {
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
// True if any existing members are being moved (they get re-inserted
|
|
538
|
+
// at the tracked section, before the getter insertion point)
|
|
539
|
+
const hasExistingToDecorate = classUsages.some(
|
|
540
|
+
(u) => u.existingNodesToDecorate?.length > 0
|
|
541
|
+
);
|
|
542
|
+
if (hasExistingToDecorate) {
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
// True if there are non-macro members before the insertion point
|
|
546
|
+
return classBody.body.some(
|
|
547
|
+
(m) => !macroNodeSet.has(m) && m.range[0] < insertPos
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ---------------------------------------------------------------------------
|
|
552
|
+
// Node range helpers
|
|
553
|
+
// ---------------------------------------------------------------------------
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Get the start position of a usage, including its decorators AND any
|
|
557
|
+
* leading whitespace on the same line. This ensures the replacement range
|
|
558
|
+
* covers the full indentation so the generated code can control its own
|
|
559
|
+
* indentation without doubling.
|
|
560
|
+
*/
|
|
561
|
+
function getNodeStart(usage, sourceCode) {
|
|
562
|
+
const { propertyNode } = usage;
|
|
563
|
+
const text = sourceCode.getText();
|
|
564
|
+
let pos =
|
|
565
|
+
propertyNode.decorators?.length > 0
|
|
566
|
+
? propertyNode.decorators[0].range[0]
|
|
567
|
+
: propertyNode.range[0];
|
|
568
|
+
|
|
569
|
+
// Walk back to the start of the line (past whitespace)
|
|
570
|
+
while (pos > 0 && text[pos - 1] !== "\n") {
|
|
571
|
+
pos--;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return pos;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Get the end position of a PropertyDefinition, consuming trailing semicolons.
|
|
579
|
+
*/
|
|
580
|
+
function getNodeEnd(node, sourceCode) {
|
|
581
|
+
let end = node.range[1];
|
|
582
|
+
const text = sourceCode.getText();
|
|
583
|
+
while (end < text.length && text[end] === ";") {
|
|
584
|
+
end++;
|
|
585
|
+
}
|
|
586
|
+
return end;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// ---------------------------------------------------------------------------
|
|
590
|
+
// Indentation helpers
|
|
591
|
+
// ---------------------------------------------------------------------------
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Detect the indentation of a node by looking at leading whitespace.
|
|
595
|
+
*/
|
|
596
|
+
function detectIndent(node, sourceCode) {
|
|
597
|
+
const text = sourceCode.getText();
|
|
598
|
+
let pos =
|
|
599
|
+
node.decorators?.length > 0 ? node.decorators[0].range[0] : node.range[0];
|
|
600
|
+
|
|
601
|
+
while (pos > 0 && text[pos - 1] !== "\n") {
|
|
602
|
+
pos--;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
let indent = "";
|
|
606
|
+
while (pos < text.length && (text[pos] === " " || text[pos] === "\t")) {
|
|
607
|
+
indent += text[pos];
|
|
608
|
+
pos++;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return indent;
|
|
612
|
+
}
|