@dereekb/dbx-web 13.10.6 → 13.10.8
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/docs/README.md +10 -0
- package/eslint/index.cjs.default.js +1 -0
- package/eslint/index.cjs.js +1096 -0
- package/eslint/index.cjs.mjs +2 -0
- package/eslint/index.d.ts +1 -0
- package/eslint/index.esm.js +1091 -0
- package/eslint/src/index.d.ts +1 -0
- package/eslint/src/lib/index.d.ts +4 -0
- package/eslint/src/lib/no-redundant-on-destroy.rule.d.ts +40 -0
- package/eslint/src/lib/plugin.d.ts +20 -0
- package/eslint/src/lib/require-clean-subscription.rule.d.ts +79 -0
- package/eslint/src/lib/require-complete-on-destroy.rule.d.ts +33 -0
- package/eslint/src/lib/util.d.ts +256 -0
- package/fesm2022/dereekb-dbx-web-calendar.mjs +9 -9
- package/fesm2022/dereekb-dbx-web-docs.mjs +146 -0
- package/fesm2022/dereekb-dbx-web-docs.mjs.map +1 -0
- package/fesm2022/dereekb-dbx-web-mapbox.mjs +58 -64
- package/fesm2022/dereekb-dbx-web-mapbox.mjs.map +1 -1
- package/fesm2022/dereekb-dbx-web-table.mjs +80 -80
- package/fesm2022/dereekb-dbx-web-table.mjs.map +1 -1
- package/fesm2022/dereekb-dbx-web.mjs +1705 -944
- package/fesm2022/dereekb-dbx-web.mjs.map +1 -1
- package/lib/action/snackbar/_snackbar.scss +5 -0
- package/lib/button/_button.scss +27 -0
- package/lib/error/_error.scss +5 -0
- package/lib/extension/pdf/_pdf.scss +19 -59
- package/lib/interaction/dialog/_dialog.scss +5 -0
- package/lib/interaction/popover/_popover.scss +5 -0
- package/lib/interaction/popup/_popup.scss +5 -0
- package/lib/interaction/prompt/_prompt.scss +4 -0
- package/lib/interaction/upload/_upload.scss +15 -2
- package/lib/layout/avatar/_avatar.scss +26 -0
- package/lib/layout/bar/_bar.scss +27 -0
- package/lib/layout/block/_block.scss +4 -0
- package/lib/layout/column/_column.scss +3 -0
- package/lib/layout/content/_content.scss +29 -0
- package/lib/layout/flex/_flex.scss +37 -0
- package/lib/layout/list/_list.scss +99 -0
- package/lib/layout/section/_section.scss +7 -0
- package/lib/layout/style/_style.scss +49 -0
- package/lib/layout/text/_text.scss +298 -14
- package/lib/loading/_loading.scss +6 -0
- package/lib/style/_variables.scss +167 -0
- package/package.json +27 -14
- package/types/dereekb-dbx-web-docs.d.ts +73 -0
- package/types/dereekb-dbx-web-mapbox.d.ts +4 -4
- package/types/dereekb-dbx-web.d.ts +827 -179
|
@@ -0,0 +1,1091 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module that holds Angular component decorators.
|
|
3
|
+
*/ var ANGULAR_CORE_MODULE = '@angular/core';
|
|
4
|
+
/**
|
|
5
|
+
* Decorators that mark a class with a component-scoped DestroyRef lifecycle.
|
|
6
|
+
*
|
|
7
|
+
* `@Injectable` is intentionally excluded — services often expose long-lived
|
|
8
|
+
* Subjects as part of their public API and cleaning them on destroy is wrong.
|
|
9
|
+
*/ var ANGULAR_COMPONENT_DECORATORS = new Set([
|
|
10
|
+
'Component',
|
|
11
|
+
'Directive',
|
|
12
|
+
'Pipe'
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* Module that holds the dbx-components RxJS extras (SubscriptionObject).
|
|
16
|
+
*/ var DEREEKB_RXJS_MODULE = '@dereekb/rxjs';
|
|
17
|
+
/**
|
|
18
|
+
* Module that holds Subject/BehaviorSubject/etc.
|
|
19
|
+
*/ var RXJS_MODULE = 'rxjs';
|
|
20
|
+
/**
|
|
21
|
+
* Module that holds the cleanup helpers (cleanSubscription, completeOnDestroy, clean).
|
|
22
|
+
*/ var DEREEKB_DBX_CORE_MODULE = '@dereekb/dbx-core';
|
|
23
|
+
/**
|
|
24
|
+
* Identifier name for the `SubscriptionObject` class.
|
|
25
|
+
*/ var SUBSCRIPTION_OBJECT_NAME = 'SubscriptionObject';
|
|
26
|
+
/**
|
|
27
|
+
* Identifier names for RxJS Subject classes that should be wrapped with `completeOnDestroy`.
|
|
28
|
+
*/ var SUBJECT_NAMES = new Set([
|
|
29
|
+
'Subject',
|
|
30
|
+
'BehaviorSubject',
|
|
31
|
+
'ReplaySubject',
|
|
32
|
+
'AsyncSubject'
|
|
33
|
+
]);
|
|
34
|
+
/**
|
|
35
|
+
* Helper imported from `@dereekb/dbx-core` that replaces a manual SubscriptionObject creation.
|
|
36
|
+
*/ var CLEAN_SUBSCRIPTION_HELPER = 'cleanSubscription';
|
|
37
|
+
/**
|
|
38
|
+
* Helper imported from `@dereekb/dbx-core` that wraps a Subject so it completes on destroy.
|
|
39
|
+
*/ var COMPLETE_ON_DESTROY_HELPER = 'completeOnDestroy';
|
|
40
|
+
/**
|
|
41
|
+
* Underlying Destroyable/DestroyFunction primitive helper from `@dereekb/dbx-core`.
|
|
42
|
+
*
|
|
43
|
+
* Accepted as a wrapper for `new SubscriptionObject(...)` since `SubscriptionObject`
|
|
44
|
+
* is `Destroyable`. Not accepted for raw Subjects since those are neither
|
|
45
|
+
* `Destroyable` nor `DestroyFunction` and would not actually call `.complete()`.
|
|
46
|
+
*/ var CLEAN_HELPER = 'clean';
|
|
47
|
+
/**
|
|
48
|
+
* Creates an empty {@link ImportRegistry}.
|
|
49
|
+
*
|
|
50
|
+
* @returns A fresh empty registry.
|
|
51
|
+
*/ function createImportRegistry() {
|
|
52
|
+
return {
|
|
53
|
+
bySource: new Map(),
|
|
54
|
+
localToSource: new Map(),
|
|
55
|
+
sourceToDeclaration: new Map(),
|
|
56
|
+
lastImportDeclaration: null
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Records an `ImportDeclaration` node in the registry. Call from the rule's
|
|
61
|
+
* `ImportDeclaration` visitor for every import in the file.
|
|
62
|
+
*
|
|
63
|
+
* @param registry - The registry to mutate.
|
|
64
|
+
* @param node - The ImportDeclaration AST node.
|
|
65
|
+
*/ function trackImportDeclaration(registry, node) {
|
|
66
|
+
var _registry_bySource_get, _node_specifiers;
|
|
67
|
+
var _node_source;
|
|
68
|
+
var source = (_node_source = node.source) === null || _node_source === void 0 ? void 0 : _node_source.value;
|
|
69
|
+
if (!source) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
registry.lastImportDeclaration = node;
|
|
73
|
+
registry.sourceToDeclaration.set(source, node);
|
|
74
|
+
var localNames = (_registry_bySource_get = registry.bySource.get(source)) !== null && _registry_bySource_get !== void 0 ? _registry_bySource_get : new Set();
|
|
75
|
+
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
|
|
76
|
+
try {
|
|
77
|
+
for(var _iterator = ((_node_specifiers = node.specifiers) !== null && _node_specifiers !== void 0 ? _node_specifiers : [])[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
|
|
78
|
+
var specifier = _step.value;
|
|
79
|
+
if (specifier.type === 'ImportSpecifier' || specifier.type === 'ImportDefaultSpecifier' || specifier.type === 'ImportNamespaceSpecifier') {
|
|
80
|
+
var _specifier_local;
|
|
81
|
+
var localName = (_specifier_local = specifier.local) === null || _specifier_local === void 0 ? void 0 : _specifier_local.name;
|
|
82
|
+
if (localName) {
|
|
83
|
+
localNames.add(localName);
|
|
84
|
+
registry.localToSource.set(localName, source);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
_didIteratorError = true;
|
|
90
|
+
_iteratorError = err;
|
|
91
|
+
} finally{
|
|
92
|
+
try {
|
|
93
|
+
if (!_iteratorNormalCompletion && _iterator.return != null) {
|
|
94
|
+
_iterator.return();
|
|
95
|
+
}
|
|
96
|
+
} finally{
|
|
97
|
+
if (_didIteratorError) {
|
|
98
|
+
throw _iteratorError;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
registry.bySource.set(source, localNames);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Returns true when the given local identifier name was imported from the given module.
|
|
106
|
+
*
|
|
107
|
+
* @param registry - The import registry built from the file's import declarations.
|
|
108
|
+
* @param localName - The local identifier (as it appears in code).
|
|
109
|
+
* @param fromSource - The expected source-module string.
|
|
110
|
+
* @returns True when the local name maps to the given source.
|
|
111
|
+
*/ function isImportedFrom(registry, localName, fromSource) {
|
|
112
|
+
return registry.localToSource.get(localName) === fromSource;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Extracts the decorator name from a decorator AST node.
|
|
116
|
+
*
|
|
117
|
+
* Handles `@Foo()` (CallExpression) and `@Foo` (Identifier). Returns the empty
|
|
118
|
+
* string for anything else.
|
|
119
|
+
*
|
|
120
|
+
* @param decorator - The decorator AST node.
|
|
121
|
+
* @returns The decorator name, or empty string when unrecognized.
|
|
122
|
+
*/ function getDecoratorName(decorator) {
|
|
123
|
+
var expression = decorator === null || decorator === void 0 ? void 0 : decorator.expression;
|
|
124
|
+
if (!expression) {
|
|
125
|
+
return '';
|
|
126
|
+
}
|
|
127
|
+
if (expression.type === 'CallExpression') {
|
|
128
|
+
var _expression_callee, _expression_callee1, _expression_callee_property;
|
|
129
|
+
if (((_expression_callee = expression.callee) === null || _expression_callee === void 0 ? void 0 : _expression_callee.type) === 'Identifier') {
|
|
130
|
+
return expression.callee.name;
|
|
131
|
+
}
|
|
132
|
+
if (((_expression_callee1 = expression.callee) === null || _expression_callee1 === void 0 ? void 0 : _expression_callee1.type) === 'MemberExpression' && ((_expression_callee_property = expression.callee.property) === null || _expression_callee_property === void 0 ? void 0 : _expression_callee_property.type) === 'Identifier') {
|
|
133
|
+
return expression.callee.property.name;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (expression.type === 'Identifier') {
|
|
137
|
+
return expression.name;
|
|
138
|
+
}
|
|
139
|
+
return '';
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Returns the first decorator on the class that names a component-tier
|
|
143
|
+
* Angular decorator (`@Component`, `@Directive`, `@Pipe`) imported from
|
|
144
|
+
* `@angular/core`, or null when none match.
|
|
145
|
+
*
|
|
146
|
+
* @param classNode - The ClassDeclaration / ClassExpression AST node.
|
|
147
|
+
* @param registry - The file's import registry, used to verify the decorator
|
|
148
|
+
* identifier really came from `@angular/core` (and not a local alias).
|
|
149
|
+
* @returns The matching decorator and its name, or null.
|
|
150
|
+
*/ function findAngularComponentDecorator(classNode, registry) {
|
|
151
|
+
var decorators = classNode === null || classNode === void 0 ? void 0 : classNode.decorators;
|
|
152
|
+
var result = null;
|
|
153
|
+
if (decorators && decorators.length > 0) {
|
|
154
|
+
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
|
|
155
|
+
try {
|
|
156
|
+
for(var _iterator = decorators[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
|
|
157
|
+
var decorator = _step.value;
|
|
158
|
+
var name = getDecoratorName(decorator);
|
|
159
|
+
if (ANGULAR_COMPONENT_DECORATORS.has(name) && isImportedFrom(registry, name, ANGULAR_CORE_MODULE)) {
|
|
160
|
+
result = {
|
|
161
|
+
decorator: decorator,
|
|
162
|
+
name: name
|
|
163
|
+
};
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch (err) {
|
|
168
|
+
_didIteratorError = true;
|
|
169
|
+
_iteratorError = err;
|
|
170
|
+
} finally{
|
|
171
|
+
try {
|
|
172
|
+
if (!_iteratorNormalCompletion && _iterator.return != null) {
|
|
173
|
+
_iterator.return();
|
|
174
|
+
}
|
|
175
|
+
} finally{
|
|
176
|
+
if (_didIteratorError) {
|
|
177
|
+
throw _iteratorError;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Returns the property name of a class member when its key is a simple Identifier or string Literal.
|
|
186
|
+
*
|
|
187
|
+
* Returns null for computed/symbol/private keys.
|
|
188
|
+
*
|
|
189
|
+
* @param member - A ClassBody member AST node.
|
|
190
|
+
* @returns The property name, or null when not a simple key.
|
|
191
|
+
*/ function getClassMemberName(member) {
|
|
192
|
+
var key = member === null || member === void 0 ? void 0 : member.key;
|
|
193
|
+
if (!key || member.computed) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
if (key.type === 'Identifier') {
|
|
197
|
+
return key.name;
|
|
198
|
+
}
|
|
199
|
+
if (key.type === 'Literal' && typeof key.value === 'string') {
|
|
200
|
+
return key.value;
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Finds the `ngOnDestroy` method declaration on the given class, if any.
|
|
206
|
+
*
|
|
207
|
+
* @param classNode - The ClassDeclaration / ClassExpression AST node.
|
|
208
|
+
* @returns The MethodDefinition AST node for `ngOnDestroy`, or null.
|
|
209
|
+
*/ function findNgOnDestroyMethod(classNode) {
|
|
210
|
+
var _classNode_body;
|
|
211
|
+
var members = classNode === null || classNode === void 0 ? void 0 : (_classNode_body = classNode.body) === null || _classNode_body === void 0 ? void 0 : _classNode_body.body;
|
|
212
|
+
var result = null;
|
|
213
|
+
if (members) {
|
|
214
|
+
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
|
|
215
|
+
try {
|
|
216
|
+
for(var _iterator = members[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
|
|
217
|
+
var member = _step.value;
|
|
218
|
+
if (member.type === 'MethodDefinition' && member.kind === 'method' && getClassMemberName(member) === 'ngOnDestroy') {
|
|
219
|
+
result = member;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} catch (err) {
|
|
224
|
+
_didIteratorError = true;
|
|
225
|
+
_iteratorError = err;
|
|
226
|
+
} finally{
|
|
227
|
+
try {
|
|
228
|
+
if (!_iteratorNormalCompletion && _iterator.return != null) {
|
|
229
|
+
_iterator.return();
|
|
230
|
+
}
|
|
231
|
+
} finally{
|
|
232
|
+
if (_didIteratorError) {
|
|
233
|
+
throw _iteratorError;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* If the given expression is a `CallExpression` whose callee is one of the
|
|
242
|
+
* accepted identifier names, returns the matching name. Otherwise null.
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* ```
|
|
246
|
+
* isCalledIdentifier(node, ['cleanSubscription', 'clean']) // returns 'cleanSubscription'
|
|
247
|
+
* ```
|
|
248
|
+
*
|
|
249
|
+
* @param node - The expression AST node.
|
|
250
|
+
* @param names - The accepted identifier names.
|
|
251
|
+
* @returns The matched name, or null.
|
|
252
|
+
*/ function isCalledIdentifier(node, names) {
|
|
253
|
+
if ((node === null || node === void 0 ? void 0 : node.type) !== 'CallExpression') {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
var callee = node.callee;
|
|
257
|
+
if ((callee === null || callee === void 0 ? void 0 : callee.type) === 'Identifier' && names.has(callee.name)) {
|
|
258
|
+
return callee.name;
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Returns true when the given AST node is a `this.<propName>` MemberExpression.
|
|
264
|
+
*
|
|
265
|
+
* @param node - The AST node to check.
|
|
266
|
+
* @param propName - The expected property name.
|
|
267
|
+
* @returns True when the node is `this.<propName>`.
|
|
268
|
+
*/ function isThisMemberAccess(node, propName) {
|
|
269
|
+
var _node_object, _node_property;
|
|
270
|
+
return (node === null || node === void 0 ? void 0 : node.type) === 'MemberExpression' && ((_node_object = node.object) === null || _node_object === void 0 ? void 0 : _node_object.type) === 'ThisExpression' && !node.computed && ((_node_property = node.property) === null || _node_property === void 0 ? void 0 : _node_property.type) === 'Identifier' && node.property.name === propName;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Builds a fix operation that ensures `importName` is imported from
|
|
274
|
+
* `fromSource` in the file. Returns null when the import is already present.
|
|
275
|
+
*
|
|
276
|
+
* Side effect: mutates the registry to mark the import as present, so two
|
|
277
|
+
* separate report fixes in the same lint pass don't both insert the same import.
|
|
278
|
+
*
|
|
279
|
+
* @param input - The fixer, registry, and import names.
|
|
280
|
+
* @returns A fix operation, or null when the import is already present.
|
|
281
|
+
*/ function ensureNamedImportFix(input) {
|
|
282
|
+
var fixer = input.fixer, registry = input.registry, importName = input.importName, fromSource = input.fromSource;
|
|
283
|
+
var existing = registry.bySource.get(fromSource);
|
|
284
|
+
if (existing === null || existing === void 0 ? void 0 : existing.has(importName)) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
var declaration = registry.sourceToDeclaration.get(fromSource);
|
|
288
|
+
var result = null;
|
|
289
|
+
if (declaration) {
|
|
290
|
+
var _declaration_specifiers;
|
|
291
|
+
var lastSpecifier = (_declaration_specifiers = declaration.specifiers) === null || _declaration_specifiers === void 0 ? void 0 : _declaration_specifiers[declaration.specifiers.length - 1];
|
|
292
|
+
if (lastSpecifier) {
|
|
293
|
+
var updatedSet = existing !== null && existing !== void 0 ? existing : new Set();
|
|
294
|
+
updatedSet.add(importName);
|
|
295
|
+
registry.bySource.set(fromSource, updatedSet);
|
|
296
|
+
registry.localToSource.set(importName, fromSource);
|
|
297
|
+
result = fixer.insertTextAfter(lastSpecifier, ", ".concat(importName));
|
|
298
|
+
}
|
|
299
|
+
} else if (registry.lastImportDeclaration) {
|
|
300
|
+
var updatedSet1 = existing !== null && existing !== void 0 ? existing : new Set();
|
|
301
|
+
updatedSet1.add(importName);
|
|
302
|
+
registry.bySource.set(fromSource, updatedSet1);
|
|
303
|
+
registry.localToSource.set(importName, fromSource);
|
|
304
|
+
result = fixer.insertTextAfter(registry.lastImportDeclaration, "\nimport { ".concat(importName, " } from '").concat(fromSource, "';"));
|
|
305
|
+
}
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Returns true when the given PropertyDefinition declares a `static` member.
|
|
310
|
+
*
|
|
311
|
+
* @param node - The PropertyDefinition AST node.
|
|
312
|
+
* @returns True when the node has `static` modifier.
|
|
313
|
+
*/ function isStaticProperty(node) {
|
|
314
|
+
return (node === null || node === void 0 ? void 0 : node.static) === true;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Returns true when the given PropertyDefinition uses `declare`
|
|
318
|
+
* (`declare readonly foo: T`) — i.e. has no runtime initializer.
|
|
319
|
+
*
|
|
320
|
+
* @param node - The PropertyDefinition AST node.
|
|
321
|
+
* @returns True when the property is declared abstractly.
|
|
322
|
+
*/ function isDeclareProperty(node) {
|
|
323
|
+
return (node === null || node === void 0 ? void 0 : node.declare) === true;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The Angular `OnDestroy` lifecycle interface name.
|
|
327
|
+
*/ var ON_DESTROY_INTERFACE_NAME = 'OnDestroy';
|
|
328
|
+
/**
|
|
329
|
+
* Locates an `implements OnDestroy` clause on the class whose `OnDestroy`
|
|
330
|
+
* identifier resolves to the import from `@angular/core`. Returns null when
|
|
331
|
+
* no matching clause exists.
|
|
332
|
+
*
|
|
333
|
+
* @param classNode - The ClassDeclaration / ClassExpression AST node.
|
|
334
|
+
* @param registry - The file's import registry.
|
|
335
|
+
* @returns The match details, or null.
|
|
336
|
+
*/ function findOnDestroyImplementsClause(classNode, registry) {
|
|
337
|
+
var _ref;
|
|
338
|
+
var allImplements = (_ref = classNode === null || classNode === void 0 ? void 0 : classNode.implements) !== null && _ref !== void 0 ? _ref : [];
|
|
339
|
+
var result = null;
|
|
340
|
+
for(var index = 0; index < allImplements.length; index += 1){
|
|
341
|
+
var clauseSpecifier = allImplements[index];
|
|
342
|
+
var expression = clauseSpecifier === null || clauseSpecifier === void 0 ? void 0 : clauseSpecifier.expression;
|
|
343
|
+
if ((expression === null || expression === void 0 ? void 0 : expression.type) === 'Identifier' && expression.name === ON_DESTROY_INTERFACE_NAME && isImportedFrom(registry, ON_DESTROY_INTERFACE_NAME, ANGULAR_CORE_MODULE)) {
|
|
344
|
+
result = {
|
|
345
|
+
allImplements: allImplements,
|
|
346
|
+
clauseSpecifier: clauseSpecifier,
|
|
347
|
+
index: index
|
|
348
|
+
};
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return result;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Computes the source range to remove for the given `implements` specifier so
|
|
356
|
+
* that the surrounding `implements` clause stays well-formed.
|
|
357
|
+
*
|
|
358
|
+
* Behavior:
|
|
359
|
+
* - When the specifier is the only entry, the entire `implements <X>` clause
|
|
360
|
+
* is removed, including the leading whitespace before the `implements`
|
|
361
|
+
* keyword (so `class Foo implements OnDestroy {` becomes `class Foo {`).
|
|
362
|
+
* - When the specifier is the first of several, the specifier and the
|
|
363
|
+
* following comma+whitespace are removed.
|
|
364
|
+
* - Otherwise, the preceding comma+whitespace and the specifier are removed.
|
|
365
|
+
*
|
|
366
|
+
* @param match - The `implements OnDestroy` match details.
|
|
367
|
+
* @param sourceCode - The ESLint sourceCode service.
|
|
368
|
+
* @returns A `[start, end]` range tuple suitable for `fixer.removeRange`.
|
|
369
|
+
*/ function getImplementsSpecifierRemovalRange(match, sourceCode) {
|
|
370
|
+
var allImplements = match.allImplements, clauseSpecifier = match.clauseSpecifier, index = match.index;
|
|
371
|
+
var result;
|
|
372
|
+
if (allImplements.length === 1) {
|
|
373
|
+
var implementsKeyword = sourceCode.getTokenBefore(clauseSpecifier, {
|
|
374
|
+
filter: function filter(token) {
|
|
375
|
+
return token.type === 'Keyword' && token.value === 'implements';
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
var tokenBeforeImplements = implementsKeyword ? sourceCode.getTokenBefore(implementsKeyword) : null;
|
|
379
|
+
var startPos = tokenBeforeImplements ? tokenBeforeImplements.range[1] : implementsKeyword ? implementsKeyword.range[0] : clauseSpecifier.range[0];
|
|
380
|
+
result = [
|
|
381
|
+
startPos,
|
|
382
|
+
clauseSpecifier.range[1]
|
|
383
|
+
];
|
|
384
|
+
} else if (index === 0) {
|
|
385
|
+
result = [
|
|
386
|
+
clauseSpecifier.range[0],
|
|
387
|
+
allImplements[1].range[0]
|
|
388
|
+
];
|
|
389
|
+
} else {
|
|
390
|
+
result = [
|
|
391
|
+
allImplements[index - 1].range[1],
|
|
392
|
+
clauseSpecifier.range[1]
|
|
393
|
+
];
|
|
394
|
+
}
|
|
395
|
+
return result;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Identifier names accepted as the wrapper around a manual `new SubscriptionObject(...)`.
|
|
400
|
+
*/ var ACCEPTED_WRAPPERS$1 = new Set([
|
|
401
|
+
CLEAN_SUBSCRIPTION_HELPER,
|
|
402
|
+
CLEAN_HELPER
|
|
403
|
+
]);
|
|
404
|
+
/**
|
|
405
|
+
* ESLint rule that requires class-field initializers of `new SubscriptionObject(...)`
|
|
406
|
+
* to be replaced with `cleanSubscription(...)` (which auto-registers cleanup with
|
|
407
|
+
* Angular's DestroyRef) on `@Component` / `@Directive` / `@Pipe` classes.
|
|
408
|
+
*
|
|
409
|
+
* Fires only when `SubscriptionObject` is imported from `@dereekb/rxjs`.
|
|
410
|
+
*
|
|
411
|
+
* Auto-fix:
|
|
412
|
+
* - Rewrites the initializer to `cleanSubscription(...)` (preserving any constructor argument).
|
|
413
|
+
* - Inserts the `cleanSubscription` named import from `@dereekb/dbx-core` if missing.
|
|
414
|
+
* - Removes any matching `this.<field>.destroy();` line from the same class's `ngOnDestroy`.
|
|
415
|
+
*/ var dbxWebRequireCleanSubscriptionRule = {
|
|
416
|
+
meta: {
|
|
417
|
+
type: 'problem',
|
|
418
|
+
fixable: 'code',
|
|
419
|
+
docs: {
|
|
420
|
+
description: 'Require cleanSubscription() instead of new SubscriptionObject() in Angular component, directive, or pipe classes',
|
|
421
|
+
recommended: true
|
|
422
|
+
},
|
|
423
|
+
messages: {
|
|
424
|
+
missingCleanSubscription: 'Replace `new SubscriptionObject(...)` with `cleanSubscription(...)` from @dereekb/dbx-core. cleanSubscription registers cleanup with Angular DestroyRef automatically, removing the need for manual destroy() in ngOnDestroy.'
|
|
425
|
+
},
|
|
426
|
+
schema: []
|
|
427
|
+
},
|
|
428
|
+
create: function create(context) {
|
|
429
|
+
var registry = createImportRegistry();
|
|
430
|
+
var sourceCode = context.sourceCode;
|
|
431
|
+
var visitClass = function visitClass(classNode) {
|
|
432
|
+
var _ref;
|
|
433
|
+
var _classNode_body;
|
|
434
|
+
var matchedDecorator = findAngularComponentDecorator(classNode, registry);
|
|
435
|
+
if (!matchedDecorator) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
var members = (_ref = (_classNode_body = classNode.body) === null || _classNode_body === void 0 ? void 0 : _classNode_body.body) !== null && _ref !== void 0 ? _ref : [];
|
|
439
|
+
var ngOnDestroy = findNgOnDestroyMethod(classNode);
|
|
440
|
+
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
|
|
441
|
+
try {
|
|
442
|
+
var _loop = function() {
|
|
443
|
+
var member = _step.value;
|
|
444
|
+
if (member.type !== 'PropertyDefinition' || isStaticProperty(member) || isDeclareProperty(member)) {
|
|
445
|
+
return "continue";
|
|
446
|
+
}
|
|
447
|
+
var propName = getClassMemberName(member);
|
|
448
|
+
var initializer = member.value;
|
|
449
|
+
if (!propName || !initializer) {
|
|
450
|
+
return "continue";
|
|
451
|
+
}
|
|
452
|
+
if (!isUnwrappedSubscriptionObjectNew(initializer, registry)) {
|
|
453
|
+
return "continue";
|
|
454
|
+
}
|
|
455
|
+
context.report({
|
|
456
|
+
node: initializer,
|
|
457
|
+
messageId: 'missingCleanSubscription',
|
|
458
|
+
fix: function fix(fixer) {
|
|
459
|
+
return buildSubscriptionObjectFix({
|
|
460
|
+
fixer: fixer,
|
|
461
|
+
newExpr: initializer,
|
|
462
|
+
propName: propName,
|
|
463
|
+
ngOnDestroy: ngOnDestroy,
|
|
464
|
+
registry: registry,
|
|
465
|
+
sourceCode: sourceCode
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
};
|
|
470
|
+
for(var _iterator = members[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true)_loop();
|
|
471
|
+
} catch (err) {
|
|
472
|
+
_didIteratorError = true;
|
|
473
|
+
_iteratorError = err;
|
|
474
|
+
} finally{
|
|
475
|
+
try {
|
|
476
|
+
if (!_iteratorNormalCompletion && _iterator.return != null) {
|
|
477
|
+
_iterator.return();
|
|
478
|
+
}
|
|
479
|
+
} finally{
|
|
480
|
+
if (_didIteratorError) {
|
|
481
|
+
throw _iteratorError;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
return {
|
|
487
|
+
ImportDeclaration: function ImportDeclaration(node) {
|
|
488
|
+
trackImportDeclaration(registry, node);
|
|
489
|
+
},
|
|
490
|
+
ClassDeclaration: function ClassDeclaration(classNode) {
|
|
491
|
+
visitClass(classNode);
|
|
492
|
+
},
|
|
493
|
+
ClassExpression: function ClassExpression(classNode) {
|
|
494
|
+
visitClass(classNode);
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
/**
|
|
500
|
+
* Returns true when the given initializer is a bare `new SubscriptionObject(...)`
|
|
501
|
+
* expression where `SubscriptionObject` resolves to the import from `@dereekb/rxjs`.
|
|
502
|
+
*
|
|
503
|
+
* Returns false when the expression is wrapped (e.g. `cleanSubscription(...)` or
|
|
504
|
+
* `clean(new SubscriptionObject(...))`).
|
|
505
|
+
*
|
|
506
|
+
* @param expression - The initializer expression AST node.
|
|
507
|
+
* @param registry - The file's import registry.
|
|
508
|
+
* @returns True when the expression is a flagged unwrapped `new SubscriptionObject(...)`.
|
|
509
|
+
*/ function isUnwrappedSubscriptionObjectNew(expression, registry) {
|
|
510
|
+
var result = false;
|
|
511
|
+
if (!isCalledIdentifier(expression, ACCEPTED_WRAPPERS$1) && expression.type === 'NewExpression') {
|
|
512
|
+
var callee = expression.callee;
|
|
513
|
+
if ((callee === null || callee === void 0 ? void 0 : callee.type) === 'Identifier' && callee.name === SUBSCRIPTION_OBJECT_NAME && isImportedFrom(registry, SUBSCRIPTION_OBJECT_NAME, DEREEKB_RXJS_MODULE)) {
|
|
514
|
+
result = true;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return result;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Builds the composite fix for one violating property.
|
|
521
|
+
*
|
|
522
|
+
* @param input - The flagged expression, its property name, the class's ngOnDestroy node, the import registry, and source-code services.
|
|
523
|
+
* @returns A list of fix operations, or null when no fix is producible.
|
|
524
|
+
*/ function buildSubscriptionObjectFix(input) {
|
|
525
|
+
var _newExpr_callee;
|
|
526
|
+
var fixer = input.fixer, newExpr = input.newExpr, propName = input.propName, ngOnDestroy = input.ngOnDestroy, registry = input.registry, sourceCode = input.sourceCode;
|
|
527
|
+
var calleeRange = (_newExpr_callee = newExpr.callee) === null || _newExpr_callee === void 0 ? void 0 : _newExpr_callee.range;
|
|
528
|
+
var fixes = null;
|
|
529
|
+
if (calleeRange) {
|
|
530
|
+
var collected = [];
|
|
531
|
+
collected.push(fixer.replaceTextRange([
|
|
532
|
+
newExpr.range[0],
|
|
533
|
+
calleeRange[1]
|
|
534
|
+
], CLEAN_SUBSCRIPTION_HELPER));
|
|
535
|
+
var importFix = ensureNamedImportFix({
|
|
536
|
+
fixer: fixer,
|
|
537
|
+
registry: registry,
|
|
538
|
+
importName: CLEAN_SUBSCRIPTION_HELPER,
|
|
539
|
+
fromSource: DEREEKB_DBX_CORE_MODULE
|
|
540
|
+
});
|
|
541
|
+
if (importFix) {
|
|
542
|
+
collected.push(importFix);
|
|
543
|
+
}
|
|
544
|
+
if (ANGULAR_COMPONENT_DECORATORS.size > 0 && ngOnDestroy) {
|
|
545
|
+
collectNgOnDestroyRemovalFixes({
|
|
546
|
+
fixer: fixer,
|
|
547
|
+
ngOnDestroy: ngOnDestroy,
|
|
548
|
+
propName: propName,
|
|
549
|
+
methodName: 'destroy',
|
|
550
|
+
sourceCode: sourceCode,
|
|
551
|
+
fixes: collected
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
fixes = collected;
|
|
555
|
+
}
|
|
556
|
+
return fixes;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Pushes fixes that remove `this.<propName>.<methodName>()` ExpressionStatements
|
|
560
|
+
* from `ngOnDestroy`'s body. Removes the statement node and any preceding
|
|
561
|
+
* indentation on the same line so a blank line isn't left behind.
|
|
562
|
+
*
|
|
563
|
+
* @param input - The fixer, ngOnDestroy method, target property/method names, source-code service, and fix collector.
|
|
564
|
+
*/ function collectNgOnDestroyRemovalFixes(input) {
|
|
565
|
+
var _ngOnDestroy_value_body, _ngOnDestroy_value;
|
|
566
|
+
var fixer = input.fixer, ngOnDestroy = input.ngOnDestroy, propName = input.propName, methodName = input.methodName, sourceCode = input.sourceCode, fixes = input.fixes;
|
|
567
|
+
var body = (_ngOnDestroy_value = ngOnDestroy.value) === null || _ngOnDestroy_value === void 0 ? void 0 : (_ngOnDestroy_value_body = _ngOnDestroy_value.body) === null || _ngOnDestroy_value_body === void 0 ? void 0 : _ngOnDestroy_value_body.body;
|
|
568
|
+
if (!body) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
|
|
572
|
+
try {
|
|
573
|
+
for(var _iterator = body[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
|
|
574
|
+
var statement = _step.value;
|
|
575
|
+
var _call_callee, _member_property;
|
|
576
|
+
if (statement.type !== 'ExpressionStatement') {
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
var call = statement.expression;
|
|
580
|
+
if ((call === null || call === void 0 ? void 0 : call.type) !== 'CallExpression' || ((_call_callee = call.callee) === null || _call_callee === void 0 ? void 0 : _call_callee.type) !== 'MemberExpression') {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
var member = call.callee;
|
|
584
|
+
if (member.computed || ((_member_property = member.property) === null || _member_property === void 0 ? void 0 : _member_property.type) !== 'Identifier' || member.property.name !== methodName) {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
if (!isThisMemberAccess(member.object, propName)) {
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
fixes.push(fixer.removeRange(getStatementRangeWithLeadingWhitespace(statement, sourceCode)));
|
|
591
|
+
}
|
|
592
|
+
} catch (err) {
|
|
593
|
+
_didIteratorError = true;
|
|
594
|
+
_iteratorError = err;
|
|
595
|
+
} finally{
|
|
596
|
+
try {
|
|
597
|
+
if (!_iteratorNormalCompletion && _iterator.return != null) {
|
|
598
|
+
_iterator.return();
|
|
599
|
+
}
|
|
600
|
+
} finally{
|
|
601
|
+
if (_didIteratorError) {
|
|
602
|
+
throw _iteratorError;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Returns the range to remove for a statement, expanded to include any
|
|
609
|
+
* leading whitespace on the same line and the trailing newline. This avoids
|
|
610
|
+
* leaving a blank line after fix application.
|
|
611
|
+
*
|
|
612
|
+
* @param statement - The ExpressionStatement AST node.
|
|
613
|
+
* @param sourceCode - The ESLint sourceCode service.
|
|
614
|
+
* @returns A range tuple `[start, end]` to pass to `fixer.removeRange`.
|
|
615
|
+
*/ function getStatementRangeWithLeadingWhitespace(statement, sourceCode) {
|
|
616
|
+
var sourceText = sourceCode.text;
|
|
617
|
+
var start = statement.range[0];
|
|
618
|
+
var end = statement.range[1];
|
|
619
|
+
var lineStart = start;
|
|
620
|
+
while(lineStart > 0 && sourceText[lineStart - 1] !== '\n'){
|
|
621
|
+
lineStart -= 1;
|
|
622
|
+
}
|
|
623
|
+
var lineEnd = end;
|
|
624
|
+
if (lineEnd < sourceText.length && sourceText[lineEnd] === '\n') {
|
|
625
|
+
lineEnd += 1;
|
|
626
|
+
}
|
|
627
|
+
return [
|
|
628
|
+
lineStart,
|
|
629
|
+
lineEnd
|
|
630
|
+
];
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Identifier names accepted as the wrapper around a manual `new <Subject>(...)`.
|
|
635
|
+
*
|
|
636
|
+
* Only `completeOnDestroy` is accepted — `clean()` does not call `.complete()`
|
|
637
|
+
* on a Subject (Subjects are neither `Destroyable` nor `DestroyFunction`).
|
|
638
|
+
*/ var ACCEPTED_WRAPPERS = new Set([
|
|
639
|
+
COMPLETE_ON_DESTROY_HELPER
|
|
640
|
+
]);
|
|
641
|
+
/**
|
|
642
|
+
* ESLint rule that requires class-field initializers of `new Subject(...)`,
|
|
643
|
+
* `new BehaviorSubject(...)`, `new ReplaySubject(...)`, and `new AsyncSubject(...)`
|
|
644
|
+
* to be wrapped with `completeOnDestroy(...)` from `@dereekb/dbx-core` on
|
|
645
|
+
* `@Component` / `@Directive` / `@Pipe` classes.
|
|
646
|
+
*
|
|
647
|
+
* Fires only when the Subject identifier is imported from `rxjs`.
|
|
648
|
+
*
|
|
649
|
+
* Auto-fix:
|
|
650
|
+
* - Wraps the initializer with `completeOnDestroy(...)`.
|
|
651
|
+
* - Inserts the `completeOnDestroy` named import from `@dereekb/dbx-core` if missing.
|
|
652
|
+
* - Removes any matching `this.<field>.complete();` line from the same class's `ngOnDestroy`.
|
|
653
|
+
*/ var dbxWebRequireCompleteOnDestroyRule = {
|
|
654
|
+
meta: {
|
|
655
|
+
type: 'problem',
|
|
656
|
+
fixable: 'code',
|
|
657
|
+
docs: {
|
|
658
|
+
description: 'Require completeOnDestroy() wrapping new Subject/BehaviorSubject/ReplaySubject/AsyncSubject in Angular component, directive, or pipe classes',
|
|
659
|
+
recommended: true
|
|
660
|
+
},
|
|
661
|
+
messages: {
|
|
662
|
+
missingCompleteOnDestroy: 'Wrap `new {{subjectName}}(...)` with `completeOnDestroy(...)` from @dereekb/dbx-core. completeOnDestroy registers cleanup with Angular DestroyRef automatically, removing the need for manual complete() in ngOnDestroy.'
|
|
663
|
+
},
|
|
664
|
+
schema: []
|
|
665
|
+
},
|
|
666
|
+
create: function create(context) {
|
|
667
|
+
var registry = createImportRegistry();
|
|
668
|
+
var sourceCode = context.sourceCode;
|
|
669
|
+
var visitClass = function visitClass(classNode) {
|
|
670
|
+
var _ref;
|
|
671
|
+
var _classNode_body;
|
|
672
|
+
var matchedDecorator = findAngularComponentDecorator(classNode, registry);
|
|
673
|
+
if (!matchedDecorator) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
var members = (_ref = (_classNode_body = classNode.body) === null || _classNode_body === void 0 ? void 0 : _classNode_body.body) !== null && _ref !== void 0 ? _ref : [];
|
|
677
|
+
var ngOnDestroy = findNgOnDestroyMethod(classNode);
|
|
678
|
+
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
|
|
679
|
+
try {
|
|
680
|
+
var _loop = function() {
|
|
681
|
+
var member = _step.value;
|
|
682
|
+
if (member.type !== 'PropertyDefinition' || isStaticProperty(member) || isDeclareProperty(member)) {
|
|
683
|
+
return "continue";
|
|
684
|
+
}
|
|
685
|
+
var propName = getClassMemberName(member);
|
|
686
|
+
var initializer = member.value;
|
|
687
|
+
if (!propName || !initializer) {
|
|
688
|
+
return "continue";
|
|
689
|
+
}
|
|
690
|
+
var subjectName = unwrappedSubjectNewName(initializer, registry);
|
|
691
|
+
if (!subjectName) {
|
|
692
|
+
return "continue";
|
|
693
|
+
}
|
|
694
|
+
context.report({
|
|
695
|
+
node: initializer,
|
|
696
|
+
messageId: 'missingCompleteOnDestroy',
|
|
697
|
+
data: {
|
|
698
|
+
subjectName: subjectName
|
|
699
|
+
},
|
|
700
|
+
fix: function fix(fixer) {
|
|
701
|
+
return buildSubjectFix({
|
|
702
|
+
fixer: fixer,
|
|
703
|
+
newExpr: initializer,
|
|
704
|
+
propName: propName,
|
|
705
|
+
ngOnDestroy: ngOnDestroy,
|
|
706
|
+
registry: registry,
|
|
707
|
+
sourceCode: sourceCode
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
};
|
|
712
|
+
for(var _iterator = members[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true)_loop();
|
|
713
|
+
} catch (err) {
|
|
714
|
+
_didIteratorError = true;
|
|
715
|
+
_iteratorError = err;
|
|
716
|
+
} finally{
|
|
717
|
+
try {
|
|
718
|
+
if (!_iteratorNormalCompletion && _iterator.return != null) {
|
|
719
|
+
_iterator.return();
|
|
720
|
+
}
|
|
721
|
+
} finally{
|
|
722
|
+
if (_didIteratorError) {
|
|
723
|
+
throw _iteratorError;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
return {
|
|
729
|
+
ImportDeclaration: function ImportDeclaration(node) {
|
|
730
|
+
trackImportDeclaration(registry, node);
|
|
731
|
+
},
|
|
732
|
+
ClassDeclaration: function ClassDeclaration(classNode) {
|
|
733
|
+
visitClass(classNode);
|
|
734
|
+
},
|
|
735
|
+
ClassExpression: function ClassExpression(classNode) {
|
|
736
|
+
visitClass(classNode);
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
/**
|
|
742
|
+
* Returns the Subject class name when the given initializer is a bare
|
|
743
|
+
* `new Subject/BehaviorSubject/ReplaySubject/AsyncSubject(...)` whose
|
|
744
|
+
* identifier resolves to the import from `rxjs`. Returns null otherwise
|
|
745
|
+
* (including when the expression is already wrapped).
|
|
746
|
+
*
|
|
747
|
+
* @param expression - The initializer expression AST node.
|
|
748
|
+
* @param registry - The file's import registry.
|
|
749
|
+
* @returns The matched Subject identifier name, or null.
|
|
750
|
+
*/ function unwrappedSubjectNewName(expression, registry) {
|
|
751
|
+
var result = null;
|
|
752
|
+
if (!isCalledIdentifier(expression, ACCEPTED_WRAPPERS) && expression.type === 'NewExpression') {
|
|
753
|
+
var callee = expression.callee;
|
|
754
|
+
if ((callee === null || callee === void 0 ? void 0 : callee.type) === 'Identifier' && SUBJECT_NAMES.has(callee.name) && isImportedFrom(registry, callee.name, RXJS_MODULE)) {
|
|
755
|
+
result = callee.name;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return result;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Builds the composite fix for one violating property.
|
|
762
|
+
*
|
|
763
|
+
* @param input - The flagged expression, its property name, the class's ngOnDestroy node, the import registry, and source-code services.
|
|
764
|
+
* @returns A list of fix operations.
|
|
765
|
+
*/ function buildSubjectFix(input) {
|
|
766
|
+
var fixer = input.fixer, newExpr = input.newExpr, propName = input.propName, ngOnDestroy = input.ngOnDestroy, registry = input.registry, sourceCode = input.sourceCode;
|
|
767
|
+
var fixes = [];
|
|
768
|
+
fixes.push(fixer.insertTextBefore(newExpr, "".concat(COMPLETE_ON_DESTROY_HELPER, "(")));
|
|
769
|
+
fixes.push(fixer.insertTextAfter(newExpr, ')'));
|
|
770
|
+
var importFix = ensureNamedImportFix({
|
|
771
|
+
fixer: fixer,
|
|
772
|
+
registry: registry,
|
|
773
|
+
importName: COMPLETE_ON_DESTROY_HELPER,
|
|
774
|
+
fromSource: DEREEKB_DBX_CORE_MODULE
|
|
775
|
+
});
|
|
776
|
+
if (importFix) {
|
|
777
|
+
fixes.push(importFix);
|
|
778
|
+
}
|
|
779
|
+
if (ngOnDestroy) {
|
|
780
|
+
collectNgOnDestroyRemovalFixes({
|
|
781
|
+
fixer: fixer,
|
|
782
|
+
ngOnDestroy: ngOnDestroy,
|
|
783
|
+
propName: propName,
|
|
784
|
+
methodName: 'complete',
|
|
785
|
+
sourceCode: sourceCode,
|
|
786
|
+
fixes: fixes
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
return fixes;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Identifier names that, when used to wrap a class field initializer, mean the
|
|
794
|
+
* field's cleanup is registered with Angular DestroyRef and a manual
|
|
795
|
+
* `.destroy()` / `.complete()` call in ngOnDestroy is redundant.
|
|
796
|
+
*/ var HELPER_NAMES = new Set([
|
|
797
|
+
CLEAN_SUBSCRIPTION_HELPER,
|
|
798
|
+
COMPLETE_ON_DESTROY_HELPER,
|
|
799
|
+
CLEAN_HELPER
|
|
800
|
+
]);
|
|
801
|
+
/**
|
|
802
|
+
* Method names on a wrapped field whose call inside ngOnDestroy is redundant.
|
|
803
|
+
*/ var REDUNDANT_METHODS = new Set([
|
|
804
|
+
'destroy',
|
|
805
|
+
'complete'
|
|
806
|
+
]);
|
|
807
|
+
/**
|
|
808
|
+
* ESLint rule that flags `ngOnDestroy()` bodies whose statements are entirely
|
|
809
|
+
* redundant `this.<field>.destroy()` / `this.<field>.complete()` calls on
|
|
810
|
+
* fields whose initializer is wrapped with `cleanSubscription`,
|
|
811
|
+
* `completeOnDestroy`, or `clean`.
|
|
812
|
+
*
|
|
813
|
+
* Auto-fix:
|
|
814
|
+
* - Removes each redundant statement, plus its leading whitespace and trailing newline.
|
|
815
|
+
* - Removes the `ngOnDestroy` method declaration when its body becomes empty.
|
|
816
|
+
* - When the `ngOnDestroy` method is removed entirely, also removes the
|
|
817
|
+
* `implements OnDestroy` clause from the class (verified against the
|
|
818
|
+
* `@angular/core` import). The now-unused `OnDestroy` import is left for
|
|
819
|
+
* `eslint-plugin-unused-imports` to clean up.
|
|
820
|
+
* - When a class declares `implements OnDestroy` from `@angular/core` but has
|
|
821
|
+
* no `ngOnDestroy()` method (e.g. left over from a previous run), the
|
|
822
|
+
* orphaned implements clause is removed.
|
|
823
|
+
*/ var dbxWebNoRedundantOnDestroyRule = {
|
|
824
|
+
meta: {
|
|
825
|
+
type: 'suggestion',
|
|
826
|
+
fixable: 'code',
|
|
827
|
+
docs: {
|
|
828
|
+
description: 'Disallow redundant ngOnDestroy calls when fields are already wrapped with cleanSubscription/completeOnDestroy/clean',
|
|
829
|
+
recommended: true
|
|
830
|
+
},
|
|
831
|
+
messages: {
|
|
832
|
+
redundantCleanupCall: 'Redundant `this.{{name}}.{{method}}()` — `{{name}}` is initialized via `{{wrapper}}(...)` which already registers cleanup with Angular DestroyRef.',
|
|
833
|
+
redundantNgOnDestroy: '`ngOnDestroy()` only contains redundant cleanup calls for fields wrapped with cleanSubscription/completeOnDestroy/clean. Remove the method.',
|
|
834
|
+
emptyNgOnDestroy: '`ngOnDestroy()` has an empty body. Remove the method.',
|
|
835
|
+
orphanedImplementsOnDestroy: 'Class declares `implements OnDestroy` but has no `ngOnDestroy()` method. Remove the implements clause.'
|
|
836
|
+
},
|
|
837
|
+
schema: []
|
|
838
|
+
},
|
|
839
|
+
create: function create(context) {
|
|
840
|
+
var registry = createImportRegistry();
|
|
841
|
+
var sourceCode = context.sourceCode;
|
|
842
|
+
var visitClass = function visitClass(classNode) {
|
|
843
|
+
var _ngOnDestroy_value_body, _ngOnDestroy_value;
|
|
844
|
+
var matchedDecorator = findAngularComponentDecorator(classNode, registry);
|
|
845
|
+
if (!matchedDecorator) {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
var ngOnDestroy = findNgOnDestroyMethod(classNode);
|
|
849
|
+
var body = ngOnDestroy === null || ngOnDestroy === void 0 ? void 0 : (_ngOnDestroy_value = ngOnDestroy.value) === null || _ngOnDestroy_value === void 0 ? void 0 : (_ngOnDestroy_value_body = _ngOnDestroy_value.body) === null || _ngOnDestroy_value_body === void 0 ? void 0 : _ngOnDestroy_value_body.body;
|
|
850
|
+
if (!ngOnDestroy || !body) {
|
|
851
|
+
var implementsMatch = findOnDestroyImplementsClause(classNode, registry);
|
|
852
|
+
if (implementsMatch) {
|
|
853
|
+
context.report({
|
|
854
|
+
node: implementsMatch.clauseSpecifier,
|
|
855
|
+
messageId: 'orphanedImplementsOnDestroy',
|
|
856
|
+
fix: function fix(fixer) {
|
|
857
|
+
return [
|
|
858
|
+
fixer.removeRange(getImplementsSpecifierRemovalRange(implementsMatch, sourceCode))
|
|
859
|
+
];
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (body.length === 0) {
|
|
866
|
+
context.report({
|
|
867
|
+
node: ngOnDestroy,
|
|
868
|
+
messageId: 'emptyNgOnDestroy',
|
|
869
|
+
fix: function fix(fixer) {
|
|
870
|
+
return buildRemoveNgOnDestroyFixes({
|
|
871
|
+
fixer: fixer,
|
|
872
|
+
ngOnDestroy: ngOnDestroy,
|
|
873
|
+
classNode: classNode,
|
|
874
|
+
registry: registry,
|
|
875
|
+
sourceCode: sourceCode
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
var wrappedFields = collectWrappedFieldNames(classNode);
|
|
882
|
+
var redundantStatements = [];
|
|
883
|
+
var hasNonRedundantStatement = false;
|
|
884
|
+
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
|
|
885
|
+
try {
|
|
886
|
+
for(var _iterator = body[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
|
|
887
|
+
var statement = _step.value;
|
|
888
|
+
var match = matchRedundantCleanupStatement(statement, wrappedFields);
|
|
889
|
+
if (match) {
|
|
890
|
+
redundantStatements.push(match);
|
|
891
|
+
} else {
|
|
892
|
+
hasNonRedundantStatement = true;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
} catch (err) {
|
|
896
|
+
_didIteratorError = true;
|
|
897
|
+
_iteratorError = err;
|
|
898
|
+
} finally{
|
|
899
|
+
try {
|
|
900
|
+
if (!_iteratorNormalCompletion && _iterator.return != null) {
|
|
901
|
+
_iterator.return();
|
|
902
|
+
}
|
|
903
|
+
} finally{
|
|
904
|
+
if (_didIteratorError) {
|
|
905
|
+
throw _iteratorError;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (redundantStatements.length === 0) {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (hasNonRedundantStatement) {
|
|
913
|
+
var _iteratorNormalCompletion1 = true, _didIteratorError1 = false, _iteratorError1 = undefined;
|
|
914
|
+
try {
|
|
915
|
+
var _loop = function() {
|
|
916
|
+
var entry = _step1.value;
|
|
917
|
+
context.report({
|
|
918
|
+
node: entry.statement,
|
|
919
|
+
messageId: 'redundantCleanupCall',
|
|
920
|
+
data: {
|
|
921
|
+
name: entry.fieldName,
|
|
922
|
+
method: entry.method,
|
|
923
|
+
wrapper: entry.wrapper
|
|
924
|
+
},
|
|
925
|
+
fix: function fix(fixer) {
|
|
926
|
+
return [
|
|
927
|
+
fixer.removeRange(getStatementRangeWithLeadingWhitespace(entry.statement, sourceCode))
|
|
928
|
+
];
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
};
|
|
932
|
+
for(var _iterator1 = redundantStatements[Symbol.iterator](), _step1; !(_iteratorNormalCompletion1 = (_step1 = _iterator1.next()).done); _iteratorNormalCompletion1 = true)_loop();
|
|
933
|
+
} catch (err) {
|
|
934
|
+
_didIteratorError1 = true;
|
|
935
|
+
_iteratorError1 = err;
|
|
936
|
+
} finally{
|
|
937
|
+
try {
|
|
938
|
+
if (!_iteratorNormalCompletion1 && _iterator1.return != null) {
|
|
939
|
+
_iterator1.return();
|
|
940
|
+
}
|
|
941
|
+
} finally{
|
|
942
|
+
if (_didIteratorError1) {
|
|
943
|
+
throw _iteratorError1;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
} else {
|
|
948
|
+
context.report({
|
|
949
|
+
node: ngOnDestroy,
|
|
950
|
+
messageId: 'redundantNgOnDestroy',
|
|
951
|
+
fix: function fix(fixer) {
|
|
952
|
+
return buildRemoveNgOnDestroyFixes({
|
|
953
|
+
fixer: fixer,
|
|
954
|
+
ngOnDestroy: ngOnDestroy,
|
|
955
|
+
classNode: classNode,
|
|
956
|
+
registry: registry,
|
|
957
|
+
sourceCode: sourceCode
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
return {
|
|
964
|
+
ImportDeclaration: function ImportDeclaration(node) {
|
|
965
|
+
trackImportDeclaration(registry, node);
|
|
966
|
+
},
|
|
967
|
+
ClassDeclaration: function ClassDeclaration(classNode) {
|
|
968
|
+
visitClass(classNode);
|
|
969
|
+
},
|
|
970
|
+
ClassExpression: function ClassExpression(classNode) {
|
|
971
|
+
visitClass(classNode);
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
/**
|
|
977
|
+
* Walks the class members and returns a map of property name to the wrapper
|
|
978
|
+
* helper used in its initializer (`cleanSubscription`, `completeOnDestroy`, or
|
|
979
|
+
* `clean`). Properties whose initializer is not a recognized wrapper are
|
|
980
|
+
* omitted.
|
|
981
|
+
*
|
|
982
|
+
* @param classNode - The ClassDeclaration / ClassExpression AST node.
|
|
983
|
+
* @returns Map of field name to wrapper helper name.
|
|
984
|
+
*/ function collectWrappedFieldNames(classNode) {
|
|
985
|
+
var _ref;
|
|
986
|
+
var _classNode_body;
|
|
987
|
+
var result = new Map();
|
|
988
|
+
var members = (_ref = (_classNode_body = classNode.body) === null || _classNode_body === void 0 ? void 0 : _classNode_body.body) !== null && _ref !== void 0 ? _ref : [];
|
|
989
|
+
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
|
|
990
|
+
try {
|
|
991
|
+
for(var _iterator = members[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
|
|
992
|
+
var member = _step.value;
|
|
993
|
+
if (member.type !== 'PropertyDefinition' || isStaticProperty(member) || isDeclareProperty(member)) {
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
var propName = getClassMemberName(member);
|
|
997
|
+
var wrapper = propName ? wrapperNameFromInitializer(member.value) : null;
|
|
998
|
+
if (propName && wrapper) {
|
|
999
|
+
result.set(propName, wrapper);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
} catch (err) {
|
|
1003
|
+
_didIteratorError = true;
|
|
1004
|
+
_iteratorError = err;
|
|
1005
|
+
} finally{
|
|
1006
|
+
try {
|
|
1007
|
+
if (!_iteratorNormalCompletion && _iterator.return != null) {
|
|
1008
|
+
_iterator.return();
|
|
1009
|
+
}
|
|
1010
|
+
} finally{
|
|
1011
|
+
if (_didIteratorError) {
|
|
1012
|
+
throw _iteratorError;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return result;
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Returns the name of the cleanup helper wrapping the given initializer
|
|
1020
|
+
* expression, or null when the expression is not wrapped.
|
|
1021
|
+
*
|
|
1022
|
+
* @param expression - The initializer expression, or null/undefined.
|
|
1023
|
+
* @returns The wrapper helper name (`cleanSubscription` etc.) or null.
|
|
1024
|
+
*/ function wrapperNameFromInitializer(expression) {
|
|
1025
|
+
return expression ? isCalledIdentifier(expression, HELPER_NAMES) : null;
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Returns details for a redundant cleanup statement, or null when the
|
|
1029
|
+
* statement is anything other than a redundant `this.<field>.<destroy|complete>()` call.
|
|
1030
|
+
*
|
|
1031
|
+
* @param statement - The body statement AST node.
|
|
1032
|
+
* @param wrappedFields - Map of class field names to their wrapper helper names.
|
|
1033
|
+
* @returns Match details, or null.
|
|
1034
|
+
*/ function matchRedundantCleanupStatement(statement, wrappedFields) {
|
|
1035
|
+
var result = null;
|
|
1036
|
+
if (statement.type === 'ExpressionStatement') {
|
|
1037
|
+
var _call_arguments, _call_callee;
|
|
1038
|
+
var call = statement.expression;
|
|
1039
|
+
var isZeroArgMemberCall = (call === null || call === void 0 ? void 0 : call.type) === 'CallExpression' && ((_call_arguments = call.arguments) === null || _call_arguments === void 0 ? void 0 : _call_arguments.length) === 0 && ((_call_callee = call.callee) === null || _call_callee === void 0 ? void 0 : _call_callee.type) === 'MemberExpression';
|
|
1040
|
+
if (isZeroArgMemberCall) {
|
|
1041
|
+
var _callee_property, _callee_object, _callee_object_property;
|
|
1042
|
+
var callee = call.callee;
|
|
1043
|
+
var methodName = !callee.computed && ((_callee_property = callee.property) === null || _callee_property === void 0 ? void 0 : _callee_property.type) === 'Identifier' ? callee.property.name : null;
|
|
1044
|
+
if (methodName && REDUNDANT_METHODS.has(methodName) && ((_callee_object = callee.object) === null || _callee_object === void 0 ? void 0 : _callee_object.type) === 'MemberExpression' && ((_callee_object_property = callee.object.property) === null || _callee_object_property === void 0 ? void 0 : _callee_object_property.type) === 'Identifier' && !callee.object.computed) {
|
|
1045
|
+
var fieldName = callee.object.property.name;
|
|
1046
|
+
var wrapper = isThisMemberAccess(callee.object, fieldName) ? wrappedFields.get(fieldName) : undefined;
|
|
1047
|
+
if (wrapper) {
|
|
1048
|
+
result = {
|
|
1049
|
+
statement: statement,
|
|
1050
|
+
fieldName: fieldName,
|
|
1051
|
+
method: methodName,
|
|
1052
|
+
wrapper: wrapper
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return result;
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Builds the fix list for removing the entire `ngOnDestroy` method along with
|
|
1062
|
+
* any matching `implements OnDestroy` clause from the class declaration.
|
|
1063
|
+
*
|
|
1064
|
+
* @param input - The fixer, method node, class node, registry, and source-code service.
|
|
1065
|
+
* @returns The fix operations to apply.
|
|
1066
|
+
*/ function buildRemoveNgOnDestroyFixes(input) {
|
|
1067
|
+
var fixer = input.fixer, ngOnDestroy = input.ngOnDestroy, classNode = input.classNode, registry = input.registry, sourceCode = input.sourceCode;
|
|
1068
|
+
var fixes = [
|
|
1069
|
+
fixer.removeRange(getStatementRangeWithLeadingWhitespace(ngOnDestroy, sourceCode))
|
|
1070
|
+
];
|
|
1071
|
+
var implementsMatch = findOnDestroyImplementsClause(classNode, registry);
|
|
1072
|
+
if (implementsMatch) {
|
|
1073
|
+
fixes.push(fixer.removeRange(getImplementsSpecifierRemovalRange(implementsMatch, sourceCode)));
|
|
1074
|
+
}
|
|
1075
|
+
return fixes;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* ESLint plugin for dbx-web rules.
|
|
1080
|
+
*
|
|
1081
|
+
* Register as a plugin in your flat ESLint config, then enable individual rules
|
|
1082
|
+
* under the chosen plugin prefix (e.g. 'dereekb-dbx-web/require-clean-subscription').
|
|
1083
|
+
*/ var dbxWebEslintPlugin = {
|
|
1084
|
+
rules: {
|
|
1085
|
+
'require-clean-subscription': dbxWebRequireCleanSubscriptionRule,
|
|
1086
|
+
'require-complete-on-destroy': dbxWebRequireCompleteOnDestroyRule,
|
|
1087
|
+
'no-redundant-on-destroy': dbxWebNoRedundantOnDestroyRule
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
export { dbxWebEslintPlugin, dbxWebNoRedundantOnDestroyRule, dbxWebRequireCleanSubscriptionRule, dbxWebRequireCompleteOnDestroyRule };
|