@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.
- 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 -1
- package/package.json +1 -1
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview ESLint rule to replace computed property macro decorators
|
|
3
|
+
* from `@ember/object/computed` and `discourse/lib/computed` with native
|
|
4
|
+
* getters using `@computed` or `@dependentKeyCompat` + `@tracked`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { analyzeMacroUsage } from "./no-computed-macros/computed-macros-analysis.mjs";
|
|
8
|
+
import { createClassFix } from "./no-computed-macros/computed-macros-fixer.mjs";
|
|
9
|
+
import { MACRO_SOURCES } from "./no-computed-macros/macro-transforms.mjs";
|
|
10
|
+
import {
|
|
11
|
+
collectImports,
|
|
12
|
+
getImportedLocalNames,
|
|
13
|
+
} from "./utils/analyze-imports.mjs";
|
|
14
|
+
import { buildImportStatement, fixImport } from "./utils/fix-import.mjs";
|
|
15
|
+
|
|
16
|
+
const USE_NATIVE_GETTER_INSTEAD = "Use a native getter instead of `@{{name}}`";
|
|
17
|
+
|
|
18
|
+
export default {
|
|
19
|
+
meta: {
|
|
20
|
+
type: "suggestion",
|
|
21
|
+
docs: {
|
|
22
|
+
description: "Replace computed property macros with native getters",
|
|
23
|
+
},
|
|
24
|
+
fixable: "code",
|
|
25
|
+
schema: [],
|
|
26
|
+
messages: {
|
|
27
|
+
replaceMacro: `${USE_NATIVE_GETTER_INSTEAD}.`,
|
|
28
|
+
addTracked:
|
|
29
|
+
"Add `@tracked` to `{{name}}` (dependency of a converted macro).",
|
|
30
|
+
cannotAutoFixComplex: `${USE_NATIVE_GETTER_INSTEAD}: {{reason}}.`,
|
|
31
|
+
cannotAutoFixDynamic: `${USE_NATIVE_GETTER_INSTEAD}: it has non-literal arguments (convert manually).`,
|
|
32
|
+
cannotAutoFixSelfReference: `${USE_NATIVE_GETTER_INSTEAD}: \`{{propName}}\` references itself (convert manually).`,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
create(context) {
|
|
37
|
+
const sourceCode = context.getSourceCode();
|
|
38
|
+
let analysis = null;
|
|
39
|
+
let importsMap = null;
|
|
40
|
+
let importFixGenerated = false;
|
|
41
|
+
|
|
42
|
+
function ensureAnalysis() {
|
|
43
|
+
if (analysis) {
|
|
44
|
+
return analysis;
|
|
45
|
+
}
|
|
46
|
+
importsMap = collectImports(sourceCode);
|
|
47
|
+
analysis = analyzeMacroUsage(sourceCode, importsMap);
|
|
48
|
+
return analysis;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Precompute per-class fixable usages and existing nodes to decorate.
|
|
52
|
+
// Lazily initialised on first access (like analysis).
|
|
53
|
+
let classFixes = null;
|
|
54
|
+
function ensureClassFixes() {
|
|
55
|
+
if (classFixes) {
|
|
56
|
+
return classFixes;
|
|
57
|
+
}
|
|
58
|
+
const { usages } = ensureAnalysis();
|
|
59
|
+
classFixes = new Map();
|
|
60
|
+
|
|
61
|
+
for (const usage of usages) {
|
|
62
|
+
if (!usage.canAutoFix) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const classBody = usage.propertyNode.parent;
|
|
66
|
+
if (!classFixes.has(classBody)) {
|
|
67
|
+
classFixes.set(classBody, {
|
|
68
|
+
fixableUsages: [],
|
|
69
|
+
existingNodesToDecorate: new Set(),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const entry = classFixes.get(classBody);
|
|
73
|
+
entry.fixableUsages.push(usage);
|
|
74
|
+
if (usage.existingNodesToDecorate) {
|
|
75
|
+
for (const node of usage.existingNodesToDecorate) {
|
|
76
|
+
entry.existingNodesToDecorate.add(node);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return classFixes;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Track which class bodies have already had a combined fix attached
|
|
85
|
+
const classFixAttached = new Set();
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
// Report on macro import declarations — only the import-level fix here
|
|
89
|
+
ImportDeclaration(node) {
|
|
90
|
+
if (!MACRO_SOURCES.has(node.source.value)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { usages, importedMacros, macroImportNodes } = ensureAnalysis();
|
|
95
|
+
|
|
96
|
+
// Only report specifiers that have at least one usage in a native class
|
|
97
|
+
const usedLocalNames = new Set(usages.map((u) => u.localName));
|
|
98
|
+
const macroSpecifiers = node.specifiers.filter(
|
|
99
|
+
(spec) =>
|
|
100
|
+
spec.type === "ImportSpecifier" &&
|
|
101
|
+
importedMacros.has(spec.local.name) &&
|
|
102
|
+
usedLocalNames.has(spec.local.name)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (macroSpecifiers.length === 0) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const fixableUsages = usages.filter((u) => u.canAutoFix);
|
|
110
|
+
const hasAnyFix = fixableUsages.length > 0;
|
|
111
|
+
|
|
112
|
+
// Build the import fix ONCE across all macro import declarations.
|
|
113
|
+
// When a file imports from both @ember/object/computed and
|
|
114
|
+
// discourse/lib/computed, we must handle all import nodes in a
|
|
115
|
+
// single fix to avoid duplicate new import lines.
|
|
116
|
+
let importFix;
|
|
117
|
+
if (hasAnyFix && !importFixGenerated) {
|
|
118
|
+
importFixGenerated = true;
|
|
119
|
+
importFix = (fixer) =>
|
|
120
|
+
buildImportFixes(fixer, {
|
|
121
|
+
sourceCode,
|
|
122
|
+
importsMap,
|
|
123
|
+
usages: fixableUsages,
|
|
124
|
+
macroImportNodes,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let fixAttached = false;
|
|
129
|
+
for (const spec of macroSpecifiers) {
|
|
130
|
+
const macroName = importedMacros.get(spec.local.name);
|
|
131
|
+
context.report({
|
|
132
|
+
node: spec,
|
|
133
|
+
messageId: "replaceMacro",
|
|
134
|
+
data: { name: macroName },
|
|
135
|
+
fix: !fixAttached ? importFix : undefined,
|
|
136
|
+
});
|
|
137
|
+
fixAttached = true;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
// Report on each macro usage in class bodies.
|
|
142
|
+
// The combined class-body fix (remove macros + insert getters at the
|
|
143
|
+
// correct position) is attached to the FIRST fixable macro per class.
|
|
144
|
+
PropertyDefinition(node) {
|
|
145
|
+
if (!node.decorators) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { usages } = ensureAnalysis();
|
|
150
|
+
const usage = usages.find((u) => u.propertyNode === node);
|
|
151
|
+
|
|
152
|
+
if (!usage) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let fix;
|
|
157
|
+
if (usage.canAutoFix) {
|
|
158
|
+
const classBody = node.parent;
|
|
159
|
+
const fixesMap = ensureClassFixes();
|
|
160
|
+
const classEntry = fixesMap.get(classBody);
|
|
161
|
+
if (classEntry && !classFixAttached.has(classBody)) {
|
|
162
|
+
classFixAttached.add(classBody);
|
|
163
|
+
fix = createClassFix(
|
|
164
|
+
classEntry.fixableUsages,
|
|
165
|
+
classEntry.existingNodesToDecorate,
|
|
166
|
+
sourceCode
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
context.report({
|
|
172
|
+
node: usage.decoratorNode || node,
|
|
173
|
+
messageId: usage.messageId || "replaceMacro",
|
|
174
|
+
data: usage.reportData || { name: usage.macroName },
|
|
175
|
+
fix,
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
// Report @tracked additions for existing class members.
|
|
180
|
+
// The actual fix is included in the combined class-body fix
|
|
181
|
+
// (attached to the first fixable macro in each class) to avoid
|
|
182
|
+
// overlapping fix ranges.
|
|
183
|
+
"Program:exit"() {
|
|
184
|
+
const { usages } = ensureAnalysis();
|
|
185
|
+
const decorated = new Set();
|
|
186
|
+
|
|
187
|
+
for (const usage of usages) {
|
|
188
|
+
if (!usage.canAutoFix || !usage.existingNodesToDecorate) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
for (const memberNode of usage.existingNodesToDecorate) {
|
|
192
|
+
if (decorated.has(memberNode)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
decorated.add(memberNode);
|
|
196
|
+
|
|
197
|
+
const name =
|
|
198
|
+
memberNode.key.type === "Identifier"
|
|
199
|
+
? memberNode.key.name
|
|
200
|
+
: String(memberNode.key.value);
|
|
201
|
+
context.report({
|
|
202
|
+
node: memberNode,
|
|
203
|
+
messageId: "addTracked",
|
|
204
|
+
data: { name },
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Import fix builder
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Build import-related fixes for ALL fixable macro usages.
|
|
219
|
+
*
|
|
220
|
+
* This function handles ALL macro import nodes at once to avoid producing
|
|
221
|
+
* duplicate new import lines when a file imports macros from both
|
|
222
|
+
* `@ember/object/computed` and `discourse/lib/computed`.
|
|
223
|
+
*
|
|
224
|
+
* Note: adding `@tracked` to existing class members is handled by the
|
|
225
|
+
* combined class-body fix (in `createClassFix`), not here.
|
|
226
|
+
*
|
|
227
|
+
* @returns {import('eslint').Rule.Fix[]}
|
|
228
|
+
*/
|
|
229
|
+
function buildImportFixes(
|
|
230
|
+
fixer,
|
|
231
|
+
{ sourceCode, importsMap, usages, macroImportNodes }
|
|
232
|
+
) {
|
|
233
|
+
const fixes = [];
|
|
234
|
+
const allImportedNames = getImportedLocalNames(sourceCode);
|
|
235
|
+
|
|
236
|
+
// Exclude names we're about to remove from the collision set
|
|
237
|
+
const fixableLocalNames = new Set(usages.map((u) => u.localName));
|
|
238
|
+
for (const name of fixableLocalNames) {
|
|
239
|
+
allImportedNames.delete(name);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Collect all required new imports from fixable usages
|
|
243
|
+
const newImports = collectRequiredImports(usages);
|
|
244
|
+
|
|
245
|
+
// Determine conditional imports based on dep classification
|
|
246
|
+
const needsComputed = usages.some((u) => !u.allLocal);
|
|
247
|
+
const needsDependentKeyCompat = usages.some((u) => u.allLocal);
|
|
248
|
+
const needsTracked = usages.some(
|
|
249
|
+
(u) =>
|
|
250
|
+
(u.allLocal &&
|
|
251
|
+
(u.trackedDeps?.length > 0 || u.existingNodesToDecorate?.length > 0)) ||
|
|
252
|
+
u.transform.overrideTrackedFields
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
// Add in the order we want them to appear in the output
|
|
256
|
+
if (needsTracked) {
|
|
257
|
+
addToImportSet(newImports, "@glimmer/tracking", "tracked");
|
|
258
|
+
}
|
|
259
|
+
if (needsComputed) {
|
|
260
|
+
addToImportSet(newImports, "@ember/object", "computed");
|
|
261
|
+
}
|
|
262
|
+
// Setter-specific imports (e.g. set from @ember/object for alias).
|
|
263
|
+
// Added after computed so that named imports appear in the right order.
|
|
264
|
+
for (const usage of usages) {
|
|
265
|
+
if (!usage.allLocal && usage.transform.setterRequiredImports) {
|
|
266
|
+
for (const req of usage.transform.setterRequiredImports) {
|
|
267
|
+
addToImportSet(newImports, req.source, req.name, req.isDefault);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (needsDependentKeyCompat) {
|
|
272
|
+
addToImportSet(newImports, "@ember/object/compat", "dependentKeyCompat");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Build new import lines for required imports (once for all macro sources)
|
|
276
|
+
const newImportLines = [];
|
|
277
|
+
for (const [source, names] of newImports) {
|
|
278
|
+
const existing = importsMap.get(source);
|
|
279
|
+
|
|
280
|
+
if (existing) {
|
|
281
|
+
// Modify existing import — generate a fixImport call
|
|
282
|
+
const existingNamedSet = new Set(
|
|
283
|
+
existing.specifiers
|
|
284
|
+
.filter((s) => s.type === "ImportSpecifier")
|
|
285
|
+
.map((s) => s.imported.name)
|
|
286
|
+
);
|
|
287
|
+
const defaultSpec = existing.specifiers.find(
|
|
288
|
+
(s) => s.type === "ImportDefaultSpecifier"
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const namedToAdd = [];
|
|
292
|
+
let defaultToAdd;
|
|
293
|
+
|
|
294
|
+
for (const { name, isDefault } of names) {
|
|
295
|
+
if (isDefault) {
|
|
296
|
+
if (!defaultSpec) {
|
|
297
|
+
defaultToAdd = name;
|
|
298
|
+
}
|
|
299
|
+
} else if (!existingNamedSet.has(name)) {
|
|
300
|
+
const localName = allImportedNames.has(name) ? `${name}Import` : name;
|
|
301
|
+
namedToAdd.push(
|
|
302
|
+
localName === name ? name : `${name} as ${localName}`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (namedToAdd.length > 0 || defaultToAdd) {
|
|
308
|
+
fixes.push(
|
|
309
|
+
fixImport(fixer, existing.node, {
|
|
310
|
+
defaultImport: defaultToAdd,
|
|
311
|
+
namedImportsToAdd: namedToAdd,
|
|
312
|
+
})
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
// Build a new import line string (will be appended below)
|
|
317
|
+
newImportLines.push(
|
|
318
|
+
resolveNewImportLine(source, names, allImportedNames)
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Process each macro import node — remove/replace specifiers
|
|
324
|
+
let newImportsPlaced = false;
|
|
325
|
+
for (const [, importNode] of macroImportNodes) {
|
|
326
|
+
const removableSpecifiers = importNode.specifiers.filter(
|
|
327
|
+
(spec) =>
|
|
328
|
+
spec.type === "ImportSpecifier" &&
|
|
329
|
+
fixableLocalNames.has(spec.local.name) &&
|
|
330
|
+
usages
|
|
331
|
+
.filter((u) => u.localName === spec.local.name)
|
|
332
|
+
.every((u) => u.canAutoFix)
|
|
333
|
+
);
|
|
334
|
+
const remainingSpecifiers = importNode.specifiers.filter(
|
|
335
|
+
(s) => !removableSpecifiers.includes(s)
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
if (removableSpecifiers.length === 0) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (remainingSpecifiers.length === 0) {
|
|
343
|
+
// All specifiers removed — replace with new imports or remove entirely
|
|
344
|
+
if (!newImportsPlaced && newImportLines.length > 0) {
|
|
345
|
+
fixes.push(fixer.replaceText(importNode, newImportLines.join("\n")));
|
|
346
|
+
newImportsPlaced = true;
|
|
347
|
+
} else {
|
|
348
|
+
// Remove the entire import line (including trailing newline)
|
|
349
|
+
const text = sourceCode.getText();
|
|
350
|
+
let end = importNode.range[1];
|
|
351
|
+
if (end < text.length && text[end] === "\n") {
|
|
352
|
+
end++;
|
|
353
|
+
}
|
|
354
|
+
fixes.push(fixer.removeRange([importNode.range[0], end]));
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
// Some specifiers remain — remove fixable ones
|
|
358
|
+
const namesToRemove = removableSpecifiers.map((s) => s.imported.name);
|
|
359
|
+
fixes.push(
|
|
360
|
+
fixImport(fixer, importNode, {
|
|
361
|
+
namedImportsToRemove: namesToRemove,
|
|
362
|
+
})
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
// Append new import lines after this import (only once)
|
|
366
|
+
if (!newImportsPlaced && newImportLines.length > 0) {
|
|
367
|
+
for (const line of newImportLines) {
|
|
368
|
+
fixes.push(fixer.insertTextAfter(importNode, `\n${line}`));
|
|
369
|
+
}
|
|
370
|
+
newImportsPlaced = true;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return fixes;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Resolve import names (handling collisions) and delegate to
|
|
380
|
+
* the shared `buildImportStatement` utility.
|
|
381
|
+
*
|
|
382
|
+
* @param {string} source
|
|
383
|
+
* @param {Array<{name: string, isDefault?: boolean}>} names
|
|
384
|
+
* @param {Set<string>} allImportedNames
|
|
385
|
+
* @returns {string}
|
|
386
|
+
*/
|
|
387
|
+
function resolveNewImportLine(source, names, allImportedNames) {
|
|
388
|
+
const namedImports = [];
|
|
389
|
+
let defaultImport;
|
|
390
|
+
|
|
391
|
+
for (const { name, isDefault } of names) {
|
|
392
|
+
if (isDefault) {
|
|
393
|
+
defaultImport = name;
|
|
394
|
+
} else {
|
|
395
|
+
const localName = allImportedNames.has(name) ? `${name}Import` : name;
|
|
396
|
+
namedImports.push(localName === name ? name : `${name} as ${localName}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return buildImportStatement(source, { defaultImport, namedImports });
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
// Import helpers
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Collect all required imports from fixable usages into a Map.
|
|
409
|
+
*/
|
|
410
|
+
function collectRequiredImports(usages) {
|
|
411
|
+
const result = new Map();
|
|
412
|
+
|
|
413
|
+
for (const usage of usages) {
|
|
414
|
+
if (!usage.canAutoFix || !usage.transform.requiredImports) {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
for (const req of usage.transform.requiredImports) {
|
|
418
|
+
addToImportSet(result, req.source, req.name, req.isDefault);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Add a named or default import to the import set, avoiding duplicates.
|
|
427
|
+
*/
|
|
428
|
+
function addToImportSet(importSet, source, name, isDefault = false) {
|
|
429
|
+
if (!importSet.has(source)) {
|
|
430
|
+
importSet.set(source, []);
|
|
431
|
+
}
|
|
432
|
+
const list = importSet.get(source);
|
|
433
|
+
if (!list.some((i) => i.name === name && i.isDefault === isDefault)) {
|
|
434
|
+
list.push({ name, isDefault });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
collectImports,
|
|
5
5
|
getImportedLocalNames,
|
|
6
6
|
} from "./utils/analyze-imports.mjs";
|
|
7
|
-
import { fixImport } from "./utils/fix-import.mjs";
|
|
7
|
+
import { buildImportStatement, fixImport } from "./utils/fix-import.mjs";
|
|
8
8
|
|
|
9
9
|
const USE_COMPUTED_INSTEAD = "Use `@computed` instead of `@{{name}}`";
|
|
10
10
|
|
|
@@ -75,12 +75,10 @@ function fixDiscourseImport(
|
|
|
75
75
|
})
|
|
76
76
|
);
|
|
77
77
|
} else {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
)
|
|
83
|
-
);
|
|
78
|
+
const newImport = buildImportStatement("@ember/object", {
|
|
79
|
+
namedImports: [computedImportString],
|
|
80
|
+
});
|
|
81
|
+
fixes.push(fixer.insertTextAfter(importNode, `\n${newImport}`));
|
|
84
82
|
}
|
|
85
83
|
}
|
|
86
84
|
} else {
|
|
@@ -103,7 +101,9 @@ function fixDiscourseImport(
|
|
|
103
101
|
fixes.push(
|
|
104
102
|
fixer.replaceText(
|
|
105
103
|
importNode,
|
|
106
|
-
|
|
104
|
+
buildImportStatement("@ember/object", {
|
|
105
|
+
namedImports: [computedImportString],
|
|
106
|
+
})
|
|
107
107
|
)
|
|
108
108
|
);
|
|
109
109
|
}
|
|
@@ -128,13 +128,15 @@ function fixDiscourseImport(
|
|
|
128
128
|
}
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
131
|
+
fixes.push(
|
|
132
|
+
fixer.replaceText(
|
|
133
|
+
importNode,
|
|
134
|
+
buildImportStatement(importNode.source.value, {
|
|
135
|
+
defaultImport: "discourseComputed",
|
|
136
|
+
namedImports: namedImportStrings,
|
|
137
|
+
})
|
|
138
|
+
)
|
|
139
|
+
);
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
if (!hasComputedImport) {
|
|
@@ -145,12 +147,10 @@ function fixDiscourseImport(
|
|
|
145
147
|
})
|
|
146
148
|
);
|
|
147
149
|
} else {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
)
|
|
153
|
-
);
|
|
150
|
+
const newImport = buildImportStatement("@ember/object", {
|
|
151
|
+
namedImports: [computedImportString],
|
|
152
|
+
});
|
|
153
|
+
fixes.push(fixer.insertTextAfter(importNode, `\n${newImport}`));
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { buildImportStatement } from "./utils/fix-import.mjs";
|
|
2
|
+
|
|
1
3
|
export default {
|
|
2
4
|
meta: {
|
|
3
5
|
type: "suggestion",
|
|
@@ -24,11 +26,13 @@ export default {
|
|
|
24
26
|
sourceName = "notEq";
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
const namedImport =
|
|
30
|
+
localName === sourceName
|
|
31
|
+
? localName
|
|
32
|
+
: `${sourceName} as ${localName}`;
|
|
33
|
+
code = buildImportStatement("truth-helpers", {
|
|
34
|
+
namedImports: [namedImport],
|
|
35
|
+
});
|
|
32
36
|
|
|
33
37
|
return fixer.replaceText(node, code);
|
|
34
38
|
},
|
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build an import statement string from its parts.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} source - The import source (e.g. "@ember/object").
|
|
5
|
+
* @param {Object} [options]
|
|
6
|
+
* @param {string|null} [options.defaultImport] - Default import name, or null.
|
|
7
|
+
* @param {string[]} [options.namedImports] - Named import specifiers (may include aliases like "foo as bar").
|
|
8
|
+
* @param {string} [options.quote] - Quote style: `"` (default) or `'`.
|
|
9
|
+
* @returns {string} A complete import statement string.
|
|
10
|
+
*/
|
|
11
|
+
export function buildImportStatement(
|
|
12
|
+
source,
|
|
13
|
+
{ defaultImport = null, namedImports = [] } = {}
|
|
14
|
+
) {
|
|
15
|
+
let stmt = "import ";
|
|
16
|
+
if (defaultImport) {
|
|
17
|
+
stmt += defaultImport;
|
|
18
|
+
if (namedImports.length > 0) {
|
|
19
|
+
stmt += ", ";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (namedImports.length > 0) {
|
|
23
|
+
stmt += `{ ${namedImports.join(", ")} }`;
|
|
24
|
+
}
|
|
25
|
+
stmt += ` from "${source}";`;
|
|
26
|
+
return stmt;
|
|
27
|
+
}
|
|
28
|
+
|
|
1
29
|
/**
|
|
2
30
|
* Fix an import declaration
|
|
3
31
|
*
|
|
@@ -51,18 +79,10 @@ export function fixImport(
|
|
|
51
79
|
])
|
|
52
80
|
);
|
|
53
81
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (finalNamedImports.length > 0) {
|
|
59
|
-
newImportStatement += ", ";
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (finalNamedImports.length > 0) {
|
|
63
|
-
newImportStatement += `{ ${finalNamedImports.join(", ")} }`;
|
|
64
|
-
}
|
|
65
|
-
newImportStatement += ` from "${importDeclarationNode.source.value}";`;
|
|
82
|
+
const newImportStatement = buildImportStatement(
|
|
83
|
+
importDeclarationNode.source.value,
|
|
84
|
+
{ defaultImport: finalDefaultImport, namedImports: finalNamedImports }
|
|
85
|
+
);
|
|
66
86
|
|
|
67
87
|
// Replace the entire import declaration
|
|
68
88
|
return fixer.replaceText(importDeclarationNode, newImportStatement);
|
package/eslint.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import lineBeforeDefaultExport from "./eslint-rules/line-before-default-export.m
|
|
|
23
23
|
import linesBetweenClassMembers from "./eslint-rules/lines-between-class-members.mjs";
|
|
24
24
|
import migrateTrackedBuiltInsToEmberCollections from "./eslint-rules/migrate-tracked-built-ins-to-ember-collections.mjs";
|
|
25
25
|
import movedPackagesImportPaths from "./eslint-rules/moved-packages-import-paths.mjs";
|
|
26
|
+
import noComputedMacros from "./eslint-rules/no-computed-macros.mjs";
|
|
26
27
|
import noCurlyComponents from "./eslint-rules/no-curly-components.mjs";
|
|
27
28
|
import noDiscourseComputed from "./eslint-rules/no-discourse-computed.mjs";
|
|
28
29
|
import noOnclick from "./eslint-rules/no-onclick.mjs";
|
|
@@ -146,6 +147,7 @@ export default [
|
|
|
146
147
|
"no-route-template": noRouteTemplate,
|
|
147
148
|
"template-tag-no-self-this": templateTagNoSelfThis,
|
|
148
149
|
"moved-packages-import-paths": movedPackagesImportPaths,
|
|
150
|
+
"no-computed-macros": noComputedMacros,
|
|
149
151
|
"no-discourse-computed": noDiscourseComputed,
|
|
150
152
|
"test-filename-suffix": testFilenameSuffix,
|
|
151
153
|
"no-unnecessary-tracked": noUnnecessaryTracked,
|
|
@@ -313,8 +315,9 @@ export default [
|
|
|
313
315
|
"discourse/no-route-template": ["error"],
|
|
314
316
|
"discourse/moved-packages-import-paths": ["error"],
|
|
315
317
|
"discourse/test-filename-suffix": ["error"],
|
|
316
|
-
"discourse/
|
|
318
|
+
"discourse/no-computed-macros": ["error"],
|
|
317
319
|
"discourse/no-discourse-computed": ["error"],
|
|
320
|
+
"discourse/keep-array-sorted": ["error"],
|
|
318
321
|
"discourse/no-unnecessary-tracked": ["warn"],
|
|
319
322
|
"discourse/migrate-tracked-built-ins-to-ember-collections": ["error"],
|
|
320
323
|
},
|