@discourse/lint-configs 2.39.1 → 2.41.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/deprecated-imports.mjs +12 -0
- package/eslint-rules/keep-array-sorted.mjs +192 -0
- package/eslint-rules/no-discourse-computed/discourse-computed-analysis.mjs +614 -0
- package/eslint-rules/no-discourse-computed/discourse-computed-fixer.mjs +319 -0
- package/eslint-rules/no-discourse-computed.mjs +388 -0
- package/eslint-rules/utils/analyze-imports.mjs +65 -0
- package/eslint-rules/utils/property-path.mjs +78 -0
- package/eslint.mjs +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Generic utilities to inspect ImportDeclaration nodes in a source AST.
|
|
2
|
+
// These helpers are intentionally generic so they can be reused by multiple
|
|
3
|
+
// lint rules in this repo instead of containing rule-specific logic.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {import('estree').Node} Node
|
|
7
|
+
* @typedef {import('estree').ImportDeclaration} ImportDeclaration
|
|
8
|
+
* @typedef {Object} ImportInfo
|
|
9
|
+
* @property {ImportDeclaration} node - The ImportDeclaration AST node
|
|
10
|
+
* @property {Array<import('estree').ImportSpecifier|import('estree').ImportDefaultSpecifier|import('estree').ImportNamespaceSpecifier>} specifiers - The import specifiers
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Collects all ImportDeclaration nodes from the provided ESLint SourceCode
|
|
15
|
+
* and returns a Map keyed by the import source string (e.g. "@ember/object").
|
|
16
|
+
*
|
|
17
|
+
* The resulting Map values are objects with the original ImportDeclaration
|
|
18
|
+
* node and a shallow copy of its specifiers. Rules can use this to inspect
|
|
19
|
+
* existing imports and build fixes that modify or add import statements.
|
|
20
|
+
*
|
|
21
|
+
* @param {import('eslint').SourceCode} sourceCode - ESLint SourceCode object
|
|
22
|
+
* @returns {Map<string, ImportInfo>} Map from import source value to import info
|
|
23
|
+
*/
|
|
24
|
+
export function collectImports(sourceCode) {
|
|
25
|
+
const imports = new Map();
|
|
26
|
+
|
|
27
|
+
sourceCode.ast.body.forEach((statement) => {
|
|
28
|
+
if (statement && statement.type === "ImportDeclaration") {
|
|
29
|
+
const sourceValue = statement.source && statement.source.value;
|
|
30
|
+
if (!imports.has(sourceValue)) {
|
|
31
|
+
imports.set(sourceValue, {
|
|
32
|
+
node: statement,
|
|
33
|
+
specifiers: Array.isArray(statement.specifiers)
|
|
34
|
+
? statement.specifiers.slice()
|
|
35
|
+
: [],
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return imports;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns the set of all locally imported identifier names in the file. This
|
|
46
|
+
* is useful to detect name collisions before inserting new imports (for
|
|
47
|
+
* example, to decide whether to alias an imported identifier).
|
|
48
|
+
*
|
|
49
|
+
* @param {import('eslint').SourceCode} sourceCode - ESLint SourceCode object
|
|
50
|
+
* @returns {Set<string>} Set of local import names
|
|
51
|
+
*/
|
|
52
|
+
export function getImportedLocalNames(sourceCode) {
|
|
53
|
+
const names = new Set();
|
|
54
|
+
const imports = collectImports(sourceCode);
|
|
55
|
+
|
|
56
|
+
for (const { specifiers } of imports.values()) {
|
|
57
|
+
specifiers.forEach((spec) => {
|
|
58
|
+
if (spec?.local?.name) {
|
|
59
|
+
names.add(spec.local.name);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return names;
|
|
65
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Utilities for working with property paths used in @discourseComputed
|
|
2
|
+
// decorators. These helpers make it easier to reason about and convert
|
|
3
|
+
// parameter property paths to `this`-based optional chaining accessors.
|
|
4
|
+
|
|
5
|
+
// Extract the "clean" attribute path before special tokens like @each, [],
|
|
6
|
+
// or template-style placeholders. Example:
|
|
7
|
+
// - "items.@each.title" -> "items"
|
|
8
|
+
// - "data.0.value" -> "data.0.value" (numeric index preserved)
|
|
9
|
+
// - "payload{?}.name" -> "payload"
|
|
10
|
+
export function niceAttr(attr) {
|
|
11
|
+
if (!attr || typeof attr !== "string") {
|
|
12
|
+
return "";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const parts = attr.split(".");
|
|
16
|
+
let i;
|
|
17
|
+
|
|
18
|
+
for (i = 0; i < parts.length; i++) {
|
|
19
|
+
if (parts[i] === "@each" || parts[i] === "[]" || parts[i].includes("{")) {
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return parts.slice(0, i).join(".");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Convert a `propertyPath` (like "model.poll.title" or "data.0.value") into
|
|
28
|
+
// a `this`-prefixed access with optional chaining where appropriate.
|
|
29
|
+
// Parameters:
|
|
30
|
+
// - propertyPath: original path used in the decorator or parameter mapping
|
|
31
|
+
// - useOptionalChaining: if true, emit `?.` and `?.[...]` for nested parts
|
|
32
|
+
// - needsTrailingChaining: when true and the path requires it, append a
|
|
33
|
+
// trailing `?.` so that subsequent member access becomes part of the chain.
|
|
34
|
+
export function propertyPathToOptionalChaining(
|
|
35
|
+
propertyPath,
|
|
36
|
+
useOptionalChaining = true,
|
|
37
|
+
needsTrailingChaining = false
|
|
38
|
+
) {
|
|
39
|
+
const cleanPath = niceAttr(propertyPath);
|
|
40
|
+
const wasExtracted = cleanPath !== propertyPath;
|
|
41
|
+
|
|
42
|
+
if (!cleanPath) {
|
|
43
|
+
return "this";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const parts = cleanPath.split(".");
|
|
47
|
+
let result = "this";
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < parts.length; i++) {
|
|
50
|
+
const part = parts[i];
|
|
51
|
+
|
|
52
|
+
if (/^\d+$/.test(part)) {
|
|
53
|
+
// Numeric index -> bracket notation
|
|
54
|
+
if (useOptionalChaining) {
|
|
55
|
+
result += `?.[${part}]`;
|
|
56
|
+
} else {
|
|
57
|
+
result += `[${part}]`;
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
if (i === 0) {
|
|
61
|
+
result += `.${part}`;
|
|
62
|
+
} else {
|
|
63
|
+
result += useOptionalChaining ? `?.${part}` : `.${part}`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (needsTrailingChaining && useOptionalChaining) {
|
|
69
|
+
if (
|
|
70
|
+
(wasExtracted && parts.length === 1) ||
|
|
71
|
+
(!wasExtracted && parts.length > 1)
|
|
72
|
+
) {
|
|
73
|
+
result += "?.";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return result;
|
|
78
|
+
}
|
package/eslint.mjs
CHANGED
|
@@ -17,11 +17,13 @@ import deprecatedPluginApis from "./eslint-rules/deprecated-plugin-apis.mjs";
|
|
|
17
17
|
import discourseCommonImports from "./eslint-rules/discourse-common-imports.mjs";
|
|
18
18
|
import i18nImport from "./eslint-rules/i18n-import-location.mjs";
|
|
19
19
|
import i18nT from "./eslint-rules/i18n-t.mjs";
|
|
20
|
+
import keepArraySorted from "./eslint-rules/keep-array-sorted.mjs";
|
|
20
21
|
import lineAfterImports from "./eslint-rules/line-after-imports.mjs";
|
|
21
22
|
import lineBeforeDefaultExport from "./eslint-rules/line-before-default-export.mjs";
|
|
22
23
|
import linesBetweenClassMembers from "./eslint-rules/lines-between-class-members.mjs";
|
|
23
24
|
import movedPackagesImportPaths from "./eslint-rules/moved-packages-import-paths.mjs";
|
|
24
25
|
import noCurlyComponents from "./eslint-rules/no-curly-components.mjs";
|
|
26
|
+
import noDiscourseComputed from "./eslint-rules/no-discourse-computed.mjs";
|
|
25
27
|
import noOnclick from "./eslint-rules/no-onclick.mjs";
|
|
26
28
|
import noRouteTemplate from "./eslint-rules/no-route-template.mjs";
|
|
27
29
|
import noSimpleQuerySelector from "./eslint-rules/no-simple-query-selector.mjs";
|
|
@@ -122,6 +124,7 @@ export default [
|
|
|
122
124
|
rules: {
|
|
123
125
|
"i18n-import-location": i18nImport,
|
|
124
126
|
"i18n-t": i18nT,
|
|
127
|
+
"keep-array-sorted": keepArraySorted,
|
|
125
128
|
"service-inject-import": serviceInjectImport,
|
|
126
129
|
"truth-helpers-imports": truthHelpersImports,
|
|
127
130
|
"no-unused-services": noUnusedServices,
|
|
@@ -141,6 +144,7 @@ export default [
|
|
|
141
144
|
"no-route-template": noRouteTemplate,
|
|
142
145
|
"template-tag-no-self-this": templateTagNoSelfThis,
|
|
143
146
|
"moved-packages-import-paths": movedPackagesImportPaths,
|
|
147
|
+
"no-discourse-computed": noDiscourseComputed,
|
|
144
148
|
"test-filename-suffix": testFilenameSuffix,
|
|
145
149
|
},
|
|
146
150
|
},
|
|
@@ -311,6 +315,8 @@ export default [
|
|
|
311
315
|
"discourse/no-route-template": ["error"],
|
|
312
316
|
"discourse/moved-packages-import-paths": ["error"],
|
|
313
317
|
"discourse/test-filename-suffix": ["error"],
|
|
318
|
+
"discourse/keep-array-sorted": ["error"],
|
|
319
|
+
"discourse/no-discourse-computed": ["error"],
|
|
314
320
|
},
|
|
315
321
|
},
|
|
316
322
|
{
|