@discourse/lint-configs 2.40.0 → 2.42.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/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 +3 -0
- package/package.json +5 -5
|
@@ -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
|
@@ -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 movedPackagesImportPaths from "./eslint-rules/moved-packages-import-paths.mjs";
|
|
25
25
|
import noCurlyComponents from "./eslint-rules/no-curly-components.mjs";
|
|
26
|
+
import noDiscourseComputed from "./eslint-rules/no-discourse-computed.mjs";
|
|
26
27
|
import noOnclick from "./eslint-rules/no-onclick.mjs";
|
|
27
28
|
import noRouteTemplate from "./eslint-rules/no-route-template.mjs";
|
|
28
29
|
import noSimpleQuerySelector from "./eslint-rules/no-simple-query-selector.mjs";
|
|
@@ -143,6 +144,7 @@ export default [
|
|
|
143
144
|
"no-route-template": noRouteTemplate,
|
|
144
145
|
"template-tag-no-self-this": templateTagNoSelfThis,
|
|
145
146
|
"moved-packages-import-paths": movedPackagesImportPaths,
|
|
147
|
+
"no-discourse-computed": noDiscourseComputed,
|
|
146
148
|
"test-filename-suffix": testFilenameSuffix,
|
|
147
149
|
},
|
|
148
150
|
},
|
|
@@ -314,6 +316,7 @@ export default [
|
|
|
314
316
|
"discourse/moved-packages-import-paths": ["error"],
|
|
315
317
|
"discourse/test-filename-suffix": ["error"],
|
|
316
318
|
"discourse/keep-array-sorted": ["error"],
|
|
319
|
+
"discourse/no-discourse-computed": ["error"],
|
|
317
320
|
},
|
|
318
321
|
},
|
|
319
322
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@discourse/lint-configs",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.42.0",
|
|
4
4
|
"description": "Shareable lint configs for Discourse core, plugins, and themes",
|
|
5
5
|
"author": "Discourse",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,13 +39,13 @@
|
|
|
39
39
|
"eslint-plugin-decorator-position": "^6.0.0",
|
|
40
40
|
"eslint-plugin-ember": "^12.7.5",
|
|
41
41
|
"eslint-plugin-import": "^2.32.0",
|
|
42
|
-
"eslint-plugin-qunit": "^8.2.
|
|
42
|
+
"eslint-plugin-qunit": "^8.2.6",
|
|
43
43
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
44
44
|
"eslint-plugin-sort-class-members": "^1.21.0",
|
|
45
|
-
"globals": "^17.
|
|
45
|
+
"globals": "^17.4.0",
|
|
46
46
|
"prettier": "^3.8.1",
|
|
47
47
|
"prettier-plugin-ember-template-tag": "^2.1.3",
|
|
48
|
-
"stylelint": "^17.
|
|
48
|
+
"stylelint": "^17.4.0",
|
|
49
49
|
"stylelint-config-standard": "^40.0.0",
|
|
50
50
|
"stylelint-config-standard-scss": "^17.0.0",
|
|
51
51
|
"stylelint-scss": "^7.0.0",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"ember-template-lint": "7.9.3",
|
|
56
56
|
"eslint": "9.39.2",
|
|
57
57
|
"prettier": "3.8.1",
|
|
58
|
-
"stylelint": "17.
|
|
58
|
+
"stylelint": "17.4.0"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"lint": "eslint --no-error-on-unmatched-pattern \"**/*.{cjs,mjs,js}\" && pnpm prettier --check \"**/*.{cjs,mjs,js}\"",
|