@discourse/lint-configs 2.1.0 → 2.2.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.
@@ -62,7 +62,7 @@ export function fixImport(
62
62
  if (finalNamedImports.length > 0) {
63
63
  newImportStatement += `{ ${finalNamedImports.join(", ")} }`;
64
64
  }
65
- newImportStatement += ` from '${importDeclarationNode.source.value}';`;
65
+ newImportStatement += ` from "${importDeclarationNode.source.value}";`;
66
66
 
67
67
  // Replace the entire import declaration
68
68
  return fixer.replaceText(importDeclarationNode, newImportStatement);
package/eslint.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import BabelParser from "@babel/eslint-parser";
2
2
  import js from "@eslint/js";
3
+ import stylisticJs from "@stylistic/eslint-plugin-js";
3
4
  import EmberESLintParser from "ember-eslint-parser";
4
5
  import DecoratorPosition from "eslint-plugin-decorator-position";
5
6
  import EmberPlugin from "eslint-plugin-ember";
@@ -85,6 +86,7 @@ export default [
85
86
  },
86
87
  },
87
88
  plugins: {
89
+ "@stylistic/js": stylisticJs,
88
90
  ember: EmberPlugin,
89
91
  "sort-class-members": SortClassMembers,
90
92
  "decorator-position": DecoratorPosition,
@@ -147,6 +149,16 @@ export default [
147
149
  "no-duplicate-imports": "error",
148
150
  "object-shorthand": ["error", "properties"],
149
151
  "no-dupe-class-members": "error",
152
+ "@stylistic/js/lines-between-class-members": [
153
+ "error",
154
+ {
155
+ enforce: [
156
+ { blankLine: "always", prev: "*", next: "method" },
157
+ { blankLine: "always", prev: "method", next: "*" },
158
+ ],
159
+ },
160
+ { exceptAfterSingleLine: true },
161
+ ],
150
162
  "ember/no-classic-components": "off",
151
163
  "ember/no-component-lifecycle-hooks": "off",
152
164
  "ember/require-tagless-components": "off",
@@ -200,6 +212,8 @@ export default [
200
212
  "[properties]",
201
213
  "[private-properties]",
202
214
  "constructor",
215
+ "init",
216
+ "willDestroy",
203
217
  "[everything-else]",
204
218
  "[template-tag]",
205
219
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@discourse/lint-configs",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Shareable lint configs for Discourse core, plugins, and themes",
5
5
  "author": "Discourse",
6
6
  "license": "MIT",
@@ -32,6 +32,7 @@
32
32
  "@babel/core": "^7.25.8",
33
33
  "@babel/eslint-parser": "^7.25.8",
34
34
  "@babel/plugin-proposal-decorators": "^7.25.7",
35
+ "@stylistic/eslint-plugin-js": "^2.11.0",
35
36
  "ember-template-lint": "^6.0.0",
36
37
  "eslint": "^9.14.0",
37
38
  "eslint-plugin-decorator-position": "^6.0.0",
@@ -1,4 +1,5 @@
1
1
  import NoAtClass from "./no-at-class.mjs";
2
+ import NoImplicitThis from "./no-implicit-this.mjs";
2
3
 
3
4
  export default {
4
5
  // Name of plugin
@@ -7,5 +8,6 @@ export default {
7
8
  // Define rules for this plugin. Each path should map to a plugin rule
8
9
  rules: {
9
10
  "discourse/no-at-class": NoAtClass,
11
+ "discourse/no-implicit-this": NoImplicitThis,
10
12
  },
11
13
  };
@@ -0,0 +1,174 @@
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
+ }
@@ -46,5 +46,20 @@ module.exports = {
46
46
 
47
47
  // Discourse custom
48
48
  "discourse/no-at-class": true,
49
+ "discourse/no-implicit-this": {
50
+ allow: [
51
+ "hide-application-footer",
52
+ "hide-application-sidebar",
53
+ "loading-spinner",
54
+ ],
55
+ },
49
56
  },
57
+ overrides: [
58
+ {
59
+ files: ["**/*.gjs", "**/*.gts"],
60
+ rules: {
61
+ "discourse/no-implicit-this": false,
62
+ },
63
+ },
64
+ ],
50
65
  };