@ethlete/eslint-plugin 1.0.0-next.13 → 1.0.0-next.14

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0-next.14
4
+
5
+ ### Patch Changes
6
+
7
+ - [`423b30b`](https://github.com/ethlete-io/ethdk/commit/423b30ba4f368d7cee8c464ebd89b5df20d3934c) Thanks [@TomTomB](https://github.com/TomTomB)! - The `static` class member ban now excepts `ngTemplateContextGuard`, which Angular's template type checker requires to be static.
8
+
9
+ - [`7ca77d4`](https://github.com/ethlete-io/ethdk/commit/7ca77d4a9e1dd3abad7237227c62643159719b74) Thanks [@TomTomB](https://github.com/TomTomB)! - `no-trivial-return-type`: self-referencing (recursive) functions keep their return type annotation — TypeScript cannot infer a return type that depends on itself (TS7023), so the fixer no longer strips it there.
10
+
3
11
  ## 1.0.0-next.13
4
12
 
5
13
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethlete/eslint-plugin",
3
- "version": "1.0.0-next.13",
3
+ "version": "1.0.0-next.14",
4
4
  "license": "MIT",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -139,9 +139,10 @@ const recommendedTs = {
139
139
  selector: 'FunctionExpression[async=true]',
140
140
  message: 'No async/await. Use RxJS for all async operations.',
141
141
  },
142
- // No static class members
142
+ // No static class members — except ngTemplateContextGuard, which Angular's template
143
+ // type checker requires to be static (it types a structural directive's `let-` bindings)
143
144
  {
144
- selector: 'MethodDefinition[static=true]',
145
+ selector: "MethodDefinition[static=true]:not([key.name='ngTemplateContextGuard'])",
145
146
  message: 'No static class members.',
146
147
  },
147
148
  {
@@ -11,6 +11,11 @@
11
11
  *
12
12
  * ALSO GOOD (non-trivial, keep it):
13
13
  * const parse = (): Date => new Date(value)
14
+ *
15
+ * Self-referencing (recursive) functions are exempt: TypeScript cannot infer a
16
+ * return type that depends on itself (TS7023 under noImplicitAny), so the
17
+ * annotation is required there:
18
+ * const walk = (node): boolean => node.ok && walk(node.next)
14
19
  */
15
20
 
16
21
  const TRIVIAL_TYPES = new Set([
@@ -69,6 +74,93 @@ const noTrivialReturnType = {
69
74
  return false;
70
75
  };
71
76
 
77
+ /**
78
+ * The names under which a function implementation can call itself: its own name
79
+ * (named function expression, or the variable an arrow is assigned to) and/or
80
+ * `this.<key>` for class/object methods and class-field arrows.
81
+ * @param {any} fn
82
+ */
83
+ const selfReferenceNames = (fn) => {
84
+ /** @type {{ kind: 'name' | 'this', name: string }[]} */
85
+ const names = [];
86
+
87
+ if (fn.id && fn.id.type === 'Identifier') {
88
+ names.push({ kind: 'name', name: fn.id.name });
89
+ }
90
+
91
+ const parent = fn.parent;
92
+
93
+ if (parent) {
94
+ if (
95
+ (parent.type === 'MethodDefinition' || parent.type === 'PropertyDefinition' || parent.type === 'Property') &&
96
+ !parent.computed &&
97
+ parent.key &&
98
+ parent.key.type === 'Identifier'
99
+ ) {
100
+ names.push({ kind: 'this', name: parent.key.name });
101
+ }
102
+
103
+ if (parent.type === 'VariableDeclarator' && parent.id && parent.id.type === 'Identifier') {
104
+ names.push({ kind: 'name', name: parent.id.name });
105
+ }
106
+ }
107
+
108
+ return names;
109
+ };
110
+
111
+ /**
112
+ * Whether the function body references the function itself (recursion) — there the
113
+ * annotation is required, since TypeScript cannot infer a self-dependent return type.
114
+ * @param {any} fn
115
+ */
116
+ const isSelfReferencing = (fn) => {
117
+ const names = selfReferenceNames(fn);
118
+
119
+ if (!names.length || !fn.body) {
120
+ return false;
121
+ }
122
+
123
+ const stack = [fn.body];
124
+
125
+ while (stack.length) {
126
+ const current = stack.pop();
127
+
128
+ if (!current || typeof current !== 'object') continue;
129
+
130
+ if (Array.isArray(current)) {
131
+ stack.push(...current);
132
+ continue;
133
+ }
134
+
135
+ if (typeof current.type !== 'string') continue;
136
+
137
+ for (const { kind, name } of names) {
138
+ if (kind === 'name' && current.type === 'Identifier' && current.name === name) {
139
+ return true;
140
+ }
141
+
142
+ if (
143
+ kind === 'this' &&
144
+ current.type === 'MemberExpression' &&
145
+ current.object.type === 'ThisExpression' &&
146
+ !current.computed &&
147
+ current.property.type === 'Identifier' &&
148
+ current.property.name === name
149
+ ) {
150
+ return true;
151
+ }
152
+ }
153
+
154
+ for (const key of Object.keys(current)) {
155
+ if (key === 'parent') continue;
156
+ const value = current[key];
157
+ if (value && typeof value === 'object') stack.push(value);
158
+ }
159
+ }
160
+
161
+ return false;
162
+ };
163
+
72
164
  /**
73
165
  * @param {import('eslint').Rule.Node} node
74
166
  */
@@ -81,6 +173,7 @@ const noTrivialReturnType = {
81
173
 
82
174
  const annotation = fn.returnType.typeAnnotation;
83
175
  if (!TRIVIAL_TYPES.has(annotation.type)) return;
176
+ if (isSelfReferencing(fn)) return;
84
177
 
85
178
  const label = TRIVIAL_LABELS[annotation.type] ?? annotation.type;
86
179