@discourse/lint-configs 2.44.1 → 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.
@@ -0,0 +1,645 @@
1
+ /**
2
+ * @fileoverview Transform registry for computed property macros.
3
+ *
4
+ * Maps each macro name to its source, whether it can be auto-fixed,
5
+ * what additional imports the transformation requires, how to generate
6
+ * the getter body, and how to derive the dependent keys for @computed.
7
+ */
8
+
9
+ import { propertyPathToOptionalChaining } from "../utils/property-path.mjs";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Helpers
13
+ // ---------------------------------------------------------------------------
14
+
15
+ /**
16
+ * Convert a property-path string to a `this.`-prefixed accessor.
17
+ * Delegates to the shared utility from `utils/property-path.mjs`.
18
+ *
19
+ * @param {string} path
20
+ * @returns {string}
21
+ */
22
+ function toAccess(path) {
23
+ return propertyPathToOptionalChaining(path, true, false);
24
+ }
25
+
26
+ /**
27
+ * Render a JS literal value suitable for source output.
28
+ * Strings → quoted, numbers/booleans → as-is, null → "null".
29
+ *
30
+ * @param {*} value
31
+ * @returns {string}
32
+ */
33
+ function renderLiteral(value) {
34
+ if (typeof value === "string") {
35
+ return JSON.stringify(value);
36
+ }
37
+ return String(value);
38
+ }
39
+
40
+ /**
41
+ * Parse an Ember-style format string (using `%@` / `%@N` placeholders)
42
+ * and return a JS template-literal expression.
43
+ *
44
+ * @param {string} format - the format string, e.g. "/admin/users/%@1/%@2"
45
+ * @param {string[]} propPaths - the property-path arguments preceding the format string
46
+ * @returns {string} a template literal expression (with backticks)
47
+ */
48
+ function fmtToTemplateLiteral(format, propPaths) {
49
+ let seqIdx = 0;
50
+ const result = format.replace(/%@(\d+)?/g, (_match, indexStr) => {
51
+ const idx = indexStr ? parseInt(indexStr, 10) - 1 : seqIdx++;
52
+ const path = propPaths[idx];
53
+ if (!path) {
54
+ return "";
55
+ }
56
+ return `\${${toAccess(path)}}`;
57
+ });
58
+ return `\`${result}\``;
59
+ }
60
+
61
+ /**
62
+ * Check whether a dependent-key string is "local" (no dots, no special
63
+ * tokens like `@each` or `[]`).
64
+ *
65
+ * @param {string} key
66
+ * @returns {boolean}
67
+ */
68
+ export function isLocalKey(key) {
69
+ return !key.includes(".");
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Individual transforms
74
+ // ---------------------------------------------------------------------------
75
+
76
+ /** @typedef {{ name: string, source: string, isDefault?: boolean }} RequiredImport */
77
+
78
+ /**
79
+ * @typedef {Object} MacroTransform
80
+ * @property {string} source - import source where the macro lives
81
+ * @property {boolean} canAutoFix
82
+ * @property {string} [reason] - explanation when canAutoFix is false
83
+ * @property {number} [depKeyArgCount] - how many leading args must be string
84
+ * literals (dep key paths); remaining args are "value args" that can be
85
+ * non-literal (their source text is used verbatim in the getter body)
86
+ * @property {RequiredImport[]} [requiredImports]
87
+ * @property {RequiredImport[]} [setterRequiredImports] - extra imports needed
88
+ * only when the setter uses Ember's `set()` (the `!allLocal` / `@computed` path)
89
+ * @property {(args: TransformArgs) => string} [toGetterBody]
90
+ * @property {(args: SetterTransformArgs) => string} [toSetterBody]
91
+ * @property {(args: TransformArgs) => string[]} [toDependentKeys]
92
+ */
93
+
94
+ /**
95
+ * @typedef {Object} TransformArgs
96
+ * @property {string[]} literalArgs - the literal string/number values of the decorator arguments
97
+ * @property {import('estree').Node[]} argNodes - raw AST argument nodes
98
+ * @property {string} propName - the decorated property name
99
+ * @property {import('eslint').SourceCode} sourceCode
100
+ */
101
+
102
+ /**
103
+ * @typedef {TransformArgs & { useEmberSet: boolean }} SetterTransformArgs
104
+ * `useEmberSet` is true when the `@computed` path requires Ember's `set()`,
105
+ * false when the `@dependentKeyCompat` path uses native assignment.
106
+ */
107
+
108
+ const EMBER_SOURCE = "@ember/object/computed";
109
+ const DISCOURSE_SOURCE = "discourse/lib/computed";
110
+
111
+ // ---- @ember/object/computed ------------------------------------------------
112
+
113
+ const simpleAccess = {
114
+ source: EMBER_SOURCE,
115
+ canAutoFix: true,
116
+ toGetterBody({ literalArgs: [path] }) {
117
+ return `return ${toAccess(path)};`;
118
+ },
119
+ toDependentKeys({ literalArgs: [path] }) {
120
+ return [path];
121
+ },
122
+ };
123
+
124
+ const alias = {
125
+ ...simpleAccess,
126
+ setterRequiredImports: [{ name: "set", source: "@ember/object" }],
127
+ toSetterBody({ literalArgs: [path], useEmberSet }) {
128
+ if (useEmberSet) {
129
+ return `set(this, ${JSON.stringify(path)}, value);`;
130
+ }
131
+ return `this.${path} = value;`;
132
+ },
133
+ };
134
+ const readOnly = { ...simpleAccess };
135
+
136
+ // oneWay/reads: one-way binding that can be overridden locally.
137
+ // Getter reads from source until the setter is called, then the property
138
+ // permanently diverges. Uses a single `@tracked _propOverride` field (placed
139
+ // in the [private-properties] section via the `_` prefix). Checking against
140
+ // `undefined` distinguishes "never set" from "set to a value".
141
+ const oneWayTransform = {
142
+ source: EMBER_SOURCE,
143
+ canAutoFix: true,
144
+ overrideTrackedFields({ propName }) {
145
+ return [{ name: `_${propName}Override` }];
146
+ },
147
+ toGetterBody({ literalArgs: [path], propName }) {
148
+ return [
149
+ `if (this._${propName}Override !== undefined) {`,
150
+ ` return this._${propName}Override;`,
151
+ `}`,
152
+ `return ${toAccess(path)};`,
153
+ ].join("\n");
154
+ },
155
+ toSetterBody({ propName }) {
156
+ return `this._${propName}Override = value;`;
157
+ },
158
+ toDependentKeys({ literalArgs: [path] }) {
159
+ return [path];
160
+ },
161
+ };
162
+ const reads = { ...oneWayTransform };
163
+ const oneWay = { ...oneWayTransform };
164
+
165
+ const not = {
166
+ source: EMBER_SOURCE,
167
+ canAutoFix: true,
168
+ toGetterBody({ literalArgs: [path] }) {
169
+ return `return !${toAccess(path)};`;
170
+ },
171
+ toDependentKeys({ literalArgs: [path] }) {
172
+ return [path];
173
+ },
174
+ };
175
+
176
+ const bool = {
177
+ source: EMBER_SOURCE,
178
+ canAutoFix: true,
179
+ toGetterBody({ literalArgs: [path] }) {
180
+ return `return !!${toAccess(path)};`;
181
+ },
182
+ toDependentKeys({ literalArgs: [path] }) {
183
+ return [path];
184
+ },
185
+ };
186
+
187
+ const and = {
188
+ source: EMBER_SOURCE,
189
+ canAutoFix: true,
190
+ toGetterBody({ literalArgs }) {
191
+ return `return ${literalArgs.map(toAccess).join(" && ")};`;
192
+ },
193
+ toDependentKeys({ literalArgs }) {
194
+ return [...literalArgs];
195
+ },
196
+ };
197
+
198
+ const or = {
199
+ source: EMBER_SOURCE,
200
+ canAutoFix: true,
201
+ toGetterBody({ literalArgs }) {
202
+ return `return ${literalArgs.map(toAccess).join(" || ")};`;
203
+ },
204
+ toDependentKeys({ literalArgs }) {
205
+ return [...literalArgs];
206
+ },
207
+ };
208
+
209
+ function comparisonMacro(operator) {
210
+ return {
211
+ source: EMBER_SOURCE,
212
+ canAutoFix: true,
213
+ depKeyArgCount: 1,
214
+ toGetterBody({ literalArgs: [path], argNodes, sourceCode }) {
215
+ const valueText = sourceCode.getText(argNodes[1]);
216
+ return `return ${toAccess(path)} ${operator} ${valueText};`;
217
+ },
218
+ toDependentKeys({ literalArgs: [path] }) {
219
+ return [path];
220
+ },
221
+ };
222
+ }
223
+
224
+ const equal = comparisonMacro("===");
225
+ const gt = comparisonMacro(">");
226
+ const gte = comparisonMacro(">=");
227
+ const lt = comparisonMacro("<");
228
+ const lte = comparisonMacro("<=");
229
+
230
+ const notEmpty = {
231
+ source: EMBER_SOURCE,
232
+ canAutoFix: true,
233
+ requiredImports: [{ name: "isEmpty", source: "@ember/utils" }],
234
+ toGetterBody({ literalArgs: [path] }) {
235
+ return `return !isEmpty(${toAccess(path)});`;
236
+ },
237
+ toDependentKeys({ literalArgs: [path] }) {
238
+ return [`${path}.length`];
239
+ },
240
+ };
241
+
242
+ const empty = {
243
+ source: EMBER_SOURCE,
244
+ canAutoFix: true,
245
+ requiredImports: [{ name: "isEmpty", source: "@ember/utils" }],
246
+ toGetterBody({ literalArgs: [path] }) {
247
+ return `return isEmpty(${toAccess(path)});`;
248
+ },
249
+ toDependentKeys({ literalArgs: [path] }) {
250
+ return [`${path}.length`];
251
+ },
252
+ };
253
+
254
+ const none = {
255
+ source: EMBER_SOURCE,
256
+ canAutoFix: true,
257
+ toGetterBody({ literalArgs: [path] }) {
258
+ return `return ${toAccess(path)} == null;`;
259
+ },
260
+ toDependentKeys({ literalArgs: [path] }) {
261
+ return [path];
262
+ },
263
+ };
264
+
265
+ const match = {
266
+ source: EMBER_SOURCE,
267
+ canAutoFix: true,
268
+ toGetterBody({ literalArgs: [path], argNodes, sourceCode }) {
269
+ const regexText = sourceCode.getText(argNodes[1]);
270
+ return `return ${regexText}.test(${toAccess(path)});`;
271
+ },
272
+ toDependentKeys({ literalArgs: [path] }) {
273
+ return [path];
274
+ },
275
+ };
276
+
277
+ const mapBy = {
278
+ source: EMBER_SOURCE,
279
+ canAutoFix: true,
280
+ toGetterBody({ literalArgs: [arrPath, prop] }) {
281
+ return `return ${toAccess(arrPath)}?.map?.((item) => item.${prop}) ?? [];`;
282
+ },
283
+ toDependentKeys({ literalArgs: [arrPath, prop] }) {
284
+ return [`${arrPath}.@each.${prop}`];
285
+ },
286
+ };
287
+
288
+ const filterBy = {
289
+ source: EMBER_SOURCE,
290
+ canAutoFix: true,
291
+ toGetterBody({ literalArgs, argNodes }) {
292
+ const arrPath = literalArgs[0];
293
+ const prop = literalArgs[1];
294
+ if (argNodes.length === 3) {
295
+ const valueText =
296
+ argNodes[2].raw !== undefined
297
+ ? argNodes[2].raw
298
+ : renderLiteral(argNodes[2].value);
299
+ return `return ${toAccess(arrPath)}?.filter?.((item) => item.${prop} === ${valueText}) ?? [];`;
300
+ }
301
+ return `return ${toAccess(arrPath)}?.filter?.((item) => item.${prop}) ?? [];`;
302
+ },
303
+ toDependentKeys({ literalArgs: [arrPath, prop] }) {
304
+ return [`${arrPath}.@each.${prop}`];
305
+ },
306
+ };
307
+
308
+ const collect = {
309
+ source: EMBER_SOURCE,
310
+ canAutoFix: true,
311
+ toGetterBody({ literalArgs }) {
312
+ const items = literalArgs
313
+ .map((p) => {
314
+ const access = toAccess(p);
315
+ return `${access} === undefined ? null : ${access}`;
316
+ })
317
+ .join(", ");
318
+ return `return [${items}];`;
319
+ },
320
+ toDependentKeys({ literalArgs }) {
321
+ return [...literalArgs];
322
+ },
323
+ };
324
+
325
+ const sum = {
326
+ source: EMBER_SOURCE,
327
+ canAutoFix: true,
328
+ toGetterBody({ literalArgs: [path] }) {
329
+ return `return ${toAccess(path)}?.reduce?.((s, v) => s + v, 0) ?? 0;`;
330
+ },
331
+ toDependentKeys({ literalArgs: [path] }) {
332
+ return [path];
333
+ },
334
+ };
335
+
336
+ const max = {
337
+ source: EMBER_SOURCE,
338
+ canAutoFix: true,
339
+ toGetterBody({ literalArgs: [path] }) {
340
+ return `return ${toAccess(path)}?.reduce?.((m, v) => Math.max(m, v), -Infinity) ?? -Infinity;`;
341
+ },
342
+ toDependentKeys({ literalArgs: [path] }) {
343
+ return [path];
344
+ },
345
+ };
346
+
347
+ const min = {
348
+ source: EMBER_SOURCE,
349
+ canAutoFix: true,
350
+ toGetterBody({ literalArgs: [path] }) {
351
+ return `return ${toAccess(path)}?.reduce?.((m, v) => Math.min(m, v), Infinity) ?? Infinity;`;
352
+ },
353
+ toDependentKeys({ literalArgs: [path] }) {
354
+ return [path];
355
+ },
356
+ };
357
+
358
+ const uniq = {
359
+ source: EMBER_SOURCE,
360
+ canAutoFix: true,
361
+ requiredImports: [
362
+ { name: "uniqueItemsFromArray", source: "discourse/lib/array-tools" },
363
+ ],
364
+ toGetterBody({ literalArgs }) {
365
+ if (literalArgs.length === 1) {
366
+ return `return uniqueItemsFromArray(${toAccess(literalArgs[0])} ?? []);`;
367
+ }
368
+ const spreads = literalArgs
369
+ .map((p) => `...(${toAccess(p)} ?? [])`)
370
+ .join(", ");
371
+ return `return uniqueItemsFromArray([${spreads}]);`;
372
+ },
373
+ toDependentKeys({ literalArgs }) {
374
+ return [...literalArgs];
375
+ },
376
+ };
377
+
378
+ const uniqBy = {
379
+ source: EMBER_SOURCE,
380
+ canAutoFix: true,
381
+ requiredImports: [
382
+ { name: "uniqueItemsFromArray", source: "discourse/lib/array-tools" },
383
+ ],
384
+ toGetterBody({ literalArgs: [arrPath, key] }) {
385
+ return `return uniqueItemsFromArray(${toAccess(arrPath)} ?? [], ${renderLiteral(key)});`;
386
+ },
387
+ toDependentKeys({ literalArgs: [arrPath] }) {
388
+ return [arrPath];
389
+ },
390
+ };
391
+
392
+ // union === uniq (same function in Ember)
393
+ const union = { ...uniq };
394
+
395
+ const intersect = {
396
+ source: EMBER_SOURCE,
397
+ canAutoFix: true,
398
+ toGetterBody({ literalArgs }) {
399
+ // Ember filters from the LAST array against all others
400
+ const last = literalArgs[literalArgs.length - 1];
401
+ const rest = literalArgs.slice(0, -1);
402
+ const conditions = rest
403
+ .map((p) => `${toAccess(p)}?.includes?.(item)`)
404
+ .join(" && ");
405
+ return `return ${toAccess(last)}?.filter?.((item) => ${conditions}) ?? [];`;
406
+ },
407
+ toDependentKeys({ literalArgs }) {
408
+ return [...literalArgs];
409
+ },
410
+ };
411
+
412
+ const setDiff = {
413
+ source: EMBER_SOURCE,
414
+ canAutoFix: true,
415
+ toGetterBody({ literalArgs: [a, b] }) {
416
+ return `return ${toAccess(a)}?.filter?.((item) => !${toAccess(b)}?.includes?.(item)) ?? [];`;
417
+ },
418
+ toDependentKeys({ literalArgs: [a, b] }) {
419
+ return [a, b];
420
+ },
421
+ };
422
+
423
+ const sort = {
424
+ source: EMBER_SOURCE,
425
+ canAutoFix: true,
426
+ requiredImports: [
427
+ { name: "arraySortedByProperties", source: "discourse/lib/array-tools" },
428
+ ],
429
+ toGetterBody({ literalArgs: [arrPath, sortDefPath] }) {
430
+ return `return arraySortedByProperties(${toAccess(arrPath)}, ${toAccess(sortDefPath)});`;
431
+ },
432
+ toDependentKeys({ literalArgs: [arrPath, sortDefPath] }) {
433
+ return [arrPath, sortDefPath];
434
+ },
435
+ };
436
+
437
+ // filter/map with callback — not auto-fixable
438
+ const filter = {
439
+ source: EMBER_SOURCE,
440
+ canAutoFix: false,
441
+ reason: "callback-based macro requires manual conversion",
442
+ };
443
+
444
+ const map = {
445
+ source: EMBER_SOURCE,
446
+ canAutoFix: false,
447
+ reason: "callback-based macro requires manual conversion",
448
+ };
449
+
450
+ // ---- discourse/lib/computed ------------------------------------------------
451
+
452
+ const propertyEqual = {
453
+ source: DISCOURSE_SOURCE,
454
+ canAutoFix: true,
455
+ requiredImports: [{ name: "deepEqual", source: "discourse/lib/object" }],
456
+ toGetterBody({ literalArgs: [a, b] }) {
457
+ return `return deepEqual(${toAccess(a)}, ${toAccess(b)});`;
458
+ },
459
+ toDependentKeys({ literalArgs: [a, b] }) {
460
+ return [a, b];
461
+ },
462
+ };
463
+
464
+ const propertyNotEqual = {
465
+ source: DISCOURSE_SOURCE,
466
+ canAutoFix: true,
467
+ requiredImports: [{ name: "deepEqual", source: "discourse/lib/object" }],
468
+ toGetterBody({ literalArgs: [a, b] }) {
469
+ return `return !deepEqual(${toAccess(a)}, ${toAccess(b)});`;
470
+ },
471
+ toDependentKeys({ literalArgs: [a, b] }) {
472
+ return [a, b];
473
+ },
474
+ };
475
+
476
+ const propertyGreaterThan = {
477
+ source: DISCOURSE_SOURCE,
478
+ canAutoFix: true,
479
+ toGetterBody({ literalArgs: [a, b] }) {
480
+ return `return ${toAccess(a)} > ${toAccess(b)};`;
481
+ },
482
+ toDependentKeys({ literalArgs: [a, b] }) {
483
+ return [a, b];
484
+ },
485
+ };
486
+
487
+ const propertyLessThan = {
488
+ source: DISCOURSE_SOURCE,
489
+ canAutoFix: true,
490
+ toGetterBody({ literalArgs: [a, b] }) {
491
+ return `return ${toAccess(a)} < ${toAccess(b)};`;
492
+ },
493
+ toDependentKeys({ literalArgs: [a, b] }) {
494
+ return [a, b];
495
+ },
496
+ };
497
+
498
+ const setting = {
499
+ source: DISCOURSE_SOURCE,
500
+ canAutoFix: true,
501
+ toGetterBody({ literalArgs: [name] }) {
502
+ return `return this.siteSettings.${name};`;
503
+ },
504
+ toDependentKeys({ literalArgs: [name] }) {
505
+ return [`siteSettings.${name}`];
506
+ },
507
+ };
508
+
509
+ const fmt = {
510
+ source: DISCOURSE_SOURCE,
511
+ canAutoFix: true,
512
+ toGetterBody({ literalArgs }) {
513
+ const format = literalArgs[literalArgs.length - 1];
514
+ const propPaths = literalArgs.slice(0, -1);
515
+ return `return ${fmtToTemplateLiteral(format, propPaths)};`;
516
+ },
517
+ toDependentKeys({ literalArgs }) {
518
+ return literalArgs.slice(0, -1);
519
+ },
520
+ };
521
+
522
+ const url = {
523
+ source: DISCOURSE_SOURCE,
524
+ canAutoFix: true,
525
+ requiredImports: [
526
+ { name: "getURL", source: "discourse/lib/get-url", isDefault: true },
527
+ ],
528
+ toGetterBody({ literalArgs }) {
529
+ const format = literalArgs[literalArgs.length - 1];
530
+ const propPaths = literalArgs.slice(0, -1);
531
+ return `return getURL(${fmtToTemplateLiteral(format, propPaths)});`;
532
+ },
533
+ toDependentKeys({ literalArgs }) {
534
+ return literalArgs.slice(0, -1);
535
+ },
536
+ };
537
+
538
+ const i18n = {
539
+ source: DISCOURSE_SOURCE,
540
+ canAutoFix: true,
541
+ requiredImports: [{ name: "i18n", source: "discourse-i18n" }],
542
+ toGetterBody({ literalArgs }) {
543
+ const format = literalArgs[literalArgs.length - 1];
544
+ const propPaths = literalArgs.slice(0, -1);
545
+ return `return i18n(${fmtToTemplateLiteral(format, propPaths)});`;
546
+ },
547
+ toDependentKeys({ literalArgs }) {
548
+ return literalArgs.slice(0, -1);
549
+ },
550
+ };
551
+
552
+ const htmlSafe = {
553
+ source: DISCOURSE_SOURCE,
554
+ canAutoFix: true,
555
+ requiredImports: [{ name: "htmlSafe", source: "@ember/template" }],
556
+ toGetterBody({ literalArgs: [path] }) {
557
+ return `return htmlSafe(${toAccess(path)});`;
558
+ },
559
+ toDependentKeys({ literalArgs: [path] }) {
560
+ return [path];
561
+ },
562
+ };
563
+
564
+ const endWith = {
565
+ source: DISCOURSE_SOURCE,
566
+ canAutoFix: true,
567
+ toGetterBody({ literalArgs }) {
568
+ const suffix = literalArgs[literalArgs.length - 1];
569
+ const propPaths = literalArgs.slice(0, -1);
570
+ if (propPaths.length === 1) {
571
+ return `return ${toAccess(propPaths[0])}?.endsWith(${renderLiteral(suffix)});`;
572
+ }
573
+ const checks = propPaths
574
+ .map((p) => `${toAccess(p)}?.endsWith(${renderLiteral(suffix)})`)
575
+ .join(" && ");
576
+ return `return ${checks};`;
577
+ },
578
+ toDependentKeys({ literalArgs }) {
579
+ return literalArgs.slice(0, -1);
580
+ },
581
+ };
582
+
583
+ // ---------------------------------------------------------------------------
584
+ // Registry
585
+ // ---------------------------------------------------------------------------
586
+
587
+ /** @type {Map<string, MacroTransform>} */
588
+ export const MACRO_TRANSFORMS = new Map([
589
+ // @ember/object/computed
590
+ ["alias", alias],
591
+ ["readOnly", readOnly],
592
+ ["reads", reads],
593
+ ["oneWay", oneWay],
594
+ ["not", not],
595
+ ["bool", bool],
596
+ ["and", and],
597
+ ["or", or],
598
+ ["equal", equal],
599
+ ["gt", gt],
600
+ ["gte", gte],
601
+ ["lt", lt],
602
+ ["lte", lte],
603
+ ["notEmpty", notEmpty],
604
+ ["empty", empty],
605
+ ["none", none],
606
+ ["match", match],
607
+ ["mapBy", mapBy],
608
+ ["filterBy", filterBy],
609
+ ["collect", collect],
610
+ ["sum", sum],
611
+ ["max", max],
612
+ ["min", min],
613
+ ["uniq", uniq],
614
+ ["uniqBy", uniqBy],
615
+ ["union", union],
616
+ ["intersect", intersect],
617
+ ["setDiff", setDiff],
618
+ ["sort", sort],
619
+ ["filter", filter],
620
+ ["map", map],
621
+ // discourse/lib/computed
622
+ ["propertyEqual", propertyEqual],
623
+ ["propertyNotEqual", propertyNotEqual],
624
+ ["propertyGreaterThan", propertyGreaterThan],
625
+ ["propertyLessThan", propertyLessThan],
626
+ ["setting", setting],
627
+ ["fmt", fmt],
628
+ ["url", url],
629
+ ["i18n", i18n],
630
+ ["computedI18n", i18n], // alias — exported as both names
631
+ ["htmlSafe", htmlSafe],
632
+ ["endWith", endWith],
633
+ ]);
634
+
635
+ /** The import sources this rule targets. */
636
+ export const MACRO_SOURCES = new Set([
637
+ "@ember/object/computed",
638
+ "discourse/lib/computed",
639
+ "discourse/lib/decorators",
640
+ ]);
641
+
642
+ /** Maps alternative import sources to the canonical source they alias. */
643
+ export const SOURCE_ALIASES = new Map([
644
+ ["discourse/lib/decorators", "@ember/object/computed"],
645
+ ]);