@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
|
{
|