@discourse/lint-configs 2.46.0 → 3.0.1
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/README.md +0 -6
- package/eslint-rules/no-at-class.mjs +46 -0
- package/eslint-rules/no-template-lint-directives.mjs +103 -0
- package/eslint-rules/plugin-outlet-lazy-hash.mjs +43 -0
- package/eslint-rules/template-tag-no-self-this.mjs +23 -6
- package/eslint.mjs +54 -9
- package/package.json +3 -10
- package/template-lint-rules/index.mjs +0 -15
- package/template-lint-rules/no-at-class.mjs +0 -33
- package/template-lint-rules/no-implicit-this.mjs +0 -174
- package/template-lint-rules/plugin-outlet-lazy-hash.mjs +0 -27
- package/template-lint.config.cjs +0 -65
package/README.md
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const AFFECTED_COMPONENTS = new Set([
|
|
2
|
+
"DButton",
|
|
3
|
+
"DModal",
|
|
4
|
+
"TableHeaderToggle",
|
|
5
|
+
"Textarea",
|
|
6
|
+
"TextArea",
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "suggestion",
|
|
12
|
+
docs: {
|
|
13
|
+
description:
|
|
14
|
+
"Disallow `@class` on Discourse components that own their root element.",
|
|
15
|
+
},
|
|
16
|
+
fixable: "code",
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
noAtClass: "Use `class` instead of `@class` for {{tag}}.",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
create(context) {
|
|
24
|
+
return {
|
|
25
|
+
GlimmerElementNode(node) {
|
|
26
|
+
if (!AFFECTED_COMPONENTS.has(node.tag) || !node.attributes) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
for (const attribute of node.attributes) {
|
|
30
|
+
if (attribute.name !== "@class") {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
context.report({
|
|
34
|
+
node: attribute,
|
|
35
|
+
messageId: "noAtClass",
|
|
36
|
+
data: { tag: node.tag },
|
|
37
|
+
fix(fixer) {
|
|
38
|
+
const [start] = attribute.range;
|
|
39
|
+
return fixer.replaceTextRange([start, start + 1], "");
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const DIRECTIVE_COMMENT =
|
|
2
|
+
/^(?<open>\{\{!(?:--)?)\s*template-lint-(?<action>disable|enable)(?<rules>\s+[^]*?)?\s*(?:--)?\}\}$/;
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
meta: {
|
|
6
|
+
type: "suggestion",
|
|
7
|
+
docs: {
|
|
8
|
+
description:
|
|
9
|
+
"Convert `{{! template-lint-disable }}` comments to `{{! eslint-disable }}` equivalents.",
|
|
10
|
+
},
|
|
11
|
+
fixable: "code",
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
convert: "Use `eslint-{{action}}` instead of `template-lint-{{action}}`.",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
create(context) {
|
|
19
|
+
const sourceCode = context.sourceCode;
|
|
20
|
+
// Glimmer parks comments that appear inside an element's opening tag
|
|
21
|
+
// (between attributes) on `element.comments`, not in the children. We
|
|
22
|
+
// collect them so the Program:exit pass can lift a converted directive
|
|
23
|
+
// to before its enclosing element — ESLint scopes line-based directives
|
|
24
|
+
// from where they appear, and the violation typically lives on the
|
|
25
|
+
// element's start line, not on the in-attribute line.
|
|
26
|
+
const elementByCommentStart = new Map();
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
GlimmerElementNode(node) {
|
|
30
|
+
for (const c of node.comments || []) {
|
|
31
|
+
if (c.range) {
|
|
32
|
+
elementByCommentStart.set(c.range[0], node);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
"Program:exit"() {
|
|
38
|
+
for (const comment of sourceCode.getAllComments()) {
|
|
39
|
+
const raw = sourceCode.text.slice(...comment.range);
|
|
40
|
+
const converted = convertComment(raw);
|
|
41
|
+
if (!converted) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const enclosingElement = elementByCommentStart.get(comment.range[0]);
|
|
45
|
+
context.report({
|
|
46
|
+
node: comment,
|
|
47
|
+
messageId: "convert",
|
|
48
|
+
data: { action: converted.action },
|
|
49
|
+
fix: (fixer) =>
|
|
50
|
+
enclosingElement
|
|
51
|
+
? liftBeforeElement(
|
|
52
|
+
fixer,
|
|
53
|
+
comment,
|
|
54
|
+
converted.newComment,
|
|
55
|
+
enclosingElement,
|
|
56
|
+
sourceCode
|
|
57
|
+
)
|
|
58
|
+
: fixer.replaceTextRange(comment.range, converted.newComment),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function convertComment(rawComment) {
|
|
67
|
+
const match = rawComment.match(DIRECTIVE_COMMENT);
|
|
68
|
+
if (!match) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const { open, action, rules: rulesPart } = match.groups;
|
|
72
|
+
// ESLint directives use comma-separated rule names; template-lint uses
|
|
73
|
+
// whitespace. Prefix each with `ember/template-` to land in the namespace
|
|
74
|
+
// where the ports live.
|
|
75
|
+
const rules = (rulesPart || "")
|
|
76
|
+
.trim()
|
|
77
|
+
.split(/\s+/)
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.map((r) => `ember/template-${r}`)
|
|
80
|
+
.join(", ");
|
|
81
|
+
const body = rules ? `eslint-${action} ${rules}` : `eslint-${action}`;
|
|
82
|
+
// Emit symmetric markers regardless of what the source did.
|
|
83
|
+
const close = open.length === 5 ? "--}}" : "}}";
|
|
84
|
+
return {
|
|
85
|
+
action,
|
|
86
|
+
newComment: `${open} ${body} ${close}`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Strip the in-attribute comment line entirely (leading indent through
|
|
91
|
+
// trailing newline) and re-emit the converted directive on its own line at
|
|
92
|
+
// the element's indent, just above the element.
|
|
93
|
+
function liftBeforeElement(fixer, comment, newComment, element, sourceCode) {
|
|
94
|
+
const text = sourceCode.text;
|
|
95
|
+
const lineStart = comment.range[0] - comment.loc.start.column;
|
|
96
|
+
const lineEnd =
|
|
97
|
+
text[comment.range[1]] === "\n" ? comment.range[1] + 1 : comment.range[1];
|
|
98
|
+
const indent = " ".repeat(element.loc.start.column);
|
|
99
|
+
return [
|
|
100
|
+
fixer.removeRange([lineStart, lineEnd]),
|
|
101
|
+
fixer.insertTextBeforeRange(element.range, `${newComment}\n${indent}`),
|
|
102
|
+
];
|
|
103
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
meta: {
|
|
3
|
+
type: "suggestion",
|
|
4
|
+
docs: {
|
|
5
|
+
description:
|
|
6
|
+
"Require `{{lazyHash}}` instead of `{{hash}}` for `@outletArgs` on `<PluginOutlet>`.",
|
|
7
|
+
},
|
|
8
|
+
schema: [],
|
|
9
|
+
messages: {
|
|
10
|
+
useLazyHash:
|
|
11
|
+
"Use {{lazyHash}} instead of {{hash}} for @outletArgs in <PluginOutlet>.",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
create(context) {
|
|
16
|
+
return {
|
|
17
|
+
GlimmerElementNode(node) {
|
|
18
|
+
if (node.tag !== "PluginOutlet" || !node.attributes) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const outletArgsAttr = node.attributes.find(
|
|
22
|
+
(attr) => attr.name === "@outletArgs"
|
|
23
|
+
);
|
|
24
|
+
if (!outletArgsAttr || !outletArgsAttr.value) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const value = outletArgsAttr.value;
|
|
28
|
+
if (
|
|
29
|
+
value.type === "GlimmerMustacheStatement" &&
|
|
30
|
+
value.path?.type === "GlimmerPathExpression" &&
|
|
31
|
+
value.path.head?.type === "VarHead" &&
|
|
32
|
+
value.path.head.name === "hash" &&
|
|
33
|
+
!value.path.tail?.length
|
|
34
|
+
) {
|
|
35
|
+
context.report({
|
|
36
|
+
node: value,
|
|
37
|
+
messageId: "useLazyHash",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
const THIS_BINDING_TYPES = new Set([
|
|
2
|
+
"ClassDeclaration",
|
|
3
|
+
"ClassExpression",
|
|
4
|
+
"FunctionDeclaration",
|
|
5
|
+
"FunctionExpression",
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
function findThisBindingAncestor(target) {
|
|
9
|
+
let current = target.parent;
|
|
10
|
+
while (current) {
|
|
11
|
+
if (THIS_BINDING_TYPES.has(current.type)) {
|
|
12
|
+
return current;
|
|
13
|
+
}
|
|
14
|
+
current = current.parent;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
1
19
|
export default {
|
|
2
20
|
meta: {
|
|
3
21
|
type: "suggestion",
|
|
@@ -24,16 +42,15 @@ export default {
|
|
|
24
42
|
(ref) => ref.identifier.parent !== node
|
|
25
43
|
);
|
|
26
44
|
|
|
45
|
+
const declarationThisBinding = findThisBindingAncestor(node);
|
|
46
|
+
|
|
27
47
|
const referencedOnlyInAdjacentTemplateTag = references.every((ref) => {
|
|
28
48
|
if (ref.identifier.type !== "VarHead") {
|
|
29
49
|
return false;
|
|
30
50
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
context.sourceCode.getScope(node).variableScope;
|
|
35
|
-
|
|
36
|
-
return hasSameVariableScope;
|
|
51
|
+
return (
|
|
52
|
+
findThisBindingAncestor(ref.identifier) === declarationThisBinding
|
|
53
|
+
);
|
|
37
54
|
});
|
|
38
55
|
|
|
39
56
|
if (referencedOnlyInAdjacentTemplateTag) {
|
package/eslint.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import BabelParser from "@babel/eslint-parser";
|
|
2
2
|
import js from "@eslint/js";
|
|
3
|
-
import EmberESLintParser from "ember-eslint-parser";
|
|
4
3
|
import DecoratorPosition from "eslint-plugin-decorator-position";
|
|
5
4
|
import EmberPlugin from "eslint-plugin-ember";
|
|
6
5
|
import EmberRecommended from "eslint-plugin-ember/configs/recommended";
|
|
6
|
+
import EmberTemplateLintMigration from "eslint-plugin-ember/configs/template-lint-migration";
|
|
7
7
|
import ImportPlugin from "eslint-plugin-import";
|
|
8
8
|
import QUnitPlugin from "eslint-plugin-qunit";
|
|
9
9
|
import QUnitRecommended from "eslint-plugin-qunit/configs/recommended";
|
|
@@ -23,15 +23,18 @@ 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 noAtClass from "./eslint-rules/no-at-class.mjs";
|
|
26
27
|
import noComputedMacros from "./eslint-rules/no-computed-macros.mjs";
|
|
27
28
|
import noCurlyComponents from "./eslint-rules/no-curly-components.mjs";
|
|
28
29
|
import noDiscourseComputed from "./eslint-rules/no-discourse-computed.mjs";
|
|
29
30
|
import noOnclick from "./eslint-rules/no-onclick.mjs";
|
|
30
31
|
import noRouteTemplate from "./eslint-rules/no-route-template.mjs";
|
|
31
32
|
import noSimpleQuerySelector from "./eslint-rules/no-simple-query-selector.mjs";
|
|
33
|
+
import noTemplateLintDirectives from "./eslint-rules/no-template-lint-directives.mjs";
|
|
32
34
|
import noUnnecessaryTracked from "./eslint-rules/no-unnecessary-tracked.mjs";
|
|
33
35
|
import noUnusedServices from "./eslint-rules/no-unused-services.mjs";
|
|
34
36
|
import pluginApiNoVersion from "./eslint-rules/plugin-api-no-version.mjs";
|
|
37
|
+
import pluginOutletLazyHash from "./eslint-rules/plugin-outlet-lazy-hash.mjs";
|
|
35
38
|
import serviceInjectImport from "./eslint-rules/service-inject-import.mjs";
|
|
36
39
|
import templateTagNoSelfThis from "./eslint-rules/template-tag-no-self-this.mjs";
|
|
37
40
|
import testFilenameSuffix from "./eslint-rules/test-filename-suffix.mjs";
|
|
@@ -50,14 +53,25 @@ export default [
|
|
|
50
53
|
js.configs.recommended,
|
|
51
54
|
QUnitRecommended,
|
|
52
55
|
...EmberRecommended,
|
|
56
|
+
...EmberTemplateLintMigration,
|
|
53
57
|
{
|
|
54
58
|
ignores: ["assets/vendor/**/*", "public/**/*"],
|
|
55
59
|
},
|
|
60
|
+
{
|
|
61
|
+
// Scope the Babel parser to non-template JS so it doesn't clobber the
|
|
62
|
+
// ember-eslint-parser that the upstream `base` config sets up for
|
|
63
|
+
// .gjs/.gts files. The parserOptions live in the main block below so
|
|
64
|
+
// they also flow through to ember-eslint-parser, which delegates the JS
|
|
65
|
+
// portion of gjs/gts to @babel/eslint-parser internally.
|
|
66
|
+
files: ["**/*.{js,mjs,cjs}"],
|
|
67
|
+
languageOptions: {
|
|
68
|
+
parser: BabelParser,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
56
71
|
{
|
|
57
72
|
languageOptions: {
|
|
58
73
|
ecmaVersion: 2022,
|
|
59
74
|
sourceType: "module",
|
|
60
|
-
parser: BabelParser,
|
|
61
75
|
parserOptions: {
|
|
62
76
|
useBabel: true,
|
|
63
77
|
requireConfigFile: false,
|
|
@@ -66,7 +80,6 @@ export default [
|
|
|
66
80
|
plugins: [[decoratorsPluginPath, { legacy: true }]],
|
|
67
81
|
},
|
|
68
82
|
},
|
|
69
|
-
|
|
70
83
|
globals: {
|
|
71
84
|
...globals.browser,
|
|
72
85
|
...globals.node,
|
|
@@ -155,6 +168,9 @@ export default [
|
|
|
155
168
|
"migrate-tracked-built-ins-to-ember-collections":
|
|
156
169
|
migrateTrackedBuiltInsToEmberCollections,
|
|
157
170
|
"ui-kit-imports": uiKitImports,
|
|
171
|
+
"no-at-class": noAtClass,
|
|
172
|
+
"plugin-outlet-lazy-hash": pluginOutletLazyHash,
|
|
173
|
+
"no-template-lint-directives": noTemplateLintDirectives,
|
|
158
174
|
},
|
|
159
175
|
},
|
|
160
176
|
},
|
|
@@ -223,6 +239,38 @@ export default [
|
|
|
223
239
|
"ember/no-unnecessary-service-injection-argument": "error",
|
|
224
240
|
"ember/no-replace-test-comments": "error",
|
|
225
241
|
"ember/route-path-style": "error",
|
|
242
|
+
|
|
243
|
+
// Intentionally disabled template rules
|
|
244
|
+
"ember/template-no-autofocus-attribute": "off",
|
|
245
|
+
"ember/template-no-positive-tabindex": "off",
|
|
246
|
+
"ember/template-require-mandatory-role-attributes": "off",
|
|
247
|
+
"ember/template-require-media-caption": "off",
|
|
248
|
+
"ember/template-builtin-component-arguments": "off",
|
|
249
|
+
"ember/template-no-builtin-form-components": "off",
|
|
250
|
+
"ember/template-no-unknown-arguments-for-builtin-components": "off",
|
|
251
|
+
|
|
252
|
+
// Pending default template rules
|
|
253
|
+
"ember/template-link-href-attributes": "off",
|
|
254
|
+
"ember/template-no-at-ember-render-modifiers": "off",
|
|
255
|
+
"ember/template-no-curly-component-invocation": "off",
|
|
256
|
+
"ember/template-no-duplicate-landmark-elements": "off",
|
|
257
|
+
"ember/template-no-inline-styles": "off",
|
|
258
|
+
"ember/template-no-link-to-tagname": "off",
|
|
259
|
+
"ember/template-no-passed-in-event-handlers": "off",
|
|
260
|
+
"ember/template-no-route-action": "off",
|
|
261
|
+
"ember/template-require-input-label": "off",
|
|
262
|
+
"ember/template-require-presentational-children": "off",
|
|
263
|
+
"ember/template-require-valid-alt-text": "off",
|
|
264
|
+
|
|
265
|
+
// Non-default rules
|
|
266
|
+
"ember/template-no-chained-this": "error",
|
|
267
|
+
"ember/template-require-strict-mode": "error",
|
|
268
|
+
|
|
269
|
+
// From `ember-template-lint`'s old `stylistic` preset
|
|
270
|
+
"ember/template-linebreak-style": "error",
|
|
271
|
+
"ember/template-no-trailing-spaces": "error",
|
|
272
|
+
"ember/template-no-unnecessary-concat": "error",
|
|
273
|
+
|
|
226
274
|
"qunit/no-loose-assertions": "error",
|
|
227
275
|
"qunit/no-identical-names": "off", // the rule doesn't consider that tests might be in different `acceptance` modules
|
|
228
276
|
"sort-class-members/sort-class-members": [
|
|
@@ -323,12 +371,9 @@ export default [
|
|
|
323
371
|
"discourse/no-unnecessary-tracked": ["warn"],
|
|
324
372
|
"discourse/migrate-tracked-built-ins-to-ember-collections": ["error"],
|
|
325
373
|
"discourse/ui-kit-imports": ["error"],
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
files: ["**/*.gjs", "**/*.gts"],
|
|
330
|
-
languageOptions: {
|
|
331
|
-
parser: EmberESLintParser,
|
|
374
|
+
"discourse/no-at-class": ["error"],
|
|
375
|
+
"discourse/plugin-outlet-lazy-hash": ["error"],
|
|
376
|
+
"discourse/no-template-lint-directives": ["error"],
|
|
332
377
|
},
|
|
333
378
|
},
|
|
334
379
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@discourse/lint-configs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Shareable lint configs for Discourse core, plugins, and themes",
|
|
5
5
|
"author": "Discourse",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,22 +22,16 @@
|
|
|
22
22
|
"./prettier": {
|
|
23
23
|
"require": "./.prettierrc.cjs"
|
|
24
24
|
},
|
|
25
|
-
"./stylelint": "./stylelint.mjs"
|
|
26
|
-
"./template-lint": {
|
|
27
|
-
"require": "./template-lint.config.cjs"
|
|
28
|
-
},
|
|
29
|
-
"./template-lint-rules": "./template-lint-rules/index.mjs"
|
|
25
|
+
"./stylelint": "./stylelint.mjs"
|
|
30
26
|
},
|
|
31
27
|
"dependencies": {
|
|
32
28
|
"@babel/core": "^7.29.0",
|
|
33
29
|
"@babel/eslint-parser": "^7.28.6",
|
|
34
30
|
"@babel/plugin-proposal-decorators": "^7.29.0",
|
|
35
31
|
"@eslint/js": "^9.39.2",
|
|
36
|
-
"ember-eslint-parser": "^0.5.13",
|
|
37
|
-
"ember-template-lint": "^7.9.3",
|
|
38
32
|
"eslint": "^9.39.2",
|
|
39
33
|
"eslint-plugin-decorator-position": "^6.0.0",
|
|
40
|
-
"eslint-plugin-ember": "^
|
|
34
|
+
"eslint-plugin-ember": "^13.2.1",
|
|
41
35
|
"eslint-plugin-import": "^2.32.0",
|
|
42
36
|
"eslint-plugin-qunit": "^8.2.6",
|
|
43
37
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
@@ -52,7 +46,6 @@
|
|
|
52
46
|
"typescript": "^5.9.3"
|
|
53
47
|
},
|
|
54
48
|
"peerDependencies": {
|
|
55
|
-
"ember-template-lint": "7.9.3",
|
|
56
49
|
"eslint": "9.39.2",
|
|
57
50
|
"prettier": "3.8.1",
|
|
58
51
|
"stylelint": "17.5.0"
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import NoAtClass from "./no-at-class.mjs";
|
|
2
|
-
import NoImplicitThis from "./no-implicit-this.mjs";
|
|
3
|
-
import PluginOutletLazyHash from "./plugin-outlet-lazy-hash.mjs";
|
|
4
|
-
|
|
5
|
-
export default {
|
|
6
|
-
// Name of plugin
|
|
7
|
-
name: "discourse",
|
|
8
|
-
|
|
9
|
-
// Define rules for this plugin. Each path should map to a plugin rule
|
|
10
|
-
rules: {
|
|
11
|
-
"discourse/no-at-class": NoAtClass,
|
|
12
|
-
"discourse/no-implicit-this": NoImplicitThis,
|
|
13
|
-
"discourse/plugin-outlet-lazy-hash": PluginOutletLazyHash,
|
|
14
|
-
},
|
|
15
|
-
};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { Rule } from "ember-template-lint";
|
|
2
|
-
|
|
3
|
-
const AFFECTED_COMPONENTS = [
|
|
4
|
-
"DButton",
|
|
5
|
-
"DModal",
|
|
6
|
-
"TableHeaderToggle",
|
|
7
|
-
"Textarea",
|
|
8
|
-
"TextArea",
|
|
9
|
-
];
|
|
10
|
-
|
|
11
|
-
export default class NoAtClass extends Rule {
|
|
12
|
-
visitor() {
|
|
13
|
-
return {
|
|
14
|
-
ElementNode(node) {
|
|
15
|
-
if (AFFECTED_COMPONENTS.includes(node.tag) && node.attributes) {
|
|
16
|
-
node.attributes.forEach((attribute) => {
|
|
17
|
-
if (attribute.name === "@class") {
|
|
18
|
-
if (this.mode === "fix") {
|
|
19
|
-
attribute.name = "class";
|
|
20
|
-
} else {
|
|
21
|
-
this.log({
|
|
22
|
-
message: `Use 'class' instead of '@class' for ${node.tag}.`,
|
|
23
|
-
node,
|
|
24
|
-
isFixable: true,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
}
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
// Adapted from https://github.com/ember-template-lint/ember-template-lint/blob/master/docs/rule/no-implicit-this.md
|
|
2
|
-
// With the addition of autofix
|
|
3
|
-
|
|
4
|
-
import { Rule } from "ember-template-lint";
|
|
5
|
-
|
|
6
|
-
function createErrorMessage(ruleName, lines, config) {
|
|
7
|
-
return [
|
|
8
|
-
`The ${ruleName} rule accepts one of the following values.`,
|
|
9
|
-
lines,
|
|
10
|
-
`You specified \`${JSON.stringify(config)}\``,
|
|
11
|
-
].join("\n");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function message(original) {
|
|
15
|
-
return (
|
|
16
|
-
`Ambiguous path '${original}' is not allowed. ` +
|
|
17
|
-
`Use '@${original}' if it is a named argument ` +
|
|
18
|
-
`or 'this.${original}' if it is a property on 'this'. ` +
|
|
19
|
-
"If it is a helper or component that has no arguments, " +
|
|
20
|
-
"you must either convert it to an angle bracket invocation " +
|
|
21
|
-
"or manually add it to the 'no-implicit-this' rule configuration, e.g. " +
|
|
22
|
-
`'no-implicit-this': { allow: ['${original}'] }.`
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function isString(value) {
|
|
27
|
-
return typeof value === "string";
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function isRegExp(value) {
|
|
31
|
-
return value instanceof RegExp;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function allowedFormat(value) {
|
|
35
|
-
return isString(value) || isRegExp(value);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Allow Ember's builtin argless syntaxes
|
|
39
|
-
export const ARGLESS_BUILTIN_HELPERS = [
|
|
40
|
-
"array",
|
|
41
|
-
"concat",
|
|
42
|
-
"debugger",
|
|
43
|
-
"has-block",
|
|
44
|
-
"hasBlock",
|
|
45
|
-
"has-block-params",
|
|
46
|
-
"hasBlockParams",
|
|
47
|
-
"hash",
|
|
48
|
-
"input",
|
|
49
|
-
"log",
|
|
50
|
-
"outlet",
|
|
51
|
-
"query-params",
|
|
52
|
-
"textarea",
|
|
53
|
-
"yield",
|
|
54
|
-
"unique-id",
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
// arg'less Components / Helpers in default ember-cli blueprint
|
|
58
|
-
const ARGLESS_DEFAULT_BLUEPRINT = [
|
|
59
|
-
"welcome-page",
|
|
60
|
-
/* from app/index.html and tests/index.html */
|
|
61
|
-
"rootURL",
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
export default class NoImplicitThis extends Rule {
|
|
65
|
-
parseConfig(config) {
|
|
66
|
-
if (config === false || config === undefined || this.isStrictMode) {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
switch (typeof config) {
|
|
71
|
-
case "undefined": {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
case "boolean": {
|
|
76
|
-
if (config) {
|
|
77
|
-
return {
|
|
78
|
-
allow: [...ARGLESS_BUILTIN_HELPERS, ...ARGLESS_DEFAULT_BLUEPRINT],
|
|
79
|
-
};
|
|
80
|
-
} else {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
case "object": {
|
|
86
|
-
if (Array.isArray(config.allow) && config.allow.every(allowedFormat)) {
|
|
87
|
-
return {
|
|
88
|
-
allow: [
|
|
89
|
-
...ARGLESS_BUILTIN_HELPERS,
|
|
90
|
-
...ARGLESS_DEFAULT_BLUEPRINT,
|
|
91
|
-
...config.allow,
|
|
92
|
-
],
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
let errorMessage = createErrorMessage(
|
|
100
|
-
this.ruleName,
|
|
101
|
-
[
|
|
102
|
-
" * boolean - `true` to enable / `false` to disable",
|
|
103
|
-
" * object -- An object with the following keys:",
|
|
104
|
-
" * `allow` -- An array of component / helper names for that may be called without arguments",
|
|
105
|
-
],
|
|
106
|
-
config
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
throw new Error(errorMessage);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// The way this visitor works is a bit sketchy. We need to lint the PathExpressions
|
|
113
|
-
// in the callee position differently those in an argument position.
|
|
114
|
-
//
|
|
115
|
-
// Unfortunately, the current visitor API doesn't give us a good way to differentiate
|
|
116
|
-
// these two cases. Instead, we rely on the fact that the _first_ PathExpression that
|
|
117
|
-
// we enter after entering a MustacheStatement/BlockStatement/... will be the callee
|
|
118
|
-
// and we track this using a flag called `nextPathIsCallee`.
|
|
119
|
-
visitor() {
|
|
120
|
-
let nextPathIsCallee = false;
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
PathExpression(path) {
|
|
124
|
-
if (nextPathIsCallee) {
|
|
125
|
-
// All paths are valid callees so there's nothing to check.
|
|
126
|
-
} else {
|
|
127
|
-
let valid =
|
|
128
|
-
path.data ||
|
|
129
|
-
path.this ||
|
|
130
|
-
this.isLocal(path) ||
|
|
131
|
-
this.config.allow.some((item) => {
|
|
132
|
-
return isRegExp(item)
|
|
133
|
-
? item.test(path.original)
|
|
134
|
-
: item === path.original;
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
if (!valid) {
|
|
138
|
-
if (this.mode === "fix") {
|
|
139
|
-
path.original = `this.${path.original}`;
|
|
140
|
-
} else {
|
|
141
|
-
this.log({
|
|
142
|
-
message: message(path.original),
|
|
143
|
-
node: path,
|
|
144
|
-
isFixable: true,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
nextPathIsCallee = false;
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
SubExpression() {
|
|
154
|
-
nextPathIsCallee = true;
|
|
155
|
-
},
|
|
156
|
-
|
|
157
|
-
ElementModifierStatement() {
|
|
158
|
-
nextPathIsCallee = true;
|
|
159
|
-
},
|
|
160
|
-
|
|
161
|
-
MustacheStatement(node) {
|
|
162
|
-
let isCall = node.params.length > 0 || node.hash.pairs.length > 0;
|
|
163
|
-
|
|
164
|
-
nextPathIsCallee = isCall;
|
|
165
|
-
},
|
|
166
|
-
|
|
167
|
-
BlockStatement: {
|
|
168
|
-
enter() {
|
|
169
|
-
nextPathIsCallee = true;
|
|
170
|
-
},
|
|
171
|
-
},
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { Rule } from "ember-template-lint";
|
|
2
|
-
|
|
3
|
-
export default class PluginOutletLazyHash extends Rule {
|
|
4
|
-
visitor() {
|
|
5
|
-
return {
|
|
6
|
-
ElementNode(node) {
|
|
7
|
-
if (node.tag === "PluginOutlet") {
|
|
8
|
-
const outletArgsAttr = node.attributes.find(
|
|
9
|
-
(attr) => attr.name === "@outletArgs"
|
|
10
|
-
);
|
|
11
|
-
|
|
12
|
-
if (
|
|
13
|
-
outletArgsAttr &&
|
|
14
|
-
outletArgsAttr.value.type === "MustacheStatement" &&
|
|
15
|
-
outletArgsAttr.value.path.original === "hash"
|
|
16
|
-
) {
|
|
17
|
-
this.log({
|
|
18
|
-
message:
|
|
19
|
-
"Use {{lazyHash}} instead of {{hash}} for @outletArgs in <PluginOutlet>.",
|
|
20
|
-
node: outletArgsAttr.value,
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
}
|
package/template-lint.config.cjs
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
extends: ["recommended", "stylistic"],
|
|
3
|
-
plugins: ["@discourse/lint-configs/template-lint-rules"],
|
|
4
|
-
ignore: ["**/*.js"],
|
|
5
|
-
rules: {
|
|
6
|
-
// Intentionally disabled default rules
|
|
7
|
-
"no-autofocus-attribute": false,
|
|
8
|
-
"no-positive-tabindex": false,
|
|
9
|
-
"require-mandatory-role-attributes": false,
|
|
10
|
-
"require-media-caption": false,
|
|
11
|
-
|
|
12
|
-
// Pending default rules
|
|
13
|
-
"link-href-attributes": false,
|
|
14
|
-
"no-at-ember-render-modifiers": false,
|
|
15
|
-
"no-curly-component-invocation": false,
|
|
16
|
-
"no-duplicate-landmark-elements": false,
|
|
17
|
-
"no-implicit-this": false,
|
|
18
|
-
"no-inline-styles": false,
|
|
19
|
-
"no-link-to-tagname": false,
|
|
20
|
-
"no-passed-in-event-handlers": false,
|
|
21
|
-
"no-route-action": false,
|
|
22
|
-
"require-input-label": false,
|
|
23
|
-
"require-presentational-children": false,
|
|
24
|
-
"require-valid-alt-text": false,
|
|
25
|
-
|
|
26
|
-
// Non-default rules
|
|
27
|
-
"no-unnecessary-curly-parens": true,
|
|
28
|
-
"no-unnecessary-curly-strings": true,
|
|
29
|
-
"simple-modifiers": true,
|
|
30
|
-
"no-chained-this": true,
|
|
31
|
-
"require-strict-mode": true,
|
|
32
|
-
|
|
33
|
-
// Pending non-default rules
|
|
34
|
-
"attribute-order": false,
|
|
35
|
-
"inline-link-to": false,
|
|
36
|
-
"no-builtin-form-components": false,
|
|
37
|
-
"no-this-in-template-only-components": false, // emits false-positives in gjs
|
|
38
|
-
|
|
39
|
-
// GJS compatibility
|
|
40
|
-
"modifier-name-case": false,
|
|
41
|
-
|
|
42
|
-
// Prettier compatibility
|
|
43
|
-
"block-indentation": false,
|
|
44
|
-
"eol-last": false,
|
|
45
|
-
quotes: false,
|
|
46
|
-
"self-closing-void-elements": false,
|
|
47
|
-
|
|
48
|
-
// Discourse custom
|
|
49
|
-
"discourse/no-at-class": true,
|
|
50
|
-
"discourse/no-implicit-this": {
|
|
51
|
-
allow: [
|
|
52
|
-
/-/, // kebab-case, probably a component or helper
|
|
53
|
-
],
|
|
54
|
-
},
|
|
55
|
-
"discourse/plugin-outlet-lazy-hash": true,
|
|
56
|
-
},
|
|
57
|
-
overrides: [
|
|
58
|
-
{
|
|
59
|
-
files: ["**/*.gjs", "**/*.gts"],
|
|
60
|
-
rules: {
|
|
61
|
-
"discourse/no-implicit-this": false,
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
};
|