@benjavicente/lint-angular 0.0.2 → 0.0.4
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 +23 -17
- package/dist/index.mjs +776 -656
- package/package.json +5 -5
package/dist/index.mjs
CHANGED
|
@@ -55,13 +55,13 @@ function unwrapExpression(node) {
|
|
|
55
55
|
}
|
|
56
56
|
//#endregion
|
|
57
57
|
//#region src/utilities/scope.ts
|
|
58
|
-
const FUNCTION_TYPES$
|
|
58
|
+
const FUNCTION_TYPES$2 = new Set([
|
|
59
59
|
"ArrowFunctionExpression",
|
|
60
60
|
"FunctionDeclaration",
|
|
61
61
|
"FunctionExpression"
|
|
62
62
|
]);
|
|
63
63
|
function isFunction$1(node) {
|
|
64
|
-
return !!node && FUNCTION_TYPES$
|
|
64
|
+
return !!node && FUNCTION_TYPES$2.has(node.type);
|
|
65
65
|
}
|
|
66
66
|
function addBindingIdentifierNodes(node, identifiers) {
|
|
67
67
|
if (!node) return;
|
|
@@ -171,19 +171,45 @@ function isShadowedIdentifier(context, node) {
|
|
|
171
171
|
}
|
|
172
172
|
//#endregion
|
|
173
173
|
//#region src/utilities/angular.ts
|
|
174
|
-
function
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
function getProgramNode(context, node) {
|
|
175
|
+
if (node.type === "Program") return node;
|
|
176
|
+
return context.sourceCode.getAncestors(node).find((ancestor) => ancestor.type === "Program") ?? null;
|
|
177
|
+
}
|
|
178
|
+
function getImportedName(context, node, source) {
|
|
179
|
+
if (node?.type !== "Identifier") return null;
|
|
180
|
+
if (isShadowedIdentifier(context, node)) return null;
|
|
181
|
+
const program = getProgramNode(context, node);
|
|
182
|
+
for (const statement of program?.body ?? []) {
|
|
183
|
+
if (statement.type !== "ImportDeclaration" || statement.source?.value !== source) continue;
|
|
184
|
+
for (const specifier of statement.specifiers ?? []) {
|
|
185
|
+
if (specifier.type !== "ImportSpecifier" || specifier.local.name !== node.name) continue;
|
|
186
|
+
return getPropertyName(specifier.imported);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
function isNamespaceImport(context, node, source) {
|
|
192
|
+
if (node?.type !== "Identifier") return false;
|
|
193
|
+
if (isShadowedIdentifier(context, node)) return false;
|
|
194
|
+
const program = getProgramNode(context, node);
|
|
195
|
+
for (const statement of program?.body ?? []) {
|
|
196
|
+
if (statement.type !== "ImportDeclaration" || statement.source?.value !== source) continue;
|
|
197
|
+
if ((statement.specifiers ?? []).some((specifier) => specifier.type === "ImportNamespaceSpecifier" && specifier.local.name === node.name)) return true;
|
|
178
198
|
}
|
|
179
|
-
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
function isImportedReference(context, node, source, importedNames) {
|
|
202
|
+
const importedName = getImportedName(context, node, source);
|
|
203
|
+
return !!importedName && importedNames.has(importedName);
|
|
204
|
+
}
|
|
205
|
+
function isImportedNamespaceMember(context, node, source, memberNames) {
|
|
206
|
+
return node?.type === "MemberExpression" && node.object?.type === "Identifier" && isNamespaceImport(context, node.object, source) && memberNames.has(getPropertyName(node.property) ?? "");
|
|
180
207
|
}
|
|
181
|
-
function isAngularCoreDecorator(context, decorator,
|
|
208
|
+
function isAngularCoreDecorator(context, decorator, decoratorNames) {
|
|
182
209
|
if (!decorator) return false;
|
|
183
210
|
const expression = decorator.expression ?? decorator;
|
|
184
211
|
const callee = expression?.type === "CallExpression" ? expression.callee : expression;
|
|
185
|
-
|
|
186
|
-
return callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && imports.angularNamespaces.has(callee.object.name) && !isShadowedIdentifier(context, callee.object) && imports.decoratorNames.has(getPropertyName(callee.property) ?? "");
|
|
212
|
+
return isImportedReference(context, callee, "@angular/core", decoratorNames) || isImportedNamespaceMember(context, callee, "@angular/core", decoratorNames);
|
|
187
213
|
}
|
|
188
214
|
//#endregion
|
|
189
215
|
//#region src/rules/class-member-order/index.ts
|
|
@@ -196,7 +222,7 @@ const ANGULAR_CLASS_DECORATOR_NAMES$1 = new Set([
|
|
|
196
222
|
]);
|
|
197
223
|
const INPUT_MODEL_CALL_NAMES = new Set(["input", "model"]);
|
|
198
224
|
const OUTPUT_CALL_NAMES = new Set(["output", "outputFromObservable"]);
|
|
199
|
-
const CLASS_FIELD_TYPES$
|
|
225
|
+
const CLASS_FIELD_TYPES$2 = new Set([
|
|
200
226
|
"AccessorProperty",
|
|
201
227
|
"FieldDefinition",
|
|
202
228
|
"PropertyDefinition"
|
|
@@ -207,38 +233,33 @@ const ORDER_LABELS = [
|
|
|
207
233
|
"outputs",
|
|
208
234
|
"everything else"
|
|
209
235
|
];
|
|
210
|
-
function hasAngularClassDecorator(context, classNode
|
|
236
|
+
function hasAngularClassDecorator(context, classNode) {
|
|
211
237
|
if (!classNode || !Array.isArray(classNode.decorators)) return false;
|
|
212
|
-
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator,
|
|
238
|
+
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, ANGULAR_CLASS_DECORATOR_NAMES$1));
|
|
213
239
|
}
|
|
214
|
-
function isApiCall(context, node,
|
|
240
|
+
function isApiCall(context, node, apiNames) {
|
|
215
241
|
if (!node || node.type !== "CallExpression") return false;
|
|
216
242
|
const callee = node.callee;
|
|
217
|
-
if (callee
|
|
243
|
+
if (isImportedReference(context, callee, "@angular/core", apiNames)) return true;
|
|
218
244
|
if (callee?.type !== "MemberExpression") return false;
|
|
219
245
|
const supportsRequiredApi = [...INPUT_MODEL_CALL_NAMES].some((name) => apiNames.has(name));
|
|
220
246
|
if (callee.object?.type === "Identifier") {
|
|
221
|
-
if (getPropertyName(callee.property) === "required") return supportsRequiredApi &&
|
|
222
|
-
return
|
|
247
|
+
if (getPropertyName(callee.property) === "required") return supportsRequiredApi && isImportedReference(context, callee.object, "@angular/core", apiNames);
|
|
248
|
+
return isImportedNamespaceMember(context, callee, "@angular/core", apiNames);
|
|
223
249
|
}
|
|
224
|
-
if (getPropertyName(callee.property) === "required" && callee.object?.type === "MemberExpression" && callee.object.object?.type === "Identifier") return supportsRequiredApi &&
|
|
250
|
+
if (getPropertyName(callee.property) === "required" && callee.object?.type === "MemberExpression" && callee.object.object?.type === "Identifier") return supportsRequiredApi && isImportedNamespaceMember(context, callee.object, "@angular/core", apiNames) && apiNames.has(getPropertyName(callee.object.property) ?? "");
|
|
225
251
|
return false;
|
|
226
252
|
}
|
|
227
|
-
function hasDecorator(context, element,
|
|
228
|
-
return Array.isArray(element.decorators) ? element.decorators.some((decorator) =>
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (isApiCall(context, element.value, imports.injectLocalNames, imports.angularNamespaces, new Set(["inject"]))) return 0;
|
|
238
|
-
if (isApiCall(context, element.value, imports.inputModelLocalNames, imports.angularNamespaces, INPUT_MODEL_CALL_NAMES)) return 1;
|
|
239
|
-
if (hasDecorator(context, element, imports.inputModelDecoratorLocalNames, imports.angularNamespaces, new Set(["Input"]))) return 1;
|
|
240
|
-
if (isApiCall(context, element.value, imports.outputLocalNames, imports.angularNamespaces, OUTPUT_CALL_NAMES)) return 2;
|
|
241
|
-
if (hasDecorator(context, element, imports.outputDecoratorLocalNames, imports.angularNamespaces, new Set(["Output"]))) return 2;
|
|
253
|
+
function hasDecorator(context, element, decoratorNames) {
|
|
254
|
+
return Array.isArray(element.decorators) ? element.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorNames)) : false;
|
|
255
|
+
}
|
|
256
|
+
function classifyMember(context, element) {
|
|
257
|
+
if (CLASS_FIELD_TYPES$2.has(element.type)) {
|
|
258
|
+
if (isApiCall(context, element.value, new Set(["inject"]))) return 0;
|
|
259
|
+
if (isApiCall(context, element.value, INPUT_MODEL_CALL_NAMES)) return 1;
|
|
260
|
+
if (hasDecorator(context, element, new Set(["Input"]))) return 1;
|
|
261
|
+
if (isApiCall(context, element.value, OUTPUT_CALL_NAMES)) return 2;
|
|
262
|
+
if (hasDecorator(context, element, new Set(["Output"]))) return 2;
|
|
242
263
|
return 3;
|
|
243
264
|
}
|
|
244
265
|
if (element.type === "MethodDefinition") return 3;
|
|
@@ -314,7 +335,7 @@ function getSortedMembersFix(context, classBody, classifiedMembers) {
|
|
|
314
335
|
for (const member of classifiedMembers) if (member.name) membersByName.set(member.name, member);
|
|
315
336
|
for (const member of classifiedMembers) {
|
|
316
337
|
const { element } = member;
|
|
317
|
-
if (!CLASS_FIELD_TYPES$
|
|
338
|
+
if (!CLASS_FIELD_TYPES$2.has(element.type)) return void 0;
|
|
318
339
|
if (element.type === "AccessorProperty") return void 0;
|
|
319
340
|
if (element.computed) return void 0;
|
|
320
341
|
if (Array.isArray(element.decorators) && element.decorators.length > 0) return void 0;
|
|
@@ -354,83 +375,47 @@ const classMemberOrder = defineRule({
|
|
|
354
375
|
messages: { outOfOrder: "Angular class member should be ordered before {{previousGroup}} and with {{expectedGroup}}." }
|
|
355
376
|
},
|
|
356
377
|
createOnce(context) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if (node.source?.value !== "@angular/core") return;
|
|
379
|
-
for (const specifier of node.specifiers ?? []) {
|
|
380
|
-
addAngularCoreDecoratorImport(specifier, ANGULAR_CLASS_DECORATOR_NAMES$1, imports);
|
|
381
|
-
if (specifier.type === "ImportSpecifier") {
|
|
382
|
-
const importedName = getPropertyName(specifier.imported);
|
|
383
|
-
if (importedName === "inject") imports.injectLocalNames.add(specifier.local.name);
|
|
384
|
-
if (importedName && INPUT_MODEL_CALL_NAMES.has(importedName)) imports.inputModelLocalNames.add(specifier.local.name);
|
|
385
|
-
if (importedName === "Input") imports.inputModelDecoratorLocalNames.add(specifier.local.name);
|
|
386
|
-
if (importedName && OUTPUT_CALL_NAMES.has(importedName)) imports.outputLocalNames.add(specifier.local.name);
|
|
387
|
-
if (importedName === "Output") imports.outputDecoratorLocalNames.add(specifier.local.name);
|
|
388
|
-
}
|
|
389
|
-
if (specifier.type === "ImportNamespaceSpecifier") imports.angularNamespaces.add(specifier.local.name);
|
|
390
|
-
}
|
|
391
|
-
},
|
|
392
|
-
ClassBody(node) {
|
|
393
|
-
const classBody = node;
|
|
394
|
-
if (!hasAngularClassDecorator(context, classBody.parent, imports)) return;
|
|
395
|
-
let highestSeen = null;
|
|
396
|
-
const classifiedMembers = [];
|
|
397
|
-
const outOfOrderReports = [];
|
|
398
|
-
for (const element of classBody.body ?? []) {
|
|
399
|
-
const group = classifyMember(context, element, imports);
|
|
400
|
-
if (group === null) continue;
|
|
401
|
-
classifiedMembers.push({
|
|
378
|
+
return { ClassBody(node) {
|
|
379
|
+
const classBody = node;
|
|
380
|
+
if (!hasAngularClassDecorator(context, classBody.parent)) return;
|
|
381
|
+
let highestSeen = null;
|
|
382
|
+
const classifiedMembers = [];
|
|
383
|
+
const outOfOrderReports = [];
|
|
384
|
+
for (const element of classBody.body ?? []) {
|
|
385
|
+
const group = classifyMember(context, element);
|
|
386
|
+
if (group === null) continue;
|
|
387
|
+
classifiedMembers.push({
|
|
388
|
+
element,
|
|
389
|
+
group,
|
|
390
|
+
effectiveGroup: group,
|
|
391
|
+
name: getMemberName$1(element.key),
|
|
392
|
+
dependencies: collectThisMemberReferences(element.value)
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
applyDependencyGroups(classifiedMembers);
|
|
396
|
+
for (const { element, effectiveGroup } of classifiedMembers) {
|
|
397
|
+
if (highestSeen !== null && effectiveGroup < highestSeen) {
|
|
398
|
+
outOfOrderReports.push({
|
|
402
399
|
element,
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
name: getMemberName$1(element.key),
|
|
406
|
-
dependencies: collectThisMemberReferences(element.value)
|
|
400
|
+
effectiveGroup,
|
|
401
|
+
previousEffectiveGroup: highestSeen
|
|
407
402
|
});
|
|
403
|
+
continue;
|
|
408
404
|
}
|
|
409
|
-
|
|
410
|
-
for (const { element, effectiveGroup } of classifiedMembers) {
|
|
411
|
-
if (highestSeen !== null && effectiveGroup < highestSeen) {
|
|
412
|
-
outOfOrderReports.push({
|
|
413
|
-
element,
|
|
414
|
-
effectiveGroup,
|
|
415
|
-
previousEffectiveGroup: highestSeen
|
|
416
|
-
});
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
highestSeen = effectiveGroup;
|
|
420
|
-
}
|
|
421
|
-
if (!outOfOrderReports.length) return;
|
|
422
|
-
const fix = getSortedMembersFix(context, classBody, classifiedMembers);
|
|
423
|
-
for (const [index, report] of outOfOrderReports.entries()) context.report({
|
|
424
|
-
node: report.element,
|
|
425
|
-
messageId: "outOfOrder",
|
|
426
|
-
data: {
|
|
427
|
-
expectedGroup: ORDER_LABELS[report.effectiveGroup],
|
|
428
|
-
previousGroup: ORDER_LABELS[report.previousEffectiveGroup]
|
|
429
|
-
},
|
|
430
|
-
fix: index === 0 ? fix : void 0
|
|
431
|
-
});
|
|
405
|
+
highestSeen = effectiveGroup;
|
|
432
406
|
}
|
|
433
|
-
|
|
407
|
+
if (!outOfOrderReports.length) return;
|
|
408
|
+
const fix = getSortedMembersFix(context, classBody, classifiedMembers);
|
|
409
|
+
for (const [index, report] of outOfOrderReports.entries()) context.report({
|
|
410
|
+
node: report.element,
|
|
411
|
+
messageId: "outOfOrder",
|
|
412
|
+
data: {
|
|
413
|
+
expectedGroup: ORDER_LABELS[report.effectiveGroup],
|
|
414
|
+
previousGroup: ORDER_LABELS[report.previousEffectiveGroup]
|
|
415
|
+
},
|
|
416
|
+
fix: index === 0 ? fix : void 0
|
|
417
|
+
});
|
|
418
|
+
} };
|
|
434
419
|
}
|
|
435
420
|
});
|
|
436
421
|
//#endregion
|
|
@@ -438,25 +423,22 @@ const classMemberOrder = defineRule({
|
|
|
438
423
|
const DEFAULT_DISALLOW_INJECT_INJECTOR = true;
|
|
439
424
|
const DEFAULT_DISALLOW_RUN_IN_INJECTION_CONTEXT = true;
|
|
440
425
|
const RUN_IN_INJECTION_CONTEXT_NAMES = new Set(["runInInjectionContext", "runInContext"]);
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
function isInjectCall(context, callNode, injectNames, angularNamespaces) {
|
|
426
|
+
const INJECT_NAMES = new Set(["inject"]);
|
|
427
|
+
const INJECTOR_NAMES = new Set(["Injector"]);
|
|
428
|
+
function isInjectCall(context, callNode) {
|
|
445
429
|
const callee = callNode.callee;
|
|
446
|
-
return callee
|
|
430
|
+
return isImportedReference(context, callee, "@angular/core", INJECT_NAMES) || isImportedNamespaceMember(context, callee, "@angular/core", INJECT_NAMES);
|
|
447
431
|
}
|
|
448
|
-
function isInjectorReference(context, node
|
|
449
|
-
return node
|
|
432
|
+
function isInjectorReference(context, node) {
|
|
433
|
+
return isImportedReference(context, node, "@angular/core", INJECTOR_NAMES) || isImportedNamespaceMember(context, node, "@angular/core", INJECTOR_NAMES);
|
|
450
434
|
}
|
|
451
|
-
function isDisallowedInjectInjector(context, callNode
|
|
452
|
-
if (!isInjectCall(context, callNode
|
|
453
|
-
return isInjectorReference(context, callNode.arguments?.[0]
|
|
435
|
+
function isDisallowedInjectInjector(context, callNode) {
|
|
436
|
+
if (!isInjectCall(context, callNode)) return false;
|
|
437
|
+
return isInjectorReference(context, callNode.arguments?.[0]);
|
|
454
438
|
}
|
|
455
|
-
function isDisallowedRunInInjectionContext(context, callNode
|
|
439
|
+
function isDisallowedRunInInjectionContext(context, callNode) {
|
|
456
440
|
const callee = callNode.callee;
|
|
457
|
-
|
|
458
|
-
if (callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && angularNamespaces.has(callee.object.name) && !isShadowedIdentifier(context, callee.object)) return RUN_IN_INJECTION_CONTEXT_NAMES.has(getPropertyName(callee.property) ?? "");
|
|
459
|
-
return false;
|
|
441
|
+
return isImportedReference(context, callee, "@angular/core", RUN_IN_INJECTION_CONTEXT_NAMES) || isImportedNamespaceMember(context, callee, "@angular/core", RUN_IN_INJECTION_CONTEXT_NAMES);
|
|
460
442
|
}
|
|
461
443
|
const avoidExplicitInjectionContext = defineRule({
|
|
462
444
|
meta: {
|
|
@@ -485,52 +467,25 @@ const avoidExplicitInjectionContext = defineRule({
|
|
|
485
467
|
}
|
|
486
468
|
},
|
|
487
469
|
createOnce(context) {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
if (node.source?.value !== "@angular/core") return;
|
|
503
|
-
for (const specifier of node.specifiers ?? []) {
|
|
504
|
-
if (specifier.type === "ImportSpecifier") {
|
|
505
|
-
const importedName = getPropertyName(specifier.imported);
|
|
506
|
-
if (importedName === "inject") injectNames.add(specifier.local.name);
|
|
507
|
-
if (importedName === "Injector") injectorNames.add(specifier.local.name);
|
|
508
|
-
if (importedName === "runInInjectionContext") runInInjectionContextNames.add(specifier.local.name);
|
|
509
|
-
if (importedName === "runInContext") runInContextNames.add(specifier.local.name);
|
|
510
|
-
}
|
|
511
|
-
if (specifier.type === "ImportNamespaceSpecifier") angularNamespaces.add(specifier.local.name);
|
|
512
|
-
}
|
|
513
|
-
},
|
|
514
|
-
CallExpression(node) {
|
|
515
|
-
const callNode = node;
|
|
516
|
-
const options = context.options[0] ?? {};
|
|
517
|
-
const disallowInjectInjector = options.disallowInjectInjector ?? DEFAULT_DISALLOW_INJECT_INJECTOR;
|
|
518
|
-
const disallowRunInInjectionContext = options.disallowRunInInjectionContext ?? DEFAULT_DISALLOW_RUN_IN_INJECTION_CONTEXT;
|
|
519
|
-
if (disallowInjectInjector && isDisallowedInjectInjector(context, callNode, injectNames, injectorNames, angularNamespaces)) context.report({
|
|
520
|
-
node: callNode.callee,
|
|
521
|
-
messageId: "avoidInjectInjector"
|
|
522
|
-
});
|
|
523
|
-
if (disallowRunInInjectionContext && isDisallowedRunInInjectionContext(context, callNode, runInInjectionContextNames, runInContextNames, angularNamespaces)) context.report({
|
|
524
|
-
node: callNode.callee,
|
|
525
|
-
messageId: "avoidRunInInjectionContext"
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
};
|
|
470
|
+
return { CallExpression(node) {
|
|
471
|
+
const callNode = node;
|
|
472
|
+
const options = context.options[0] ?? {};
|
|
473
|
+
const disallowInjectInjector = options.disallowInjectInjector ?? DEFAULT_DISALLOW_INJECT_INJECTOR;
|
|
474
|
+
const disallowRunInInjectionContext = options.disallowRunInInjectionContext ?? DEFAULT_DISALLOW_RUN_IN_INJECTION_CONTEXT;
|
|
475
|
+
if (disallowInjectInjector && isDisallowedInjectInjector(context, callNode)) context.report({
|
|
476
|
+
node: callNode.callee,
|
|
477
|
+
messageId: "avoidInjectInjector"
|
|
478
|
+
});
|
|
479
|
+
if (disallowRunInInjectionContext && isDisallowedRunInInjectionContext(context, callNode)) context.report({
|
|
480
|
+
node: callNode.callee,
|
|
481
|
+
messageId: "avoidRunInInjectionContext"
|
|
482
|
+
});
|
|
483
|
+
} };
|
|
529
484
|
}
|
|
530
485
|
});
|
|
531
486
|
//#endregion
|
|
532
487
|
//#region src/rules/avoid-explicit-subscription-management/index.ts
|
|
533
|
-
const TARGET_DECORATORS$
|
|
488
|
+
const TARGET_DECORATORS$4 = new Set([
|
|
534
489
|
"Component",
|
|
535
490
|
"Directive",
|
|
536
491
|
"Injectable"
|
|
@@ -547,9 +502,9 @@ const RXJS_SUBSCRIBABLE_NAMES = new Set([
|
|
|
547
502
|
"ReplaySubject",
|
|
548
503
|
"Subject"
|
|
549
504
|
]);
|
|
550
|
-
function hasTargetDecorator$
|
|
505
|
+
function hasTargetDecorator$4(context, classNode) {
|
|
551
506
|
if (!classNode || !Array.isArray(classNode.decorators)) return false;
|
|
552
|
-
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator,
|
|
507
|
+
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS$4));
|
|
553
508
|
}
|
|
554
509
|
function hasSubscriptionTypeReference(context, node, subscriptionLocalNames, rxjsNamespaces) {
|
|
555
510
|
if (!node) return false;
|
|
@@ -808,11 +763,6 @@ const avoidExplicitSubscriptionManagement = defineRule({
|
|
|
808
763
|
const rxjsNamespaces = /* @__PURE__ */ new Set();
|
|
809
764
|
const takeUntilDestroyedLocalNames = /* @__PURE__ */ new Set();
|
|
810
765
|
const interopNamespaces = /* @__PURE__ */ new Set();
|
|
811
|
-
const decoratorImports = {
|
|
812
|
-
decoratorNames: TARGET_DECORATORS$2,
|
|
813
|
-
decoratorLocalNames: /* @__PURE__ */ new Set(),
|
|
814
|
-
angularNamespaces: /* @__PURE__ */ new Set()
|
|
815
|
-
};
|
|
816
766
|
return {
|
|
817
767
|
before() {
|
|
818
768
|
subscriptionLocalNames.clear();
|
|
@@ -820,12 +770,9 @@ const avoidExplicitSubscriptionManagement = defineRule({
|
|
|
820
770
|
rxjsNamespaces.clear();
|
|
821
771
|
takeUntilDestroyedLocalNames.clear();
|
|
822
772
|
interopNamespaces.clear();
|
|
823
|
-
decoratorImports.decoratorLocalNames.clear();
|
|
824
|
-
decoratorImports.angularNamespaces.clear();
|
|
825
773
|
},
|
|
826
774
|
ImportDeclaration(node) {
|
|
827
775
|
const source = node.source?.value;
|
|
828
|
-
if (source === "@angular/core") for (const specifier of node.specifiers ?? []) addAngularCoreDecoratorImport(specifier, TARGET_DECORATORS$2, decoratorImports);
|
|
829
776
|
if (source === "rxjs") for (const specifier of node.specifiers ?? []) {
|
|
830
777
|
if (specifier.type === "ImportSpecifier") {
|
|
831
778
|
const importedName = getPropertyName(specifier.imported);
|
|
@@ -846,7 +793,7 @@ const avoidExplicitSubscriptionManagement = defineRule({
|
|
|
846
793
|
ClassBody(node) {
|
|
847
794
|
const classBody = node;
|
|
848
795
|
const classNode = classBody.parent;
|
|
849
|
-
if (!classNode || !hasTargetDecorator$
|
|
796
|
+
if (!classNode || !hasTargetDecorator$4(context, classNode)) return;
|
|
850
797
|
const rxjsSubscribableReferences = collectRxjsSubscribableReferences(context, classBody, classNode, rxjsSubscribableLocalNames, rxjsNamespaces);
|
|
851
798
|
const subscriptionReferences = collectSubscriptionReferences(context, classBody, classNode, subscriptionLocalNames, rxjsNamespaces, rxjsSubscribableReferences, rxjsSubscribableLocalNames, takeUntilDestroyedLocalNames, interopNamespaces);
|
|
852
799
|
walkNode$1(classBody, classNode, (current) => {
|
|
@@ -898,22 +845,59 @@ const avoidExplicitSubscriptionManagement = defineRule({
|
|
|
898
845
|
}
|
|
899
846
|
});
|
|
900
847
|
//#endregion
|
|
848
|
+
//#region src/rules/avoid-inappropriate-intimacy/index.ts
|
|
849
|
+
const TARGET_DECORATORS$3 = new Set([
|
|
850
|
+
"Component",
|
|
851
|
+
"Directive",
|
|
852
|
+
"Injectable",
|
|
853
|
+
"Service"
|
|
854
|
+
]);
|
|
855
|
+
function getAngularClassKind(context, classNode) {
|
|
856
|
+
if (!classNode || !Array.isArray(classNode.decorators)) return null;
|
|
857
|
+
if (classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, new Set(["Component"])))) return "component";
|
|
858
|
+
if (classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, new Set(["Directive"])))) return "directive";
|
|
859
|
+
if (classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS$3))) return "service";
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
const avoidInappropriateIntimacy = defineRule({
|
|
863
|
+
meta: {
|
|
864
|
+
type: "problem",
|
|
865
|
+
docs: {
|
|
866
|
+
description: "Disallow passing Angular component, directive, and service instances as function arguments.",
|
|
867
|
+
recommended: true
|
|
868
|
+
},
|
|
869
|
+
schema: [],
|
|
870
|
+
messages: { avoidThisArgument: "Avoid passing this {{kind}} instance as an argument. Pass the specific values or callbacks the callee needs." }
|
|
871
|
+
},
|
|
872
|
+
createOnce(context) {
|
|
873
|
+
return { CallExpression(node) {
|
|
874
|
+
const callNode = node;
|
|
875
|
+
const thisArguments = (callNode.arguments ?? []).filter((argument) => argument.type === "ThisExpression");
|
|
876
|
+
if (!thisArguments.length) return;
|
|
877
|
+
const kind = getAngularClassKind(context, context.sourceCode.getAncestors(callNode).findLast((ancestor) => ancestor.type === "ClassDeclaration" || ancestor.type === "ClassExpression"));
|
|
878
|
+
if (!kind) return;
|
|
879
|
+
for (const argument of thisArguments) context.report({
|
|
880
|
+
node: argument,
|
|
881
|
+
messageId: "avoidThisArgument",
|
|
882
|
+
data: { kind }
|
|
883
|
+
});
|
|
884
|
+
} };
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
//#endregion
|
|
901
888
|
//#region src/rules/avoid-ng-modules/index.ts
|
|
902
889
|
const DEFAULT_ALLOW_FOR_GROUPING = true;
|
|
903
890
|
const DEFAULT_ALLOW_FOR_PROVIDING = false;
|
|
904
891
|
const DEFAULT_ALLOW_FOR_ROUTING = false;
|
|
905
|
-
function isNgModuleDecorator(context, node
|
|
892
|
+
function isNgModuleDecorator(context, node) {
|
|
906
893
|
if (node.type !== "Decorator") return false;
|
|
907
894
|
const expression = node.expression ?? node;
|
|
908
895
|
const callee = expression?.type === "CallExpression" ? expression.callee : expression;
|
|
909
|
-
if (callee?.type === "Identifier")
|
|
910
|
-
|
|
911
|
-
return !!binding && binding.source === "@angular/core" && binding.importedName === "NgModule" && !isShadowedIdentifier(context, callee);
|
|
912
|
-
}
|
|
913
|
-
return callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && namespaceImportBindings.get(callee.object.name) === "@angular/core" && !isShadowedIdentifier(context, callee.object) && getPropertyName(callee.property) === "NgModule";
|
|
896
|
+
if (callee?.type === "Identifier") return getImportedName(context, callee, "@angular/core") === "NgModule";
|
|
897
|
+
return callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && isNamespaceImport(context, callee.object, "@angular/core") && getPropertyName(callee.property) === "NgModule";
|
|
914
898
|
}
|
|
915
|
-
function getNgModuleMetadata(context, node
|
|
916
|
-
if (!isNgModuleDecorator(context, node
|
|
899
|
+
function getNgModuleMetadata(context, node) {
|
|
900
|
+
if (!isNgModuleDecorator(context, node)) return null;
|
|
917
901
|
const expression = node.expression;
|
|
918
902
|
if (expression?.type !== "CallExpression") return null;
|
|
919
903
|
const metadata = expression.arguments?.[0];
|
|
@@ -927,18 +911,15 @@ function getArrayElementsFromProperty(metadata, propertyName) {
|
|
|
927
911
|
function isMemberCall(node, methodName) {
|
|
928
912
|
return node.type === "CallExpression" && getPropertyName(node.callee?.property) === methodName;
|
|
929
913
|
}
|
|
930
|
-
function isModuleCallFromImport(node, expectedSource, expectedModuleName
|
|
914
|
+
function isModuleCallFromImport(context, node, expectedSource, expectedModuleName) {
|
|
931
915
|
if (node.type !== "CallExpression") return false;
|
|
932
916
|
const callee = node.callee;
|
|
933
917
|
if (callee?.type !== "MemberExpression") return false;
|
|
934
918
|
const moduleRef = callee.object;
|
|
935
|
-
if (moduleRef?.type === "Identifier")
|
|
936
|
-
const binding = importBindings.get(moduleRef.name);
|
|
937
|
-
return !!binding && binding.source === expectedSource && binding.importedName === expectedModuleName;
|
|
938
|
-
}
|
|
919
|
+
if (moduleRef?.type === "Identifier") return getImportedName(context, moduleRef, expectedSource) === expectedModuleName;
|
|
939
920
|
if (moduleRef?.type !== "MemberExpression") return false;
|
|
940
921
|
if (moduleRef.object?.type !== "Identifier") return false;
|
|
941
|
-
if (
|
|
922
|
+
if (!isNamespaceImport(context, moduleRef.object, expectedSource)) return false;
|
|
942
923
|
return getPropertyName(moduleRef.property) === expectedModuleName;
|
|
943
924
|
}
|
|
944
925
|
function getNgModuleClassNode(decoratorNode) {
|
|
@@ -989,123 +970,99 @@ const avoidNgModules = defineRule({
|
|
|
989
970
|
}
|
|
990
971
|
},
|
|
991
972
|
createOnce(context) {
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
973
|
+
return { Decorator(node) {
|
|
974
|
+
const options = context.options[0] ?? {};
|
|
975
|
+
const allowForGrouping = options.allowForGrouping ?? DEFAULT_ALLOW_FOR_GROUPING;
|
|
976
|
+
const allowForProviding = options.allowForProviding ?? DEFAULT_ALLOW_FOR_PROVIDING;
|
|
977
|
+
const allowForRouting = options.allowForRouting ?? DEFAULT_ALLOW_FOR_ROUTING;
|
|
978
|
+
const metadata = getNgModuleMetadata(context, node);
|
|
979
|
+
if (!metadata) return;
|
|
980
|
+
const importsElements = getArrayElementsFromProperty(metadata, "imports");
|
|
981
|
+
const exportsElements = getArrayElementsFromProperty(metadata, "exports");
|
|
982
|
+
if (!allowForGrouping && (importsElements.length > 0 || exportsElements.length > 0)) context.report({
|
|
983
|
+
node,
|
|
984
|
+
messageId: "avoidModuleImportsExports"
|
|
985
|
+
});
|
|
986
|
+
const importCalls = importsElements.filter((element) => element.type === "CallExpression");
|
|
987
|
+
if (!allowForProviding) {
|
|
988
|
+
for (const call of importCalls) {
|
|
989
|
+
const methodName = getPropertyName(call.callee?.property);
|
|
990
|
+
if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@angular/router", "RouterModule") && !allowForRouting) {
|
|
991
|
+
context.report({
|
|
992
|
+
node: call,
|
|
993
|
+
messageId: "avoidRouterForRoot"
|
|
1009
994
|
});
|
|
1010
995
|
continue;
|
|
1011
996
|
}
|
|
1012
|
-
if (
|
|
1013
|
-
}
|
|
1014
|
-
},
|
|
1015
|
-
Decorator(node) {
|
|
1016
|
-
const options = context.options[0] ?? {};
|
|
1017
|
-
const allowForGrouping = options.allowForGrouping ?? DEFAULT_ALLOW_FOR_GROUPING;
|
|
1018
|
-
const allowForProviding = options.allowForProviding ?? DEFAULT_ALLOW_FOR_PROVIDING;
|
|
1019
|
-
const allowForRouting = options.allowForRouting ?? DEFAULT_ALLOW_FOR_ROUTING;
|
|
1020
|
-
const metadata = getNgModuleMetadata(context, node, importBindings, namespaceImportBindings);
|
|
1021
|
-
if (!metadata) return;
|
|
1022
|
-
const importsElements = getArrayElementsFromProperty(metadata, "imports");
|
|
1023
|
-
const exportsElements = getArrayElementsFromProperty(metadata, "exports");
|
|
1024
|
-
if (!allowForGrouping && (importsElements.length > 0 || exportsElements.length > 0)) context.report({
|
|
1025
|
-
node,
|
|
1026
|
-
messageId: "avoidModuleImportsExports"
|
|
1027
|
-
});
|
|
1028
|
-
const importCalls = importsElements.filter((element) => element.type === "CallExpression");
|
|
1029
|
-
if (!allowForProviding) {
|
|
1030
|
-
for (const call of importCalls) {
|
|
1031
|
-
const methodName = getPropertyName(call.callee?.property);
|
|
1032
|
-
if (methodName === "forRoot" && isModuleCallFromImport(call, "@angular/router", "RouterModule", importBindings, namespaceImportBindings) && !allowForRouting) {
|
|
1033
|
-
context.report({
|
|
1034
|
-
node: call,
|
|
1035
|
-
messageId: "avoidRouterForRoot"
|
|
1036
|
-
});
|
|
1037
|
-
continue;
|
|
1038
|
-
}
|
|
1039
|
-
if (methodName === "forRoot" && isModuleCallFromImport(call, "@ngrx/store", "StoreModule", importBindings, namespaceImportBindings)) {
|
|
1040
|
-
context.report({
|
|
1041
|
-
node: call,
|
|
1042
|
-
messageId: "avoidNgrxStoreForRoot"
|
|
1043
|
-
});
|
|
1044
|
-
continue;
|
|
1045
|
-
}
|
|
1046
|
-
if (methodName === "forRoot" && isModuleCallFromImport(call, "@ngrx/effects", "EffectsModule", importBindings, namespaceImportBindings)) {
|
|
1047
|
-
context.report({
|
|
1048
|
-
node: call,
|
|
1049
|
-
messageId: "avoidNgrxEffectsForRoot"
|
|
1050
|
-
});
|
|
1051
|
-
continue;
|
|
1052
|
-
}
|
|
1053
|
-
if (methodName === "forFeature" && isModuleCallFromImport(call, "@ngrx/store", "StoreModule", importBindings, namespaceImportBindings)) {
|
|
1054
|
-
context.report({
|
|
1055
|
-
node: call,
|
|
1056
|
-
messageId: "avoidNgrxStoreForFeature"
|
|
1057
|
-
});
|
|
1058
|
-
continue;
|
|
1059
|
-
}
|
|
1060
|
-
if (methodName === "forFeature" && isModuleCallFromImport(call, "@ngrx/effects", "EffectsModule", importBindings, namespaceImportBindings)) {
|
|
1061
|
-
context.report({
|
|
1062
|
-
node: call,
|
|
1063
|
-
messageId: "avoidNgrxEffectsForFeature"
|
|
1064
|
-
});
|
|
1065
|
-
continue;
|
|
1066
|
-
}
|
|
1067
|
-
if (methodName === "instrument" && isModuleCallFromImport(call, "@ngrx/store-devtools", "StoreDevtoolsModule", importBindings, namespaceImportBindings)) {
|
|
1068
|
-
context.report({
|
|
1069
|
-
node: call,
|
|
1070
|
-
messageId: "avoidNgrxStoreDevtoolsInstrument"
|
|
1071
|
-
});
|
|
1072
|
-
continue;
|
|
1073
|
-
}
|
|
1074
|
-
if (methodName === "forRoot" && isModuleCallFromImport(call, "@ngrx/router-store", "StoreRouterConnectingModule", importBindings, namespaceImportBindings)) {
|
|
1075
|
-
context.report({
|
|
1076
|
-
node: call,
|
|
1077
|
-
messageId: "avoidNgrxRouterStoreForRoot"
|
|
1078
|
-
});
|
|
1079
|
-
continue;
|
|
1080
|
-
}
|
|
1081
|
-
if (methodName === "forRoot" && isModuleCallFromImport(call, "@angular/router", "RouterModule", importBindings, namespaceImportBindings) && allowForRouting) continue;
|
|
1082
|
-
if (!isMemberCall(call, "forRoot")) continue;
|
|
997
|
+
if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@ngrx/store", "StoreModule")) {
|
|
1083
998
|
context.report({
|
|
1084
999
|
node: call,
|
|
1085
|
-
messageId: "
|
|
1000
|
+
messageId: "avoidNgrxStoreForRoot"
|
|
1086
1001
|
});
|
|
1002
|
+
continue;
|
|
1087
1003
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
if (
|
|
1096
|
-
|
|
1004
|
+
if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@ngrx/effects", "EffectsModule")) {
|
|
1005
|
+
context.report({
|
|
1006
|
+
node: call,
|
|
1007
|
+
messageId: "avoidNgrxEffectsForRoot"
|
|
1008
|
+
});
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
if (methodName === "forFeature" && isModuleCallFromImport(context, call, "@ngrx/store", "StoreModule")) {
|
|
1012
|
+
context.report({
|
|
1013
|
+
node: call,
|
|
1014
|
+
messageId: "avoidNgrxStoreForFeature"
|
|
1015
|
+
});
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
if (methodName === "forFeature" && isModuleCallFromImport(context, call, "@ngrx/effects", "EffectsModule")) {
|
|
1019
|
+
context.report({
|
|
1020
|
+
node: call,
|
|
1021
|
+
messageId: "avoidNgrxEffectsForFeature"
|
|
1022
|
+
});
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
if (methodName === "instrument" && isModuleCallFromImport(context, call, "@ngrx/store-devtools", "StoreDevtoolsModule")) {
|
|
1026
|
+
context.report({
|
|
1027
|
+
node: call,
|
|
1028
|
+
messageId: "avoidNgrxStoreDevtoolsInstrument"
|
|
1029
|
+
});
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@ngrx/router-store", "StoreRouterConnectingModule")) {
|
|
1033
|
+
context.report({
|
|
1034
|
+
node: call,
|
|
1035
|
+
messageId: "avoidNgrxRouterStoreForRoot"
|
|
1036
|
+
});
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
if (methodName === "forRoot" && isModuleCallFromImport(context, call, "@angular/router", "RouterModule") && allowForRouting) continue;
|
|
1040
|
+
if (!isMemberCall(call, "forRoot")) continue;
|
|
1097
1041
|
context.report({
|
|
1098
1042
|
node: call,
|
|
1099
|
-
messageId: "
|
|
1043
|
+
messageId: "avoidForRoot"
|
|
1100
1044
|
});
|
|
1101
1045
|
}
|
|
1046
|
+
const staticForRootMethod = getStaticForRootMethod(getNgModuleClassNode(node));
|
|
1047
|
+
if (staticForRootMethod) context.report({
|
|
1048
|
+
node: staticForRootMethod.key ?? staticForRootMethod,
|
|
1049
|
+
messageId: "avoidForRoot"
|
|
1050
|
+
});
|
|
1102
1051
|
}
|
|
1103
|
-
|
|
1052
|
+
if (!allowForRouting) for (const call of importCalls) {
|
|
1053
|
+
if (!isMemberCall(call, "forChild")) continue;
|
|
1054
|
+
if (!isModuleCallFromImport(context, call, "@angular/router", "RouterModule")) continue;
|
|
1055
|
+
context.report({
|
|
1056
|
+
node: call,
|
|
1057
|
+
messageId: "avoidRouterForChild"
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
} };
|
|
1104
1061
|
}
|
|
1105
1062
|
});
|
|
1106
1063
|
//#endregion
|
|
1107
1064
|
//#region src/rules/avoid-rxjs-state-in-component/index.ts
|
|
1108
|
-
const TARGET_DECORATORS$
|
|
1065
|
+
const TARGET_DECORATORS$2 = new Set(["Component", "Directive"]);
|
|
1109
1066
|
const SUBJECT_NAMES = new Set([
|
|
1110
1067
|
"BehaviorSubject",
|
|
1111
1068
|
"ReplaySubject",
|
|
@@ -1116,37 +1073,31 @@ const FIELD_NODE_TYPES$1 = new Set([
|
|
|
1116
1073
|
"FieldDefinition",
|
|
1117
1074
|
"PropertyDefinition"
|
|
1118
1075
|
]);
|
|
1119
|
-
function hasTargetDecorator$
|
|
1076
|
+
function hasTargetDecorator$3(context, classNode) {
|
|
1120
1077
|
if (!classNode || !Array.isArray(classNode.decorators)) return false;
|
|
1121
|
-
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator,
|
|
1078
|
+
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS$2));
|
|
1122
1079
|
}
|
|
1123
|
-
function getImportedSubjectKind(
|
|
1124
|
-
|
|
1080
|
+
function getImportedSubjectKind(context, node) {
|
|
1081
|
+
const importedName = getImportedName(context, node, "rxjs");
|
|
1082
|
+
return SUBJECT_NAMES.has(importedName) ? importedName : null;
|
|
1125
1083
|
}
|
|
1126
|
-
function getSubjectKindFromType(context, node
|
|
1084
|
+
function getSubjectKindFromType(context, node) {
|
|
1127
1085
|
if (!node) return null;
|
|
1128
1086
|
if (node.type !== "TSTypeReference") return null;
|
|
1129
|
-
|
|
1130
|
-
if (
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
if (!rxjsNamespaces.has(namespaceName)) return null;
|
|
1136
|
-
if (node.typeName?.type === "TSQualifiedName" && node.typeName.left?.type === "Identifier" && isShadowedIdentifier(context, node.typeName.left)) return null;
|
|
1137
|
-
return SUBJECT_NAMES.has(memberName) ? memberName : null;
|
|
1087
|
+
if (node.typeName?.type === "Identifier") return getImportedSubjectKind(context, node.typeName);
|
|
1088
|
+
if (node.typeName?.type === "TSQualifiedName" && node.typeName.left?.type === "Identifier") {
|
|
1089
|
+
const memberName = getPropertyName(node.typeName.right);
|
|
1090
|
+
return isNamespaceImport(context, node.typeName.left, "rxjs") && SUBJECT_NAMES.has(memberName) ? memberName : null;
|
|
1091
|
+
}
|
|
1092
|
+
return null;
|
|
1138
1093
|
}
|
|
1139
|
-
function getSubjectKindFromConstructor(context, node
|
|
1094
|
+
function getSubjectKindFromConstructor(context, node) {
|
|
1140
1095
|
const expression = unwrapExpression(node);
|
|
1141
1096
|
if (expression?.type !== "NewExpression") return null;
|
|
1142
1097
|
const callee = unwrapExpression(expression.callee);
|
|
1143
|
-
if (callee?.type === "Identifier")
|
|
1144
|
-
if (isShadowedIdentifier(context, callee)) return null;
|
|
1145
|
-
return getImportedSubjectKind(callee.name, subjectLocalNames);
|
|
1146
|
-
}
|
|
1098
|
+
if (callee?.type === "Identifier") return getImportedSubjectKind(context, callee);
|
|
1147
1099
|
if (callee?.type !== "MemberExpression") return null;
|
|
1148
|
-
if (callee.object?.type !== "Identifier" || !
|
|
1149
|
-
if (isShadowedIdentifier(context, callee.object)) return null;
|
|
1100
|
+
if (callee.object?.type !== "Identifier" || !isNamespaceImport(context, callee.object, "rxjs")) return null;
|
|
1150
1101
|
const memberName = getPropertyName(callee.property);
|
|
1151
1102
|
return SUBJECT_NAMES.has(memberName) ? memberName : null;
|
|
1152
1103
|
}
|
|
@@ -1205,14 +1156,14 @@ function getUsage(usages, fieldName) {
|
|
|
1205
1156
|
usages.set(fieldName, next);
|
|
1206
1157
|
return next;
|
|
1207
1158
|
}
|
|
1208
|
-
function collectSubjectFields(context, classBody
|
|
1159
|
+
function collectSubjectFields(context, classBody) {
|
|
1209
1160
|
const fields = /* @__PURE__ */ new Map();
|
|
1210
1161
|
for (const member of classBody.body ?? []) {
|
|
1211
1162
|
if (!FIELD_NODE_TYPES$1.has(member.type)) continue;
|
|
1212
1163
|
const fieldName = getMemberName(member.key);
|
|
1213
1164
|
if (!fieldName) continue;
|
|
1214
1165
|
const typeNode = member.typeAnnotation?.typeAnnotation;
|
|
1215
|
-
const kind = getSubjectKindFromConstructor(context, member.value
|
|
1166
|
+
const kind = getSubjectKindFromConstructor(context, member.value) ?? getSubjectKindFromType(context, typeNode);
|
|
1216
1167
|
if (!kind) continue;
|
|
1217
1168
|
fields.set(fieldName, {
|
|
1218
1169
|
key: member.key ?? member,
|
|
@@ -1260,58 +1211,28 @@ const avoidRxjsStateInComponent = defineRule({
|
|
|
1260
1211
|
}
|
|
1261
1212
|
},
|
|
1262
1213
|
createOnce(context) {
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
subjectLocalNames.clear();
|
|
1273
|
-
rxjsNamespaces.clear();
|
|
1274
|
-
decoratorImports.decoratorLocalNames.clear();
|
|
1275
|
-
decoratorImports.angularNamespaces.clear();
|
|
1276
|
-
},
|
|
1277
|
-
ImportDeclaration(node) {
|
|
1278
|
-
if (node.source?.value === "@angular/core") {
|
|
1279
|
-
for (const specifier of node.specifiers ?? []) addAngularCoreDecoratorImport(specifier, TARGET_DECORATORS$1, decoratorImports);
|
|
1280
|
-
return;
|
|
1281
|
-
}
|
|
1282
|
-
if (node.source?.value !== "rxjs") return;
|
|
1283
|
-
for (const specifier of node.specifiers ?? []) {
|
|
1284
|
-
if (specifier.type === "ImportSpecifier") {
|
|
1285
|
-
const importedName = getPropertyName(specifier.imported);
|
|
1286
|
-
if (SUBJECT_NAMES.has(importedName)) subjectLocalNames.set(specifier.local.name, importedName);
|
|
1287
|
-
continue;
|
|
1288
|
-
}
|
|
1289
|
-
if (specifier.type === "ImportNamespaceSpecifier") rxjsNamespaces.add(specifier.local.name);
|
|
1290
|
-
}
|
|
1291
|
-
},
|
|
1292
|
-
ClassBody(node) {
|
|
1293
|
-
const classBody = node;
|
|
1294
|
-
const classNode = classBody.parent;
|
|
1295
|
-
if (!hasTargetDecorator$1(context, classNode, decoratorImports)) return;
|
|
1296
|
-
const fields = collectSubjectFields(context, classBody, subjectLocalNames, rxjsNamespaces);
|
|
1297
|
-
const usages = collectFieldUsages(classBody, fields);
|
|
1298
|
-
for (const [fieldName, field] of fields) {
|
|
1299
|
-
const usage = usages.get(fieldName);
|
|
1300
|
-
if (field.kind === "Subject" && isDestroySubjectUsage(usage)) {
|
|
1301
|
-
context.report({
|
|
1302
|
-
node: field.key,
|
|
1303
|
-
messageId: "avoidDestroySubject"
|
|
1304
|
-
});
|
|
1305
|
-
continue;
|
|
1306
|
-
}
|
|
1214
|
+
return { ClassBody(node) {
|
|
1215
|
+
const classBody = node;
|
|
1216
|
+
const classNode = classBody.parent;
|
|
1217
|
+
if (!hasTargetDecorator$3(context, classNode)) return;
|
|
1218
|
+
const fields = collectSubjectFields(context, classBody);
|
|
1219
|
+
const usages = collectFieldUsages(classBody, fields);
|
|
1220
|
+
for (const [fieldName, field] of fields) {
|
|
1221
|
+
const usage = usages.get(fieldName);
|
|
1222
|
+
if (field.kind === "Subject" && isDestroySubjectUsage(usage)) {
|
|
1307
1223
|
context.report({
|
|
1308
1224
|
node: field.key,
|
|
1309
|
-
messageId: "
|
|
1310
|
-
data: { kind: field.kind }
|
|
1225
|
+
messageId: "avoidDestroySubject"
|
|
1311
1226
|
});
|
|
1227
|
+
continue;
|
|
1312
1228
|
}
|
|
1229
|
+
context.report({
|
|
1230
|
+
node: field.key,
|
|
1231
|
+
messageId: "avoidRxjsState",
|
|
1232
|
+
data: { kind: field.kind }
|
|
1233
|
+
});
|
|
1313
1234
|
}
|
|
1314
|
-
};
|
|
1235
|
+
} };
|
|
1315
1236
|
}
|
|
1316
1237
|
});
|
|
1317
1238
|
//#endregion
|
|
@@ -1328,22 +1249,21 @@ const KNOWN_SIGNAL_CREATION_FUNCTIONS = new Set([
|
|
|
1328
1249
|
]);
|
|
1329
1250
|
const LINKED_SIGNAL_CREATOR_NAME = "linkedSignal";
|
|
1330
1251
|
const COMPUTED_CREATOR_NAME = "computed";
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
function isSignalCreatorCall(context, node, signalCreatorNames, angularNamespaces) {
|
|
1252
|
+
const EFFECT_CREATOR_NAMES = new Set(["effect"]);
|
|
1253
|
+
const COMPUTED_CREATOR_NAMES = new Set([COMPUTED_CREATOR_NAME]);
|
|
1254
|
+
const LINKED_SIGNAL_CREATOR_NAMES = new Set([LINKED_SIGNAL_CREATOR_NAME]);
|
|
1255
|
+
function isSignalCreatorCall(context, node) {
|
|
1336
1256
|
if (node?.type !== "CallExpression") return false;
|
|
1337
1257
|
const callee = node.callee;
|
|
1338
|
-
return
|
|
1258
|
+
return isImportedReference(context, callee, "@angular/core", KNOWN_SIGNAL_CREATION_FUNCTIONS) || isImportedNamespaceMember(context, callee, "@angular/core", KNOWN_SIGNAL_CREATION_FUNCTIONS);
|
|
1339
1259
|
}
|
|
1340
|
-
function isEffectCall(context, node
|
|
1260
|
+
function isEffectCall(context, node) {
|
|
1341
1261
|
const callee = node.callee;
|
|
1342
|
-
return callee
|
|
1262
|
+
return isImportedReference(context, callee, "@angular/core", EFFECT_CREATOR_NAMES) || isImportedNamespaceMember(context, callee, "@angular/core", EFFECT_CREATOR_NAMES);
|
|
1343
1263
|
}
|
|
1344
|
-
function isReactiveCreatorCall(context, node, creatorNames
|
|
1264
|
+
function isReactiveCreatorCall(context, node, creatorNames) {
|
|
1345
1265
|
const callee = node.callee;
|
|
1346
|
-
return callee
|
|
1266
|
+
return isImportedReference(context, callee, "@angular/core", creatorNames) || isImportedNamespaceMember(context, callee, "@angular/core", creatorNames);
|
|
1347
1267
|
}
|
|
1348
1268
|
function isKnownSignalObject(context, objectNode, signalVariableBindings, classSignalProperties) {
|
|
1349
1269
|
if (!objectNode) return false;
|
|
@@ -1398,40 +1318,17 @@ const avoidWritingSignalsInReactiveContext = defineRule({
|
|
|
1398
1318
|
messages: { avoidSignalWriteInReactiveContext: "Avoid setting signal values inside {{contextName}}; move writes outside reactive derivations and effects." }
|
|
1399
1319
|
},
|
|
1400
1320
|
createOnce(context) {
|
|
1401
|
-
const effectNames = /* @__PURE__ */ new Set();
|
|
1402
|
-
const computedNames = /* @__PURE__ */ new Set();
|
|
1403
|
-
const linkedSignalNames = /* @__PURE__ */ new Set();
|
|
1404
|
-
const signalCreatorNames = /* @__PURE__ */ new Set();
|
|
1405
|
-
const angularNamespaces = /* @__PURE__ */ new Set();
|
|
1406
1321
|
const signalVariableBindings = /* @__PURE__ */ new Map();
|
|
1407
1322
|
const classSignalProperties = /* @__PURE__ */ new Set();
|
|
1408
1323
|
return {
|
|
1409
1324
|
before() {
|
|
1410
|
-
effectNames.clear();
|
|
1411
|
-
computedNames.clear();
|
|
1412
|
-
linkedSignalNames.clear();
|
|
1413
|
-
signalCreatorNames.clear();
|
|
1414
|
-
angularNamespaces.clear();
|
|
1415
1325
|
signalVariableBindings.clear();
|
|
1416
1326
|
classSignalProperties.clear();
|
|
1417
1327
|
},
|
|
1418
|
-
ImportDeclaration(node) {
|
|
1419
|
-
if (node.source?.value !== "@angular/core") return;
|
|
1420
|
-
for (const specifier of node.specifiers ?? []) {
|
|
1421
|
-
if (specifier.type === "ImportSpecifier") {
|
|
1422
|
-
const importedName = getPropertyName(specifier.imported);
|
|
1423
|
-
if (importedName === EFFECT_CREATOR_NAME) effectNames.add(specifier.local.name);
|
|
1424
|
-
if (importedName === COMPUTED_CREATOR_NAME) computedNames.add(specifier.local.name);
|
|
1425
|
-
if (importedName === LINKED_SIGNAL_CREATOR_NAME) linkedSignalNames.add(specifier.local.name);
|
|
1426
|
-
if (importedName && KNOWN_SIGNAL_CREATION_FUNCTIONS.has(importedName)) signalCreatorNames.add(specifier.local.name);
|
|
1427
|
-
}
|
|
1428
|
-
if (specifier.type === "ImportNamespaceSpecifier") angularNamespaces.add(specifier.local.name);
|
|
1429
|
-
}
|
|
1430
|
-
},
|
|
1431
1328
|
VariableDeclarator(node) {
|
|
1432
1329
|
const declarator = node;
|
|
1433
1330
|
if (declarator.id?.type !== "Identifier") return;
|
|
1434
|
-
if (!isSignalCreatorCall(context, declarator.init
|
|
1331
|
+
if (!isSignalCreatorCall(context, declarator.init)) return;
|
|
1435
1332
|
const bindings = signalVariableBindings.get(declarator.id.name) ?? /* @__PURE__ */ new Set();
|
|
1436
1333
|
bindings.add(declarator.id);
|
|
1437
1334
|
signalVariableBindings.set(declarator.id.name, bindings);
|
|
@@ -1439,7 +1336,7 @@ const avoidWritingSignalsInReactiveContext = defineRule({
|
|
|
1439
1336
|
"PropertyDefinition, FieldDefinition, AccessorProperty"(node) {
|
|
1440
1337
|
const property = node;
|
|
1441
1338
|
if (property.key?.type !== "Identifier") return;
|
|
1442
|
-
if (!isSignalCreatorCall(context, property.value
|
|
1339
|
+
if (!isSignalCreatorCall(context, property.value)) return;
|
|
1443
1340
|
classSignalProperties.add(property.key.name);
|
|
1444
1341
|
},
|
|
1445
1342
|
CallExpression(node) {
|
|
@@ -1448,21 +1345,21 @@ const avoidWritingSignalsInReactiveContext = defineRule({
|
|
|
1448
1345
|
const allowComputedAndLinkedSignals = options.allowComputedAndLinkedSignals ?? false;
|
|
1449
1346
|
const callNode = node;
|
|
1450
1347
|
const callbackCandidates = [];
|
|
1451
|
-
if (!allowEffects && isEffectCall(context, callNode
|
|
1348
|
+
if (!allowEffects && isEffectCall(context, callNode)) {
|
|
1452
1349
|
const callback = callNode.arguments?.[0];
|
|
1453
1350
|
if (callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression") callbackCandidates.push({
|
|
1454
1351
|
callback,
|
|
1455
1352
|
contextName: "effect()"
|
|
1456
1353
|
});
|
|
1457
1354
|
}
|
|
1458
|
-
if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(context, callNode,
|
|
1355
|
+
if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(context, callNode, COMPUTED_CREATOR_NAMES)) {
|
|
1459
1356
|
const callback = callNode.arguments?.[0];
|
|
1460
1357
|
if (callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression") callbackCandidates.push({
|
|
1461
1358
|
callback,
|
|
1462
1359
|
contextName: "computed()"
|
|
1463
1360
|
});
|
|
1464
1361
|
}
|
|
1465
|
-
if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(context, callNode,
|
|
1362
|
+
if (!allowComputedAndLinkedSignals && isReactiveCreatorCall(context, callNode, LINKED_SIGNAL_CREATOR_NAMES)) for (const argumentNode of callNode.arguments ?? []) {
|
|
1466
1363
|
const argument = argumentNode;
|
|
1467
1364
|
if (argument.type === "ArrowFunctionExpression" || argument.type === "FunctionExpression") {
|
|
1468
1365
|
callbackCandidates.push({
|
|
@@ -1498,11 +1395,6 @@ const avoidWritingSignalsInReactiveContext = defineRule({
|
|
|
1498
1395
|
}, true);
|
|
1499
1396
|
},
|
|
1500
1397
|
after() {
|
|
1501
|
-
effectNames.clear();
|
|
1502
|
-
computedNames.clear();
|
|
1503
|
-
linkedSignalNames.clear();
|
|
1504
|
-
signalCreatorNames.clear();
|
|
1505
|
-
angularNamespaces.clear();
|
|
1506
1398
|
signalVariableBindings.clear();
|
|
1507
1399
|
classSignalProperties.clear();
|
|
1508
1400
|
}
|
|
@@ -1510,64 +1402,145 @@ const avoidWritingSignalsInReactiveContext = defineRule({
|
|
|
1510
1402
|
}
|
|
1511
1403
|
});
|
|
1512
1404
|
//#endregion
|
|
1513
|
-
//#region src/rules/
|
|
1514
|
-
const
|
|
1405
|
+
//#region src/rules/class-matches-filename/index.ts
|
|
1406
|
+
const CLASS_FILENAME_MATCHERS = [
|
|
1407
|
+
{
|
|
1408
|
+
pattern: /^(.*)\.component\.ts$/u,
|
|
1409
|
+
decoratorNames: ["Component"]
|
|
1410
|
+
},
|
|
1411
|
+
{
|
|
1412
|
+
pattern: /^(.*)\.directive\.ts$/u,
|
|
1413
|
+
decoratorNames: ["Directive"]
|
|
1414
|
+
},
|
|
1415
|
+
{
|
|
1416
|
+
pattern: /^(.*)\.service\.ts$/u,
|
|
1417
|
+
decoratorNames: ["Service", "Injectable"]
|
|
1418
|
+
}
|
|
1419
|
+
];
|
|
1515
1420
|
function toPascalCase(raw) {
|
|
1516
1421
|
return raw.split(/[-_\s.]+/u).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1517
1422
|
}
|
|
1518
|
-
function
|
|
1423
|
+
function getClassKind(suffix) {
|
|
1424
|
+
return suffix.toLowerCase();
|
|
1425
|
+
}
|
|
1426
|
+
function getExpectedClassNames(filename, ignoreClassSuffix) {
|
|
1519
1427
|
const base = filename.split(/[/\\]/u).at(-1) ?? "";
|
|
1520
|
-
const
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1428
|
+
for (const { decoratorNames, pattern } of CLASS_FILENAME_MATCHERS) {
|
|
1429
|
+
const match = pattern.exec(base);
|
|
1430
|
+
if (!match) continue;
|
|
1431
|
+
const stem = match[1];
|
|
1432
|
+
const pascal = toPascalCase(stem);
|
|
1433
|
+
if (!pascal) return null;
|
|
1434
|
+
const suffix = decoratorNames[0];
|
|
1435
|
+
return ignoreClassSuffix ? [`${pascal}${suffix}`, pascal] : [`${pascal}${suffix}`];
|
|
1436
|
+
}
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
function getFileMatcher(filename) {
|
|
1440
|
+
const base = filename.split(/[/\\]/u).at(-1) ?? "";
|
|
1441
|
+
return CLASS_FILENAME_MATCHERS.find(({ pattern }) => pattern.test(base)) ?? null;
|
|
1442
|
+
}
|
|
1443
|
+
function getIgnoreClassSuffixOption(options, kind) {
|
|
1444
|
+
const ignoreClassSuffix = options.ignoreClassSuffix ?? false;
|
|
1445
|
+
if (typeof ignoreClassSuffix === "boolean") return ignoreClassSuffix;
|
|
1446
|
+
return ignoreClassSuffix[kind] ?? false;
|
|
1447
|
+
}
|
|
1448
|
+
function getFileKind(matcher) {
|
|
1449
|
+
return getClassKind(matcher.decoratorNames[0]);
|
|
1525
1450
|
}
|
|
1526
|
-
|
|
1451
|
+
function formatExpectedNames(expectedNames) {
|
|
1452
|
+
return expectedNames.join("' or '");
|
|
1453
|
+
}
|
|
1454
|
+
function getNodeName(node) {
|
|
1455
|
+
return node.id?.name ?? null;
|
|
1456
|
+
}
|
|
1457
|
+
function getBaseFilename$1(filename) {
|
|
1458
|
+
return filename.split(/[/\\]/u).at(-1) ?? filename;
|
|
1459
|
+
}
|
|
1460
|
+
function hasTargetDecorator$2(context, classNode, decoratorNames) {
|
|
1461
|
+
if (!Array.isArray(classNode.decorators)) return false;
|
|
1462
|
+
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorNames));
|
|
1463
|
+
}
|
|
1464
|
+
function getDecoratedClassCountDescription(count) {
|
|
1465
|
+
return count === 0 ? "no decorated classes" : `${count} decorated classes`;
|
|
1466
|
+
}
|
|
1467
|
+
const classMatchesFilename = defineRule({
|
|
1527
1468
|
meta: {
|
|
1528
1469
|
type: "suggestion",
|
|
1529
1470
|
docs: {
|
|
1530
|
-
description: "Require
|
|
1471
|
+
description: "Require Angular component, directive, and service class names to match their filenames.",
|
|
1531
1472
|
recommended: true
|
|
1532
1473
|
},
|
|
1533
|
-
schema: [
|
|
1534
|
-
|
|
1474
|
+
schema: [{
|
|
1475
|
+
type: "object",
|
|
1476
|
+
additionalProperties: false,
|
|
1477
|
+
properties: { ignoreClassSuffix: {
|
|
1478
|
+
anyOf: [{ type: "boolean" }, {
|
|
1479
|
+
type: "object",
|
|
1480
|
+
additionalProperties: false,
|
|
1481
|
+
properties: {
|
|
1482
|
+
component: { type: "boolean" },
|
|
1483
|
+
directive: { type: "boolean" },
|
|
1484
|
+
service: { type: "boolean" }
|
|
1485
|
+
}
|
|
1486
|
+
}],
|
|
1487
|
+
default: false
|
|
1488
|
+
} }
|
|
1489
|
+
}],
|
|
1490
|
+
messages: {
|
|
1491
|
+
classNameMismatch: "Angular class name should be '{{expectedName}}' to match the filename '{{filename}}'.",
|
|
1492
|
+
decoratedClassCount: "Angular filename '{{filename}}' should contain exactly one matching decorated class, but found {{actual}}."
|
|
1493
|
+
}
|
|
1535
1494
|
},
|
|
1536
1495
|
createOnce(context) {
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
angularNamespaces: /* @__PURE__ */ new Set()
|
|
1542
|
-
};
|
|
1496
|
+
let programNode = null;
|
|
1497
|
+
let fileMatcher = null;
|
|
1498
|
+
let fileDecoratorNames = /* @__PURE__ */ new Set();
|
|
1499
|
+
const decoratedClasses = [];
|
|
1543
1500
|
return {
|
|
1544
1501
|
before() {
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1502
|
+
programNode = null;
|
|
1503
|
+
decoratedClasses.length = 0;
|
|
1504
|
+
fileMatcher = getFileMatcher(context.filename ?? "");
|
|
1505
|
+
if (!fileMatcher) return false;
|
|
1506
|
+
fileDecoratorNames = new Set(fileMatcher.decoratorNames);
|
|
1548
1507
|
},
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
for (const specifier of node.specifiers ?? []) addAngularCoreDecoratorImport(specifier, COMPONENT_DECORATORS$1, decoratorImports);
|
|
1508
|
+
Program(node) {
|
|
1509
|
+
programNode = node;
|
|
1552
1510
|
},
|
|
1553
1511
|
ClassDeclaration(node) {
|
|
1512
|
+
if (!fileMatcher) return;
|
|
1554
1513
|
const classNode = node;
|
|
1555
|
-
if (
|
|
1556
|
-
if (classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, decoratorImports))) componentClasses.push(classNode);
|
|
1514
|
+
if (hasTargetDecorator$2(context, classNode, fileDecoratorNames)) decoratedClasses.push(classNode);
|
|
1557
1515
|
},
|
|
1558
1516
|
after() {
|
|
1559
1517
|
const filename = context.filename ?? "";
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1518
|
+
if (!fileMatcher) return;
|
|
1519
|
+
const baseFilename = getBaseFilename$1(filename);
|
|
1520
|
+
if (decoratedClasses.length !== 1) {
|
|
1521
|
+
const reportNode = decoratedClasses[0]?.id ?? decoratedClasses[0] ?? programNode;
|
|
1522
|
+
if (!reportNode) return;
|
|
1523
|
+
context.report({
|
|
1524
|
+
node: reportNode,
|
|
1525
|
+
messageId: "decoratedClassCount",
|
|
1526
|
+
data: {
|
|
1527
|
+
actual: getDecoratedClassCountDescription(decoratedClasses.length),
|
|
1528
|
+
filename: baseFilename
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
const expectedNames = getExpectedClassNames(filename, getIgnoreClassSuffixOption(context.options[0] ?? {}, getFileKind(fileMatcher)));
|
|
1534
|
+
if (!expectedNames) return;
|
|
1535
|
+
const classNode = decoratedClasses[0];
|
|
1536
|
+
const className = getNodeName(classNode);
|
|
1537
|
+
if (!className) return;
|
|
1538
|
+
if (expectedNames.includes(className)) return;
|
|
1566
1539
|
context.report({
|
|
1567
1540
|
node: classNode.id,
|
|
1568
1541
|
messageId: "classNameMismatch",
|
|
1569
1542
|
data: {
|
|
1570
|
-
expectedName,
|
|
1543
|
+
expectedName: formatExpectedNames(expectedNames),
|
|
1571
1544
|
filename: baseFilename
|
|
1572
1545
|
}
|
|
1573
1546
|
});
|
|
@@ -1577,7 +1550,7 @@ const componentClassMatchesFilename = defineRule({
|
|
|
1577
1550
|
});
|
|
1578
1551
|
//#endregion
|
|
1579
1552
|
//#region src/rules/component-resource-filenames/index.ts
|
|
1580
|
-
const COMPONENT_DECORATORS = new Set(["Component"]);
|
|
1553
|
+
const COMPONENT_DECORATORS$1 = new Set(["Component"]);
|
|
1581
1554
|
const STYLE_EXTENSIONS = new Set([
|
|
1582
1555
|
"css",
|
|
1583
1556
|
"less",
|
|
@@ -1609,9 +1582,9 @@ function getStaticString(node) {
|
|
|
1609
1582
|
if (node.type === "TemplateLiteral" && node.expressions?.length === 0) return { value: node.quasis?.[0]?.value?.cooked ?? node.quasis?.[0]?.value?.raw ?? "" };
|
|
1610
1583
|
return null;
|
|
1611
1584
|
}
|
|
1612
|
-
function getComponentMetadata(context, node
|
|
1585
|
+
function getComponentMetadata(context, node) {
|
|
1613
1586
|
if (node.type !== "Decorator") return null;
|
|
1614
|
-
if (!isAngularCoreDecorator(context, node,
|
|
1587
|
+
if (!isAngularCoreDecorator(context, node, COMPONENT_DECORATORS$1)) return null;
|
|
1615
1588
|
const expression = node.expression;
|
|
1616
1589
|
if (expression?.type !== "CallExpression") return null;
|
|
1617
1590
|
const metadata = expression.arguments?.[0];
|
|
@@ -1632,13 +1605,8 @@ const componentResourceFilenames = defineRule({
|
|
|
1632
1605
|
}
|
|
1633
1606
|
},
|
|
1634
1607
|
createOnce(context) {
|
|
1635
|
-
|
|
1636
|
-
decoratorNames: COMPONENT_DECORATORS,
|
|
1637
|
-
decoratorLocalNames: /* @__PURE__ */ new Set(),
|
|
1638
|
-
angularNamespaces: /* @__PURE__ */ new Set()
|
|
1639
|
-
};
|
|
1608
|
+
let expectedStem = null;
|
|
1640
1609
|
function reportTemplateUrl(valueNode) {
|
|
1641
|
-
const expectedStem = getExpectedStem(context.filename ?? "");
|
|
1642
1610
|
if (!expectedStem) return;
|
|
1643
1611
|
const staticString = getStaticString(valueNode);
|
|
1644
1612
|
if (!staticString) return;
|
|
@@ -1651,7 +1619,6 @@ const componentResourceFilenames = defineRule({
|
|
|
1651
1619
|
});
|
|
1652
1620
|
}
|
|
1653
1621
|
function reportStyleUrl(valueNode) {
|
|
1654
|
-
const expectedStem = getExpectedStem(context.filename ?? "");
|
|
1655
1622
|
if (!expectedStem) return;
|
|
1656
1623
|
const staticString = getStaticString(valueNode);
|
|
1657
1624
|
if (!staticString) return;
|
|
@@ -1673,15 +1640,11 @@ const componentResourceFilenames = defineRule({
|
|
|
1673
1640
|
}
|
|
1674
1641
|
return {
|
|
1675
1642
|
before() {
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
},
|
|
1679
|
-
ImportDeclaration(node) {
|
|
1680
|
-
if (node.source?.value !== "@angular/core") return;
|
|
1681
|
-
for (const specifier of node.specifiers ?? []) addAngularCoreDecoratorImport(specifier, COMPONENT_DECORATORS, decoratorImports);
|
|
1643
|
+
expectedStem = getExpectedStem(context.filename ?? "");
|
|
1644
|
+
if (!expectedStem) return false;
|
|
1682
1645
|
},
|
|
1683
1646
|
Decorator(node) {
|
|
1684
|
-
const metadata = getComponentMetadata(context, node
|
|
1647
|
+
const metadata = getComponentMetadata(context, node);
|
|
1685
1648
|
if (!metadata) return;
|
|
1686
1649
|
for (const property of metadata.properties ?? []) {
|
|
1687
1650
|
if (property.type !== "Property" || property.computed) continue;
|
|
@@ -1700,34 +1663,286 @@ const componentResourceFilenames = defineRule({
|
|
|
1700
1663
|
}
|
|
1701
1664
|
});
|
|
1702
1665
|
//#endregion
|
|
1666
|
+
//#region src/rules/decorator-filename-suffix/index.ts
|
|
1667
|
+
const FILENAME_SUFFIX_CONVENTIONS = [
|
|
1668
|
+
{
|
|
1669
|
+
kind: "component",
|
|
1670
|
+
suffix: ".component.ts",
|
|
1671
|
+
decoratorNames: new Set(["Component"])
|
|
1672
|
+
},
|
|
1673
|
+
{
|
|
1674
|
+
kind: "directive",
|
|
1675
|
+
suffix: ".directive.ts",
|
|
1676
|
+
decoratorNames: new Set(["Directive"])
|
|
1677
|
+
},
|
|
1678
|
+
{
|
|
1679
|
+
kind: "service",
|
|
1680
|
+
suffix: ".service.ts",
|
|
1681
|
+
decoratorNames: new Set(["Service", "Injectable"])
|
|
1682
|
+
}
|
|
1683
|
+
];
|
|
1684
|
+
function getBaseFilename(filename) {
|
|
1685
|
+
return filename.split(/[/\\]/u).at(-1) ?? filename;
|
|
1686
|
+
}
|
|
1687
|
+
function isTypeScriptFile(filename) {
|
|
1688
|
+
return getBaseFilename(filename).endsWith(".ts");
|
|
1689
|
+
}
|
|
1690
|
+
function getMatchingConvention(context, decorator) {
|
|
1691
|
+
return FILENAME_SUFFIX_CONVENTIONS.find((convention) => isAngularCoreDecorator(context, decorator, convention.decoratorNames)) ?? null;
|
|
1692
|
+
}
|
|
1693
|
+
const decoratorFilenameSuffix = defineRule({
|
|
1694
|
+
meta: {
|
|
1695
|
+
type: "suggestion",
|
|
1696
|
+
docs: {
|
|
1697
|
+
description: "Require Angular component, directive, and service decorators to be declared in files with matching suffixes.",
|
|
1698
|
+
recommended: true
|
|
1699
|
+
},
|
|
1700
|
+
schema: [],
|
|
1701
|
+
messages: { filenameSuffix: "Angular {{kind}} decorator should be declared in a file ending with '{{suffix}}'." }
|
|
1702
|
+
},
|
|
1703
|
+
createOnce(context) {
|
|
1704
|
+
return {
|
|
1705
|
+
before() {
|
|
1706
|
+
if (!isTypeScriptFile(context.filename ?? "")) return false;
|
|
1707
|
+
},
|
|
1708
|
+
Decorator(node) {
|
|
1709
|
+
const convention = getMatchingConvention(context, node);
|
|
1710
|
+
if (!convention) return;
|
|
1711
|
+
if (getBaseFilename(context.filename ?? "").endsWith(convention.suffix)) return;
|
|
1712
|
+
context.report({
|
|
1713
|
+
node,
|
|
1714
|
+
messageId: "filenameSuffix",
|
|
1715
|
+
data: {
|
|
1716
|
+
kind: convention.kind,
|
|
1717
|
+
suffix: convention.suffix
|
|
1718
|
+
}
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
//#endregion
|
|
1725
|
+
//#region src/rules/injects-tanstack-query-only-in-component-body/index.ts
|
|
1726
|
+
const TARGET_DECORATORS$1 = new Set(["Component", "Directive"]);
|
|
1727
|
+
const TANSTACK_QUERY_APIS = new Set(["injectQuery", "injectMutation"]);
|
|
1728
|
+
const TANSTACK_QUERY_SOURCES = new Set(["@tanstack/angular-query", "@benjavicente/angular-query"]);
|
|
1729
|
+
const CLASS_FIELD_TYPES$1 = new Set([
|
|
1730
|
+
"AccessorProperty",
|
|
1731
|
+
"FieldDefinition",
|
|
1732
|
+
"PropertyDefinition"
|
|
1733
|
+
]);
|
|
1734
|
+
const FUNCTION_TYPES$1 = new Set([
|
|
1735
|
+
"ArrowFunctionExpression",
|
|
1736
|
+
"FunctionDeclaration",
|
|
1737
|
+
"FunctionExpression"
|
|
1738
|
+
]);
|
|
1739
|
+
function hasTargetDecorator$1(context, classNode) {
|
|
1740
|
+
if (!classNode || !Array.isArray(classNode.decorators)) return false;
|
|
1741
|
+
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS$1));
|
|
1742
|
+
}
|
|
1743
|
+
function isTanstackQueryInjectCall(context, callNode) {
|
|
1744
|
+
return [...TANSTACK_QUERY_SOURCES].some((source) => isImportedReference(context, callNode.callee, source, TANSTACK_QUERY_APIS));
|
|
1745
|
+
}
|
|
1746
|
+
function isDirectComponentOrDirectiveFieldInitializer(context, callNode) {
|
|
1747
|
+
const ancestors = context.sourceCode.getAncestors(callNode);
|
|
1748
|
+
const classField = ancestors.findLast((ancestor) => CLASS_FIELD_TYPES$1.has(ancestor.type));
|
|
1749
|
+
if (!classField || unwrapExpression(classField.value) !== callNode) return false;
|
|
1750
|
+
const classBodyIndex = ancestors.findLastIndex((ancestor) => ancestor.type === "ClassBody");
|
|
1751
|
+
if (classBodyIndex === -1) return false;
|
|
1752
|
+
const classBody = ancestors[classBodyIndex];
|
|
1753
|
+
if (ancestors.slice(classBodyIndex + 1).some((ancestor) => FUNCTION_TYPES$1.has(ancestor.type))) return false;
|
|
1754
|
+
return hasTargetDecorator$1(context, classBody?.parent);
|
|
1755
|
+
}
|
|
1756
|
+
const injectsTanstackQueryOnlyInComponentBody = defineRule({
|
|
1757
|
+
meta: {
|
|
1758
|
+
type: "problem",
|
|
1759
|
+
docs: {
|
|
1760
|
+
description: "Require Angular TanStack Query injectQuery/injectMutation calls to be direct component/directive class fields.",
|
|
1761
|
+
recommended: true
|
|
1762
|
+
},
|
|
1763
|
+
schema: [],
|
|
1764
|
+
messages: { onlyInComponentBody: "Call {{name}} only as a direct class field initializer in an Angular component or directive." }
|
|
1765
|
+
},
|
|
1766
|
+
createOnce(context) {
|
|
1767
|
+
return { CallExpression(node) {
|
|
1768
|
+
const callNode = node;
|
|
1769
|
+
if (!isTanstackQueryInjectCall(context, callNode)) return;
|
|
1770
|
+
if (isDirectComponentOrDirectiveFieldInitializer(context, callNode)) return;
|
|
1771
|
+
context.report({
|
|
1772
|
+
node: callNode.callee ?? callNode,
|
|
1773
|
+
messageId: "onlyInComponentBody",
|
|
1774
|
+
data: { name: callNode.callee?.name ?? "this TanStack Query inject API" }
|
|
1775
|
+
});
|
|
1776
|
+
} };
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
//#endregion
|
|
1780
|
+
//#region src/rules/no-resource-api/index.ts
|
|
1781
|
+
const RESOURCE_APIS = new Set(["resource"]);
|
|
1782
|
+
const RXJS_RESOURCE_APIS = new Set(["rxResource"]);
|
|
1783
|
+
const HTTP_RESOURCE_APIS = new Set(["httpResource"]);
|
|
1784
|
+
function isResourceApiCall(context, callNode) {
|
|
1785
|
+
const callee = callNode.callee;
|
|
1786
|
+
if (isImportedReference(context, callee, "@angular/core", RESOURCE_APIS)) return true;
|
|
1787
|
+
if (isImportedNamespaceMember(context, callee, "@angular/core", RESOURCE_APIS)) return true;
|
|
1788
|
+
if (isImportedReference(context, callee, "@angular/core/rxjs-interop", RXJS_RESOURCE_APIS)) return true;
|
|
1789
|
+
if (isImportedNamespaceMember(context, callee, "@angular/core/rxjs-interop", RXJS_RESOURCE_APIS)) return true;
|
|
1790
|
+
if (isImportedReference(context, callee, "@angular/common/http", HTTP_RESOURCE_APIS)) return true;
|
|
1791
|
+
if (isImportedNamespaceMember(context, callee, "@angular/common/http", HTTP_RESOURCE_APIS)) return true;
|
|
1792
|
+
if (callee?.type === "MemberExpression" && callee.object?.type === "MemberExpression" && callee.object.object?.type === "Identifier" && isNamespaceImport(context, callee.object.object, "@angular/common/http") && HTTP_RESOURCE_APIS.has(getPropertyName(callee.object.property) ?? "")) return true;
|
|
1793
|
+
return callee?.type === "MemberExpression" && isImportedReference(context, callee.object, "@angular/common/http", HTTP_RESOURCE_APIS);
|
|
1794
|
+
}
|
|
1795
|
+
const noResourceApi = defineRule({
|
|
1796
|
+
meta: {
|
|
1797
|
+
type: "suggestion",
|
|
1798
|
+
docs: {
|
|
1799
|
+
description: "Disallow Angular resource APIs in favor of a dedicated server-state library.",
|
|
1800
|
+
recommended: true
|
|
1801
|
+
},
|
|
1802
|
+
schema: [],
|
|
1803
|
+
messages: { noResourceApi: "Avoid Angular resource APIs for server state. Prefer a dedicated server-state helper such as TanStack Query." }
|
|
1804
|
+
},
|
|
1805
|
+
createOnce(context) {
|
|
1806
|
+
return { CallExpression(node) {
|
|
1807
|
+
const callNode = node;
|
|
1808
|
+
if (!isResourceApiCall(context, callNode)) return;
|
|
1809
|
+
context.report({
|
|
1810
|
+
node: callNode.callee ?? callNode,
|
|
1811
|
+
messageId: "noResourceApi"
|
|
1812
|
+
});
|
|
1813
|
+
} };
|
|
1814
|
+
}
|
|
1815
|
+
});
|
|
1816
|
+
//#endregion
|
|
1817
|
+
//#region src/rules/no-route-resolvers/index.ts
|
|
1818
|
+
const ROUTE_TYPE_NAMES$1 = new Set(["Route"]);
|
|
1819
|
+
const ROUTES_TYPE_NAMES$1 = new Set(["Routes"]);
|
|
1820
|
+
function isImportedTypeName$1(context, typeNode, importedNames) {
|
|
1821
|
+
if (typeNode?.type === "Identifier") {
|
|
1822
|
+
const importedName = getImportedName(context, typeNode, "@angular/router");
|
|
1823
|
+
return !!importedName && importedNames.has(importedName);
|
|
1824
|
+
}
|
|
1825
|
+
return typeNode?.type === "TSQualifiedName" && typeNode.left?.type === "Identifier" && isNamespaceImport(context, typeNode.left, "@angular/router") && importedNames.has(getPropertyName(typeNode.right) ?? "");
|
|
1826
|
+
}
|
|
1827
|
+
function getTypeParameterNodes$1(typeNode) {
|
|
1828
|
+
return typeNode.typeParameters?.params ?? typeNode.typeArguments?.params ?? [];
|
|
1829
|
+
}
|
|
1830
|
+
function isRouteType$1(context, typeNode) {
|
|
1831
|
+
return typeNode?.type === "TSTypeReference" && isImportedTypeName$1(context, typeNode.typeName, ROUTE_TYPE_NAMES$1);
|
|
1832
|
+
}
|
|
1833
|
+
function isRouteArrayType$1(context, typeNode) {
|
|
1834
|
+
if (!typeNode) return false;
|
|
1835
|
+
if (typeNode.type === "TSTypeReference" && isImportedTypeName$1(context, typeNode.typeName, ROUTES_TYPE_NAMES$1)) return true;
|
|
1836
|
+
if (typeNode.type === "TSArrayType") return isRouteType$1(context, typeNode.elementType);
|
|
1837
|
+
if (typeNode.type !== "TSTypeReference") return false;
|
|
1838
|
+
if (getPropertyName(typeNode.typeName) !== "Array" && getPropertyName(typeNode.typeName) !== "ReadonlyArray") return false;
|
|
1839
|
+
const [elementType] = getTypeParameterNodes$1(typeNode);
|
|
1840
|
+
return isRouteType$1(context, elementType);
|
|
1841
|
+
}
|
|
1842
|
+
function getTypeAnnotation(node) {
|
|
1843
|
+
const typeAnnotation = node?.typeAnnotation;
|
|
1844
|
+
return typeAnnotation?.type === "TSTypeAnnotation" ? typeAnnotation.typeAnnotation : null;
|
|
1845
|
+
}
|
|
1846
|
+
const noRouteResolvers = defineRule({
|
|
1847
|
+
meta: {
|
|
1848
|
+
type: "suggestion",
|
|
1849
|
+
docs: {
|
|
1850
|
+
description: "Disallow Angular route resolvers in typed Route/Routes declarations.",
|
|
1851
|
+
recommended: true
|
|
1852
|
+
},
|
|
1853
|
+
schema: [],
|
|
1854
|
+
messages: { noRouteResolvers: "Avoid Angular route resolvers for data loading. Prefer component-level loading with signals or server-state helpers." }
|
|
1855
|
+
},
|
|
1856
|
+
createOnce(context) {
|
|
1857
|
+
function reportResolveInRouteObject(routeObject) {
|
|
1858
|
+
for (const property of routeObject.properties ?? []) {
|
|
1859
|
+
if (property.type !== "Property" || property.computed) continue;
|
|
1860
|
+
const propertyName = getPropertyName(property.key);
|
|
1861
|
+
if (propertyName === "resolve") {
|
|
1862
|
+
context.report({
|
|
1863
|
+
node: property.key ?? property,
|
|
1864
|
+
messageId: "noRouteResolvers"
|
|
1865
|
+
});
|
|
1866
|
+
continue;
|
|
1867
|
+
}
|
|
1868
|
+
if (propertyName === "children" && property.value?.type === "ArrayExpression") reportResolveInRouteArray(property.value);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
function reportResolveInRouteArray(routeArray) {
|
|
1872
|
+
for (const element of routeArray.elements ?? []) if (element?.type === "ObjectExpression") reportResolveInRouteObject(element);
|
|
1873
|
+
}
|
|
1874
|
+
return { VariableDeclarator(node) {
|
|
1875
|
+
const declarator = node;
|
|
1876
|
+
const typeNode = getTypeAnnotation(declarator.id);
|
|
1877
|
+
if (isRouteArrayType$1(context, typeNode) && declarator.init?.type === "ArrayExpression") {
|
|
1878
|
+
reportResolveInRouteArray(declarator.init);
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
if (isRouteType$1(context, typeNode) && declarator.init?.type === "ObjectExpression") reportResolveInRouteObject(declarator.init);
|
|
1882
|
+
} };
|
|
1883
|
+
}
|
|
1884
|
+
});
|
|
1885
|
+
//#endregion
|
|
1886
|
+
//#region src/rules/no-ui-inheritance/index.ts
|
|
1887
|
+
const UI_DECORATORS = new Set(["Component", "Directive"]);
|
|
1888
|
+
function hasUiDecorator(context, classNode) {
|
|
1889
|
+
if (!Array.isArray(classNode.decorators)) return false;
|
|
1890
|
+
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, UI_DECORATORS));
|
|
1891
|
+
}
|
|
1892
|
+
const noUiInheritance = defineRule({
|
|
1893
|
+
meta: {
|
|
1894
|
+
type: "suggestion",
|
|
1895
|
+
docs: {
|
|
1896
|
+
description: "Disallow inheritance for Angular components and directives.",
|
|
1897
|
+
recommended: true
|
|
1898
|
+
},
|
|
1899
|
+
schema: [],
|
|
1900
|
+
messages: { noUiInheritance: "Avoid inheritance for Angular {{kind}} classes. Prefer composition with services or inject* helpers." }
|
|
1901
|
+
},
|
|
1902
|
+
createOnce(context) {
|
|
1903
|
+
return { "ClassDeclaration, ClassExpression"(node) {
|
|
1904
|
+
const classNode = node;
|
|
1905
|
+
if (!classNode.superClass) return;
|
|
1906
|
+
if (!hasUiDecorator(context, classNode)) return;
|
|
1907
|
+
const kind = classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, new Set(["Component"]))) ? "component" : "directive";
|
|
1908
|
+
context.report({
|
|
1909
|
+
node: classNode.superClass,
|
|
1910
|
+
messageId: "noUiInheritance",
|
|
1911
|
+
data: { kind }
|
|
1912
|
+
});
|
|
1913
|
+
} };
|
|
1914
|
+
}
|
|
1915
|
+
});
|
|
1916
|
+
//#endregion
|
|
1703
1917
|
//#region src/rules/prefer-load-component-over-load-children/index.ts
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
if (
|
|
1708
|
-
|
|
1709
|
-
|
|
1918
|
+
const ROUTE_TYPE_NAMES = new Set(["Route"]);
|
|
1919
|
+
const ROUTES_TYPE_NAMES = new Set(["Routes"]);
|
|
1920
|
+
function isImportedTypeName(context, typeNode, importedNames) {
|
|
1921
|
+
if (typeNode?.type === "Identifier") {
|
|
1922
|
+
const importedName = getImportedName(context, typeNode, "@angular/router");
|
|
1923
|
+
return !!importedName && importedNames.has(importedName);
|
|
1924
|
+
}
|
|
1925
|
+
return typeNode?.type === "TSQualifiedName" && typeNode.left?.type === "Identifier" && isNamespaceImport(context, typeNode.left, "@angular/router") && importedNames.has(getPropertyName(typeNode.right) ?? "");
|
|
1710
1926
|
}
|
|
1711
1927
|
function getTypeParameterNodes(typeNode) {
|
|
1712
1928
|
return typeNode.typeParameters?.params ?? typeNode.typeArguments?.params ?? [];
|
|
1713
1929
|
}
|
|
1714
|
-
function isRouteType(
|
|
1715
|
-
return typeNode?.type === "TSTypeReference" && isImportedTypeName(typeNode.typeName,
|
|
1930
|
+
function isRouteType(context, typeNode) {
|
|
1931
|
+
return typeNode?.type === "TSTypeReference" && isImportedTypeName(context, typeNode.typeName, ROUTE_TYPE_NAMES);
|
|
1716
1932
|
}
|
|
1717
|
-
function isRouteArrayType(
|
|
1933
|
+
function isRouteArrayType(context, typeNode) {
|
|
1718
1934
|
if (!typeNode) return false;
|
|
1719
|
-
if (typeNode.type === "TSTypeReference" && isImportedTypeName(typeNode.typeName,
|
|
1720
|
-
if (typeNode.type === "TSArrayType") return isRouteType(typeNode.elementType
|
|
1935
|
+
if (typeNode.type === "TSTypeReference" && isImportedTypeName(context, typeNode.typeName, ROUTES_TYPE_NAMES)) return true;
|
|
1936
|
+
if (typeNode.type === "TSArrayType") return isRouteType(context, typeNode.elementType);
|
|
1721
1937
|
if (typeNode.type !== "TSTypeReference") return false;
|
|
1722
|
-
|
|
1723
|
-
if (typeName !== "Array" && typeName !== "ReadonlyArray") return false;
|
|
1938
|
+
if (getPropertyName(typeNode.typeName) !== "Array" && getPropertyName(typeNode.typeName) !== "ReadonlyArray") return false;
|
|
1724
1939
|
const [elementType] = getTypeParameterNodes(typeNode);
|
|
1725
|
-
return isRouteType(
|
|
1940
|
+
return isRouteType(context, elementType);
|
|
1726
1941
|
}
|
|
1727
|
-
function isRoutesTypeAnnotation(
|
|
1942
|
+
function isRoutesTypeAnnotation(context, node) {
|
|
1728
1943
|
const typeAnnotation = node?.typeAnnotation;
|
|
1729
1944
|
if (!typeAnnotation || typeAnnotation.type !== "TSTypeAnnotation") return false;
|
|
1730
|
-
return isRouteArrayType(typeAnnotation.typeAnnotation
|
|
1945
|
+
return isRouteArrayType(context, typeAnnotation.typeAnnotation);
|
|
1731
1946
|
}
|
|
1732
1947
|
function isExportedConstDeclarator(node) {
|
|
1733
1948
|
if (node.type !== "VariableDeclarator") return false;
|
|
@@ -1746,11 +1961,6 @@ const preferLoadComponentOverLoadChildren = defineRule({
|
|
|
1746
1961
|
messages: { avoidLoadChildren: "Avoid loadChildren in Routes arrays; prefer loadComponent for lazy-loading standalone route components." }
|
|
1747
1962
|
},
|
|
1748
1963
|
createOnce(context) {
|
|
1749
|
-
const imports = {
|
|
1750
|
-
routeTypeLocalNames: /* @__PURE__ */ new Set(),
|
|
1751
|
-
routesTypeLocalNames: /* @__PURE__ */ new Set(),
|
|
1752
|
-
routerNamespaces: /* @__PURE__ */ new Set()
|
|
1753
|
-
};
|
|
1754
1964
|
function reportLoadChildrenInRouteObject(routeObject) {
|
|
1755
1965
|
for (const property of routeObject.properties ?? []) {
|
|
1756
1966
|
if (property.type !== "Property" || property.computed) continue;
|
|
@@ -1768,36 +1978,14 @@ const preferLoadComponentOverLoadChildren = defineRule({
|
|
|
1768
1978
|
function reportLoadChildrenInRouteArray(routeArray) {
|
|
1769
1979
|
for (const element of routeArray.elements ?? []) if (element?.type === "ObjectExpression") reportLoadChildrenInRouteObject(element);
|
|
1770
1980
|
}
|
|
1771
|
-
return {
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
for (const specifier of node.specifiers ?? []) {
|
|
1780
|
-
if (specifier.type === "ImportSpecifier") {
|
|
1781
|
-
const importedName = getPropertyName(specifier.imported);
|
|
1782
|
-
if (importedName === "Route") imports.routeTypeLocalNames.add(specifier.local.name);
|
|
1783
|
-
if (importedName === "Routes") imports.routesTypeLocalNames.add(specifier.local.name);
|
|
1784
|
-
}
|
|
1785
|
-
if (specifier.type === "ImportNamespaceSpecifier") {
|
|
1786
|
-
imports.routerNamespaces.add(specifier.local.name);
|
|
1787
|
-
imports.routeTypeLocalNames.add("Route");
|
|
1788
|
-
imports.routesTypeLocalNames.add("Routes");
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
},
|
|
1792
|
-
VariableDeclarator(node) {
|
|
1793
|
-
const declarator = node;
|
|
1794
|
-
if (!isExportedConstDeclarator(declarator)) return;
|
|
1795
|
-
if (declarator.id?.type !== "Identifier") return;
|
|
1796
|
-
if (!isRoutesTypeAnnotation(declarator.id, imports)) return;
|
|
1797
|
-
if (declarator.init?.type !== "ArrayExpression") return;
|
|
1798
|
-
reportLoadChildrenInRouteArray(declarator.init);
|
|
1799
|
-
}
|
|
1800
|
-
};
|
|
1981
|
+
return { VariableDeclarator(node) {
|
|
1982
|
+
const declarator = node;
|
|
1983
|
+
if (!isExportedConstDeclarator(declarator)) return;
|
|
1984
|
+
if (declarator.id?.type !== "Identifier") return;
|
|
1985
|
+
if (!isRoutesTypeAnnotation(context, declarator.id)) return;
|
|
1986
|
+
if (declarator.init?.type !== "ArrayExpression") return;
|
|
1987
|
+
reportLoadChildrenInRouteArray(declarator.init);
|
|
1988
|
+
} };
|
|
1801
1989
|
}
|
|
1802
1990
|
});
|
|
1803
1991
|
//#endregion
|
|
@@ -1960,10 +2148,10 @@ const preferPrivateElements = defineRule({
|
|
|
1960
2148
|
});
|
|
1961
2149
|
//#endregion
|
|
1962
2150
|
//#region src/rules/prefer-style-url/index.ts
|
|
1963
|
-
|
|
2151
|
+
const COMPONENT_DECORATORS = new Set(["Component"]);
|
|
2152
|
+
function isComponentDecoratorCall(context, node) {
|
|
1964
2153
|
const callee = node.callee;
|
|
1965
|
-
|
|
1966
|
-
return callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && angularNamespaces.has(callee.object.name) && !isShadowedIdentifier(context, callee.object) && getPropertyName(callee.property) === "Component";
|
|
2154
|
+
return isImportedReference(context, callee, "@angular/core", COMPONENT_DECORATORS) || isImportedNamespaceMember(context, callee, "@angular/core", COMPONENT_DECORATORS);
|
|
1967
2155
|
}
|
|
1968
2156
|
function isSingleStyleFileNode(node) {
|
|
1969
2157
|
if (!node) return false;
|
|
@@ -1982,47 +2170,30 @@ const preferStyleUrl = defineRule({
|
|
|
1982
2170
|
messages: { preferStyleUrl: "Use styleUrl instead of styleUrls when a component has one style file." }
|
|
1983
2171
|
},
|
|
1984
2172
|
createOnce(context) {
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
if (
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
if (property.type !== "Property") continue;
|
|
2008
|
-
if (property.computed) continue;
|
|
2009
|
-
if (getPropertyName(property.key) !== "styleUrls") continue;
|
|
2010
|
-
if (property.value?.type !== "ArrayExpression") continue;
|
|
2011
|
-
const elements = property.value.elements?.filter(Boolean) ?? [];
|
|
2012
|
-
if (elements.length !== 1) continue;
|
|
2013
|
-
const styleFile = elements[0];
|
|
2014
|
-
if (!isSingleStyleFileNode(styleFile)) continue;
|
|
2015
|
-
const keyRange = getRange(property.key);
|
|
2016
|
-
const valueRange = getRange(property.value);
|
|
2017
|
-
const styleFileRange = getRange(styleFile);
|
|
2018
|
-
context.report({
|
|
2019
|
-
node: property.key,
|
|
2020
|
-
messageId: "preferStyleUrl",
|
|
2021
|
-
fix: keyRange && valueRange && styleFileRange ? (fixer) => [fixer.replaceTextRange(keyRange, "styleUrl"), fixer.replaceTextRange(valueRange, context.sourceCode.text.slice(styleFileRange[0], styleFileRange[1]))] : void 0
|
|
2022
|
-
});
|
|
2023
|
-
}
|
|
2173
|
+
return { CallExpression(node) {
|
|
2174
|
+
const call = node;
|
|
2175
|
+
if (!isComponentDecoratorCall(context, call)) return;
|
|
2176
|
+
const metadata = call.arguments?.[0];
|
|
2177
|
+
if (metadata?.type !== "ObjectExpression") return;
|
|
2178
|
+
for (const property of metadata.properties ?? []) {
|
|
2179
|
+
if (property.type !== "Property") continue;
|
|
2180
|
+
if (property.computed) continue;
|
|
2181
|
+
if (getPropertyName(property.key) !== "styleUrls") continue;
|
|
2182
|
+
if (property.value?.type !== "ArrayExpression") continue;
|
|
2183
|
+
const elements = property.value.elements?.filter(Boolean) ?? [];
|
|
2184
|
+
if (elements.length !== 1) continue;
|
|
2185
|
+
const styleFile = elements[0];
|
|
2186
|
+
if (!isSingleStyleFileNode(styleFile)) continue;
|
|
2187
|
+
const keyRange = getRange(property.key);
|
|
2188
|
+
const valueRange = getRange(property.value);
|
|
2189
|
+
const styleFileRange = getRange(styleFile);
|
|
2190
|
+
context.report({
|
|
2191
|
+
node: property.key,
|
|
2192
|
+
messageId: "preferStyleUrl",
|
|
2193
|
+
fix: keyRange && valueRange && styleFileRange ? (fixer) => [fixer.replaceTextRange(keyRange, "styleUrl"), fixer.replaceTextRange(valueRange, context.sourceCode.text.slice(styleFileRange[0], styleFileRange[1]))] : void 0
|
|
2194
|
+
});
|
|
2024
2195
|
}
|
|
2025
|
-
};
|
|
2196
|
+
} };
|
|
2026
2197
|
}
|
|
2027
2198
|
});
|
|
2028
2199
|
//#endregion
|
|
@@ -2036,24 +2207,21 @@ const FIELD_NODE_TYPES = new Set([
|
|
|
2036
2207
|
"FieldDefinition",
|
|
2037
2208
|
"PropertyDefinition"
|
|
2038
2209
|
]);
|
|
2039
|
-
function hasTargetDecorator(context, classNode
|
|
2210
|
+
function hasTargetDecorator(context, classNode) {
|
|
2040
2211
|
if (!classNode || !Array.isArray(classNode.decorators)) return false;
|
|
2041
|
-
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator,
|
|
2212
|
+
return classNode.decorators.some((decorator) => isAngularCoreDecorator(context, decorator, TARGET_DECORATORS));
|
|
2042
2213
|
}
|
|
2043
|
-
function
|
|
2214
|
+
function isApiCallFromAngularCore(context, node, apiNames) {
|
|
2044
2215
|
if (!node || node.type !== "CallExpression") return false;
|
|
2045
2216
|
const callee = node.callee;
|
|
2046
|
-
if (callee
|
|
2217
|
+
if (isImportedReference(context, callee, "@angular/core", apiNames)) return true;
|
|
2047
2218
|
if (callee?.type !== "MemberExpression") return false;
|
|
2048
2219
|
const supportsRequiredApi = [...INPUT_MODEL_APIS].some((name) => apiNames.has(name));
|
|
2049
|
-
if (getPropertyName(callee.property) === "required" && callee.object?.type === "Identifier") return supportsRequiredApi &&
|
|
2050
|
-
if (callee
|
|
2051
|
-
|
|
2052
|
-
return !!namespaceApiName && apiNames.has(namespaceApiName) && !isShadowedIdentifier(context, callee.object);
|
|
2053
|
-
}
|
|
2054
|
-
if (getPropertyName(callee.property) === "required" && callee.object?.type === "MemberExpression" && callee.object.object?.type === "Identifier" && angularNamespaces.has(callee.object.object.name)) {
|
|
2220
|
+
if (getPropertyName(callee.property) === "required" && callee.object?.type === "Identifier") return supportsRequiredApi && isImportedReference(context, callee.object, "@angular/core", apiNames);
|
|
2221
|
+
if (isImportedNamespaceMember(context, callee, "@angular/core", apiNames)) return true;
|
|
2222
|
+
if (getPropertyName(callee.property) === "required" && callee.object?.type === "MemberExpression" && callee.object.object?.type === "Identifier") {
|
|
2055
2223
|
const namespaceApiName = getPropertyName(callee.object.property);
|
|
2056
|
-
return supportsRequiredApi && !!namespaceApiName && apiNames.has(namespaceApiName) &&
|
|
2224
|
+
return supportsRequiredApi && !!namespaceApiName && apiNames.has(namespaceApiName) && isImportedNamespaceMember(context, callee.object, "@angular/core", apiNames);
|
|
2057
2225
|
}
|
|
2058
2226
|
return false;
|
|
2059
2227
|
}
|
|
@@ -2098,87 +2266,48 @@ const publicComponentInterface = defineRule({
|
|
|
2098
2266
|
}
|
|
2099
2267
|
},
|
|
2100
2268
|
createOnce(context) {
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
decoratorImports.decoratorLocalNames.clear();
|
|
2117
|
-
},
|
|
2118
|
-
ImportDeclaration(node) {
|
|
2119
|
-
if (node.source?.value !== "@angular/core") return;
|
|
2120
|
-
for (const specifier of node.specifiers ?? []) {
|
|
2121
|
-
addAngularCoreDecoratorImport(specifier, TARGET_DECORATORS, decoratorImports);
|
|
2122
|
-
if (specifier.type === "ImportSpecifier") {
|
|
2123
|
-
const importedName = getPropertyName(specifier.imported);
|
|
2124
|
-
if (!importedName) continue;
|
|
2125
|
-
if (INPUT_MODEL_APIS.has(importedName)) inputModelLocalNames.add(specifier.local.name);
|
|
2126
|
-
if (OUTPUT_APIS.has(importedName)) outputLocalNames.add(specifier.local.name);
|
|
2127
|
-
if (INJECT_APIS.has(importedName)) injectLocalNames.add(specifier.local.name);
|
|
2128
|
-
continue;
|
|
2129
|
-
}
|
|
2130
|
-
if (specifier.type === "ImportNamespaceSpecifier") angularNamespaces.add(specifier.local.name);
|
|
2269
|
+
return { ClassBody(node) {
|
|
2270
|
+
const classNode = node.parent;
|
|
2271
|
+
if (!hasTargetDecorator(context, classNode)) return;
|
|
2272
|
+
for (const member of node.body ?? []) {
|
|
2273
|
+
if (!FIELD_NODE_TYPES.has(member.type)) continue;
|
|
2274
|
+
const isInputModelMember = isApiCallFromAngularCore(context, member.value, INPUT_MODEL_APIS);
|
|
2275
|
+
const isOutputMember = isApiCallFromAngularCore(context, member.value, OUTPUT_APIS);
|
|
2276
|
+
if (isInputModelMember && isNonPublicMember(member)) {
|
|
2277
|
+
context.report({
|
|
2278
|
+
node: member.key ?? member,
|
|
2279
|
+
messageId: "nonPublicInputModel",
|
|
2280
|
+
data: { name: getPropertyName(member.key) ?? "member" },
|
|
2281
|
+
fix: getVisibilityFix(context, member, "public")
|
|
2282
|
+
});
|
|
2283
|
+
continue;
|
|
2131
2284
|
}
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
const classNode = node.parent;
|
|
2135
|
-
if (!hasTargetDecorator(context, classNode, decoratorImports)) return;
|
|
2136
|
-
for (const member of node.body ?? []) {
|
|
2137
|
-
if (!FIELD_NODE_TYPES.has(member.type)) continue;
|
|
2138
|
-
const isInputModelMember = isApiCallFromTrackedImports(context, member.value, inputModelLocalNames, angularNamespaces, INPUT_MODEL_APIS);
|
|
2139
|
-
const isOutputMember = isApiCallFromTrackedImports(context, member.value, outputLocalNames, angularNamespaces, OUTPUT_APIS);
|
|
2140
|
-
if (isInputModelMember && isNonPublicMember(member)) {
|
|
2141
|
-
context.report({
|
|
2142
|
-
node: member.key ?? member,
|
|
2143
|
-
messageId: "nonPublicInputModel",
|
|
2144
|
-
data: { name: getPropertyName(member.key) ?? "member" },
|
|
2145
|
-
fix: getVisibilityFix(context, member, "public")
|
|
2146
|
-
});
|
|
2147
|
-
continue;
|
|
2148
|
-
}
|
|
2149
|
-
if (isOutputMember && isNonPublicMember(member)) {
|
|
2150
|
-
context.report({
|
|
2151
|
-
node: member.key ?? member,
|
|
2152
|
-
messageId: "nonPublicOutput",
|
|
2153
|
-
data: { name: getPropertyName(member.key) ?? "member" },
|
|
2154
|
-
fix: getVisibilityFix(context, member, "public")
|
|
2155
|
-
});
|
|
2156
|
-
continue;
|
|
2157
|
-
}
|
|
2158
|
-
if (isPublicMember(member) && isApiCallFromTrackedImports(context, member.value, injectLocalNames, angularNamespaces, INJECT_APIS)) context.report({
|
|
2285
|
+
if (isOutputMember && isNonPublicMember(member)) {
|
|
2286
|
+
context.report({
|
|
2159
2287
|
node: member.key ?? member,
|
|
2160
|
-
messageId: "
|
|
2288
|
+
messageId: "nonPublicOutput",
|
|
2161
2289
|
data: { name: getPropertyName(member.key) ?? "member" },
|
|
2162
|
-
fix: getVisibilityFix(context, member, "
|
|
2290
|
+
fix: getVisibilityFix(context, member, "public")
|
|
2163
2291
|
});
|
|
2292
|
+
continue;
|
|
2164
2293
|
}
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2294
|
+
if (isPublicMember(member) && isApiCallFromAngularCore(context, member.value, INJECT_APIS)) context.report({
|
|
2295
|
+
node: member.key ?? member,
|
|
2296
|
+
messageId: "publicInjectMember",
|
|
2297
|
+
data: { name: getPropertyName(member.key) ?? "member" },
|
|
2298
|
+
fix: getVisibilityFix(context, member, "protected")
|
|
2299
|
+
});
|
|
2171
2300
|
}
|
|
2172
|
-
};
|
|
2301
|
+
} };
|
|
2173
2302
|
}
|
|
2174
2303
|
});
|
|
2175
2304
|
//#endregion
|
|
2176
2305
|
//#region src/rules/restrict-injectable-provided-in/index.ts
|
|
2177
2306
|
const ALLOWED_PROVIDED_IN_VALUES = new Set(["root", "platform"]);
|
|
2178
2307
|
const INJECTABLE_DECORATORS = new Set(["Injectable"]);
|
|
2179
|
-
function getInjectableMetadata(context, node
|
|
2308
|
+
function getInjectableMetadata(context, node) {
|
|
2180
2309
|
if (node.type !== "Decorator") return null;
|
|
2181
|
-
if (!isAngularCoreDecorator(context, node,
|
|
2310
|
+
if (!isAngularCoreDecorator(context, node, INJECTABLE_DECORATORS)) return null;
|
|
2182
2311
|
const expression = node.expression;
|
|
2183
2312
|
if (expression?.type !== "CallExpression") return null;
|
|
2184
2313
|
const metadata = expression.arguments?.[0];
|
|
@@ -2203,41 +2332,26 @@ const restrictInjectableProvidedIn = defineRule({
|
|
|
2203
2332
|
messages: { disallowedProvidedIn: "@Injectable providedIn should be the literal 'root' or 'platform', not {{actual}}." }
|
|
2204
2333
|
},
|
|
2205
2334
|
createOnce(context) {
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
Decorator(node) {
|
|
2221
|
-
const metadata = getInjectableMetadata(context, node, decoratorImports);
|
|
2222
|
-
if (!metadata) return;
|
|
2223
|
-
const providedInProperty = getProvidedInProperty(metadata);
|
|
2224
|
-
if (!providedInProperty) return;
|
|
2225
|
-
const value = providedInProperty.value;
|
|
2226
|
-
if (isAllowedProvidedInValue(value)) return;
|
|
2227
|
-
const actual = context.sourceCode.text.slice(value?.range?.[0] ?? providedInProperty.range?.[0] ?? 0, value?.range?.[1] ?? providedInProperty.range?.[1] ?? 0);
|
|
2228
|
-
context.report({
|
|
2229
|
-
node: value ?? providedInProperty,
|
|
2230
|
-
messageId: "disallowedProvidedIn",
|
|
2231
|
-
data: { actual: actual || "this value" }
|
|
2232
|
-
});
|
|
2233
|
-
}
|
|
2234
|
-
};
|
|
2335
|
+
return { Decorator(node) {
|
|
2336
|
+
const metadata = getInjectableMetadata(context, node);
|
|
2337
|
+
if (!metadata) return;
|
|
2338
|
+
const providedInProperty = getProvidedInProperty(metadata);
|
|
2339
|
+
if (!providedInProperty) return;
|
|
2340
|
+
const value = providedInProperty.value;
|
|
2341
|
+
if (isAllowedProvidedInValue(value)) return;
|
|
2342
|
+
const actual = context.sourceCode.text.slice(value?.range?.[0] ?? providedInProperty.range?.[0] ?? 0, value?.range?.[1] ?? providedInProperty.range?.[1] ?? 0);
|
|
2343
|
+
context.report({
|
|
2344
|
+
node: value ?? providedInProperty,
|
|
2345
|
+
messageId: "disallowedProvidedIn",
|
|
2346
|
+
data: { actual: actual || "this value" }
|
|
2347
|
+
});
|
|
2348
|
+
} };
|
|
2235
2349
|
}
|
|
2236
2350
|
});
|
|
2237
2351
|
//#endregion
|
|
2238
2352
|
//#region src/rules/rules-of-inject/index.ts
|
|
2239
2353
|
const DEFAULT_ALLOWED_FUNCTION_NAMES = [];
|
|
2240
|
-
const DEFAULT_INJECT_FUNCTION_PREFIXES = ["
|
|
2354
|
+
const DEFAULT_INJECT_FUNCTION_PREFIXES = ["inject"];
|
|
2241
2355
|
const DEFAULT_INJECT_FUNCTION_SUFFIXES = ["Guard"];
|
|
2242
2356
|
const DEFAULT_RUNS_IN_INJECTION_CONTEXT = [];
|
|
2243
2357
|
const ROUTER_CONTEXT_PROPERTY_NAMES = new Set([
|
|
@@ -2454,12 +2568,18 @@ const plugin = eslintCompatPlugin({
|
|
|
2454
2568
|
rules: {
|
|
2455
2569
|
"avoid-explicit-injection-context": avoidExplicitInjectionContext,
|
|
2456
2570
|
"avoid-explicit-subscription-management": avoidExplicitSubscriptionManagement,
|
|
2571
|
+
"avoid-inappropriate-intimacy": avoidInappropriateIntimacy,
|
|
2457
2572
|
"avoid-ng-modules": avoidNgModules,
|
|
2458
2573
|
"avoid-rxjs-state-in-component": avoidRxjsStateInComponent,
|
|
2459
2574
|
"avoid-writing-signals-in-reactive-context": avoidWritingSignalsInReactiveContext,
|
|
2460
2575
|
"class-member-order": classMemberOrder,
|
|
2461
|
-
"
|
|
2576
|
+
"class-matches-filename": classMatchesFilename,
|
|
2462
2577
|
"component-resource-filenames": componentResourceFilenames,
|
|
2578
|
+
"decorator-filename-suffix": decoratorFilenameSuffix,
|
|
2579
|
+
"injects-tanstack-query-only-in-component-body": injectsTanstackQueryOnlyInComponentBody,
|
|
2580
|
+
"no-resource-api": noResourceApi,
|
|
2581
|
+
"no-route-resolvers": noRouteResolvers,
|
|
2582
|
+
"no-ui-inheritance": noUiInheritance,
|
|
2463
2583
|
"prefer-load-component-over-load-children": preferLoadComponentOverLoadChildren,
|
|
2464
2584
|
"prefer-private-elements": preferPrivateElements,
|
|
2465
2585
|
"prefer-style-url": preferStyleUrl,
|