@discourse/lint-configs 2.39.0 → 2.40.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.
@@ -46,6 +46,18 @@ export default {
46
46
  return fixer.replaceText(node.source, `"discourse/lib/get-url"`);
47
47
  },
48
48
  });
49
+ } else if (node.source.value === "discourse/lib/transformer/registry") {
50
+ context.report({
51
+ node,
52
+ message:
53
+ "Use 'discourse/lib/registry/transformers' instead of 'discourse/lib/transformer/registry'",
54
+ fix(fixer) {
55
+ return fixer.replaceText(
56
+ node.source,
57
+ `"discourse/lib/registry/transformers"`
58
+ );
59
+ },
60
+ });
49
61
  } else if (
50
62
  node.source.value === "discourse/helpers/html-safe" &&
51
63
  node.specifiers[0]?.local.name === "htmlSafe"
@@ -0,0 +1,192 @@
1
+ export default {
2
+ meta: {
3
+ type: "suggestion",
4
+ docs: {
5
+ description: "Ensure marked arrays are sorted",
6
+ },
7
+ fixable: "code",
8
+ schema: [],
9
+ },
10
+
11
+ create(context) {
12
+ const sourceCode = context.getSourceCode();
13
+
14
+ return {
15
+ ArrayExpression(node) {
16
+ const checkNodeForComment = (n) => {
17
+ const comments = sourceCode.getCommentsBefore(n);
18
+ return comments.some(
19
+ (c) => c.value.trim() === "eslint-discourse keep-array-sorted"
20
+ );
21
+ };
22
+
23
+ let hasSortComment = checkNodeForComment(node);
24
+
25
+ if (!hasSortComment) {
26
+ let statement = node;
27
+ while (
28
+ statement &&
29
+ !statement.type.endsWith("Statement") &&
30
+ statement.type !== "VariableDeclaration" &&
31
+ statement.type !== "ExportDefaultDeclaration" &&
32
+ statement.type !== "ExportNamedDeclaration"
33
+ ) {
34
+ statement = statement.parent;
35
+ }
36
+ if (statement && checkNodeForComment(statement)) {
37
+ hasSortComment = true;
38
+ } else if (
39
+ statement &&
40
+ statement.parent &&
41
+ checkNodeForComment(statement.parent)
42
+ ) {
43
+ hasSortComment = true;
44
+ }
45
+ }
46
+
47
+ if (!hasSortComment) {
48
+ return;
49
+ }
50
+
51
+ const elements = node.elements;
52
+ if (elements.length < 2) {
53
+ return;
54
+ }
55
+
56
+ // Check if all elements are Literals and of the same type (string or number)
57
+ const firstType =
58
+ elements[0] && elements[0].type === "Literal"
59
+ ? typeof elements[0].value
60
+ : null;
61
+
62
+ if (firstType !== "string" && firstType !== "number") {
63
+ return;
64
+ }
65
+
66
+ const canSort = elements.every(
67
+ (el) => el && el.type === "Literal" && typeof el.value === firstType
68
+ );
69
+
70
+ if (!canSort) {
71
+ return;
72
+ }
73
+
74
+ // An explicit comparator is needed because the default sort() converts elements to strings,
75
+ // which causes incorrect ordering for numbers (e.g., [1, 10, 2] instead of [1, 2, 10]).
76
+ const sortedElements = [...elements].sort((a, b) => {
77
+ if (a.value < b.value) {
78
+ return -1;
79
+ }
80
+ if (a.value > b.value) {
81
+ return 1;
82
+ }
83
+ return 0;
84
+ });
85
+
86
+ const isSorted = elements.every(
87
+ (el, i) => el.value === sortedElements[i].value
88
+ );
89
+
90
+ if (!isSorted) {
91
+ context.report({
92
+ node,
93
+ message: "Array should be sorted.",
94
+ fix(fixer) {
95
+ const isMultiline = node.loc.start.line !== node.loc.end.line;
96
+
97
+ const line = sourceCode.lines[node.loc.start.line - 1];
98
+ const indent = line.match(/^\s*/)[0];
99
+
100
+ let innerIndent = "";
101
+ if (isMultiline && elements.length > 0) {
102
+ const firstElementLine =
103
+ sourceCode.lines[elements[0].loc.start.line - 1];
104
+ innerIndent = firstElementLine.match(/^\s*/)[0];
105
+ }
106
+
107
+ const getElementData = (el) => {
108
+ const text = sourceCode.getText(el);
109
+ const nextToken = sourceCode.getTokenAfter(el);
110
+ const commentsBefore = sourceCode.getCommentsBefore(el);
111
+ let commentsAfter = sourceCode.getCommentsAfter(el);
112
+ if (nextToken && nextToken.value === ",") {
113
+ commentsAfter = commentsAfter.concat(
114
+ sourceCode.getCommentsAfter(nextToken)
115
+ );
116
+ }
117
+
118
+ const sameLineCommentsAfter = commentsAfter.filter(
119
+ (c) => c.loc.start.line === el.loc.end.line
120
+ );
121
+
122
+ const leadingComments = commentsBefore
123
+ .filter((c) => {
124
+ if (
125
+ c.value.trim() === "eslint-discourse keep-array-sorted"
126
+ ) {
127
+ return false;
128
+ }
129
+ // If the comment is on the same line as the previous element OR the previous comma,
130
+ // it should be treated as a trailing comment of THAT element, not a leading of this one.
131
+ const prevToken = sourceCode.getTokenBefore(c);
132
+ if (
133
+ prevToken &&
134
+ prevToken.loc.end.line === c.loc.start.line
135
+ ) {
136
+ return false;
137
+ }
138
+ return true;
139
+ })
140
+ .map((c) => sourceCode.getText(c))
141
+ .join("\n" + innerIndent);
142
+
143
+ return {
144
+ text,
145
+ leadingComments: leadingComments
146
+ ? leadingComments + "\n" + innerIndent
147
+ : "",
148
+ comment: sameLineCommentsAfter.length
149
+ ? " " +
150
+ sameLineCommentsAfter
151
+ .map((c) => sourceCode.getText(c))
152
+ .join(" ")
153
+ : "",
154
+ };
155
+ };
156
+
157
+ const elementToData = new Map();
158
+ elements.forEach((el) => {
159
+ elementToData.set(el, getElementData(el));
160
+ });
161
+
162
+ if (isMultiline) {
163
+ const lastToken = sourceCode.getLastToken(node);
164
+ const tokenBeforeLast = sourceCode.getTokenBefore(lastToken);
165
+ const hasTrailingComma = tokenBeforeLast.value === ",";
166
+
167
+ const lines = sortedElements.map((el, i) => {
168
+ const d = elementToData.get(el);
169
+ const isLast = i === elements.length - 1;
170
+ const comma = !isLast || hasTrailingComma ? "," : "";
171
+ return `${d.leadingComments}${d.text}${comma}${d.comment}`;
172
+ });
173
+
174
+ return fixer.replaceText(
175
+ node,
176
+ `[\n${innerIndent}${lines.join(
177
+ `\n${innerIndent}`
178
+ )}\n${indent}]`
179
+ );
180
+ } else {
181
+ const content = sortedElements
182
+ .map((el) => elementToData.get(el).text)
183
+ .join(", ");
184
+ return fixer.replaceText(node, `[${content}]`);
185
+ }
186
+ },
187
+ });
188
+ }
189
+ },
190
+ };
191
+ },
192
+ };
package/eslint.mjs CHANGED
@@ -17,6 +17,7 @@ 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";
@@ -122,6 +123,7 @@ export default [
122
123
  rules: {
123
124
  "i18n-import-location": i18nImport,
124
125
  "i18n-t": i18nT,
126
+ "keep-array-sorted": keepArraySorted,
125
127
  "service-inject-import": serviceInjectImport,
126
128
  "truth-helpers-imports": truthHelpersImports,
127
129
  "no-unused-services": noUnusedServices,
@@ -209,6 +211,8 @@ export default [
209
211
  "ember/no-unnecessary-index-route": "off", // the assumption made in this rule doesn't seem to be true in discourse router
210
212
  "ember/no-unnecessary-service-injection-argument": "error",
211
213
  "ember/no-replace-test-comments": "error",
214
+ "ember/route-path-style": "error",
215
+ "qunit/no-loose-assertions": "error",
212
216
  "qunit/no-identical-names": "off", // the rule doesn't consider that tests might be in different `acceptance` modules
213
217
  "sort-class-members/sort-class-members": [
214
218
  "error",
@@ -309,6 +313,7 @@ export default [
309
313
  "discourse/no-route-template": ["error"],
310
314
  "discourse/moved-packages-import-paths": ["error"],
311
315
  "discourse/test-filename-suffix": ["error"],
316
+ "discourse/keep-array-sorted": ["error"],
312
317
  },
313
318
  },
314
319
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@discourse/lint-configs",
3
- "version": "2.39.0",
3
+ "version": "2.40.0",
4
4
  "description": "Shareable lint configs for Discourse core, plugins, and themes",
5
5
  "author": "Discourse",
6
6
  "license": "MIT",