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

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0-next.15
4
+
5
+ ### Minor Changes
6
+
7
+ - [`6f4b966`](https://github.com/ethlete-io/ethdk/commit/6f4b966c4dc0244b9dfc40978f42362fe9c89a58) Thanks [@TomTomB](https://github.com/TomTomB)! - Expose the `recommendedSpec` flat config (relaxes `no-non-null-assertion` in `*.spec.ts`) as `ethlete.configs.recommendedSpec`.
8
+
9
+ ## 1.0.0-next.14
10
+
11
+ ### Patch Changes
12
+
13
+ - [`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.
14
+
15
+ - [`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.
16
+
3
17
  ## 1.0.0-next.13
4
18
 
5
19
  ### 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.15",
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
  {
package/src/index.js CHANGED
@@ -131,6 +131,8 @@ const configs = {
131
131
  recommendedTs: recommendedTsWithPlugin,
132
132
  /** Angular template rules (merged into a files: ['**\/*.html'] block) */
133
133
  recommendedTemplate: recommendedTemplateWithPlugin,
134
+ /** Relaxed rules for spec files (files: ['**\/*.spec.ts']) */
135
+ recommendedSpec,
134
136
  /**
135
137
  * Both together as a flat array — the most common usage:
136
138
  * export default [...baseConfig, ...ethlete.configs.recommended]
@@ -116,7 +116,6 @@ const buildObjectTextWithAppendedProperty = (sourceCode, objectExpression, entry
116
116
  return existingText.length === 0 ? `{ ${entryText} }` : `{ ${existingText.join(', ')}, ${entryText} }`;
117
117
  }
118
118
 
119
- const openingBrace = sourceCode.getFirstToken(objectExpression);
120
119
  const closingBrace = sourceCode.getLastToken(objectExpression);
121
120
  const closingIndent = closingBrace ? getIndent(sourceCode, closingBrace) : '';
122
121
  const propertyIndent = properties[0] ? getIndent(sourceCode, properties[0]) : `${closingIndent} `;
@@ -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