@ethlete/eslint-plugin 1.0.0-next.23 → 1.0.0-next.25

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/migrations.json +12 -0
  3. package/package.json +13 -3
  4. package/src/configs/recommended.js +33 -11
  5. package/src/generators/generators.json +10 -0
  6. package/src/generators/restore-module-augmentation-interfaces/migration.js +50 -0
  7. package/src/generators/restore-module-augmentation-interfaces/restore-module-augmentation-interfaces.js +103 -0
  8. package/src/generators/restore-module-augmentation-interfaces/schema.json +14 -0
  9. package/src/index.js +10 -2
  10. package/src/rules/angular-decorator-property-order.js +20 -20
  11. package/src/rules/class-constant-property.js +8 -14
  12. package/src/rules/class-member-order.js +77 -46
  13. package/src/rules/consistent-type-definitions.js +60 -0
  14. package/src/rules/inject-member-accessibility.js +5 -3
  15. package/src/rules/internals/angular-io.js +12 -13
  16. package/src/rules/internals/angular-member-visibility.js +11 -7
  17. package/src/rules/internals/import-resolution.js +191 -0
  18. package/src/rules/no-angular-router-api.js +23 -29
  19. package/src/rules/no-angular-seo-services.js +14 -9
  20. package/src/rules/no-cdk-import.js +7 -14
  21. package/src/rules/no-csp-unsafe.js +362 -0
  22. package/src/rules/no-document-cookie.js +3 -0
  23. package/src/rules/no-effect-cleanup-return.js +7 -3
  24. package/src/rules/no-empty-angular-metadata-arrays.js +3 -17
  25. package/src/rules/no-inject-chain.js +3 -1
  26. package/src/rules/no-leading-underscore-class-member.js +26 -3
  27. package/src/rules/no-legacy-angular-decorators.js +9 -21
  28. package/src/rules/no-legacy-query-import.js +6 -15
  29. package/src/rules/no-locale-id.js +4 -4
  30. package/src/rules/no-member-alias.js +11 -7
  31. package/src/rules/no-native-html-input-name.js +1 -1
  32. package/src/rules/no-pipe-logic.js +10 -17
  33. package/src/rules/no-redundant-on-push-change-detection.js +9 -23
  34. package/src/rules/no-rxjs-in-effect.js +12 -8
  35. package/src/rules/no-screaming-case-local.js +1 -0
  36. package/src/rules/no-standalone-flag.js +3 -17
  37. package/src/rules/no-subscribe-in-pipe.js +3 -2
  38. package/src/rules/no-template-literal-before-inline-template.js +3 -14
  39. package/src/rules/no-trivial-return-type.js +1 -0
  40. package/src/rules/no-trivial-wrapper-method.js +10 -16
  41. package/src/rules/no-typed-injected-element-ref.js +14 -17
  42. package/src/rules/no-unused-class-member.js +6 -9
  43. package/src/rules/prefer-clone-equal.js +8 -6
  44. package/src/rules/prefer-concise-angular-host-directives.js +23 -29
  45. package/src/rules/prefer-concise-angular-style-metadata.js +8 -16
  46. package/src/rules/prefer-element-dimensions.js +9 -10
  47. package/src/rules/prefer-linked-signal.js +42 -41
  48. package/src/rules/prefer-match-media.js +10 -14
  49. package/src/rules/prefer-present-tense-output.js +1 -1
  50. package/src/rules/prefer-rxjs-timer.js +3 -1
  51. package/src/rules/require-dollar-suffix.js +7 -4
  52. package/src/rules/require-on-push-change-detection.js +17 -25
  53. package/src/rules/require-view-encapsulation-none.js +17 -25
  54. package/src/rules/take-until-destroyed-last.js +64 -0
  55. package/src/rules/template-member-accessibility.js +8 -6
@@ -35,6 +35,10 @@
35
35
  * injectQueryParamChanges(), injectPathParamChanges()
36
36
  */
37
37
 
38
+ const { MODULE_REFERENCE_SELECTOR, getModuleReference, isImportedAs } = require('./internals/import-resolution');
39
+
40
+ const ANGULAR_ROUTER = '@angular/router';
41
+
38
42
  /**
39
43
  * Router properties that expose route state and must not be read directly.
40
44
  * @type {Map<string, string>}
@@ -66,6 +70,17 @@ const noAngularRouterApi = {
66
70
  schema: [],
67
71
  },
68
72
  create(context) {
73
+ const sourceCode = context.sourceCode;
74
+
75
+ /**
76
+ * @param {any} call
77
+ * @param {string} token
78
+ */
79
+ const isInjectOf = (call, token) =>
80
+ call?.type === 'CallExpression' &&
81
+ isImportedAs(sourceCode, call.callee, 'inject') &&
82
+ isImportedAs(sourceCode, call.arguments[0], token, ANGULAR_ROUTER);
83
+
69
84
  /**
70
85
  * Names of variables/properties assigned from inject(Router).
71
86
  * We track identifiers whose initializer is inject(Router) so we can
@@ -78,28 +93,14 @@ const noAngularRouterApi = {
78
93
  // ── Track inject(Router) bindings ──────────────────────────────────────
79
94
  // const router = inject(Router);
80
95
  VariableDeclarator(node) {
81
- if (
82
- node.init?.type === 'CallExpression' &&
83
- node.init.callee.type === 'Identifier' &&
84
- node.init.callee.name === 'inject' &&
85
- node.init.arguments[0]?.type === 'Identifier' &&
86
- node.init.arguments[0].name === 'Router' &&
87
- node.id.type === 'Identifier'
88
- ) {
96
+ if (isInjectOf(node.init, 'Router') && node.id.type === 'Identifier') {
89
97
  routerBindings.add(node.id.name);
90
98
  }
91
99
  },
92
100
 
93
101
  // ── Track class property assignments: router = inject(Router) ──────────
94
102
  PropertyDefinition(node) {
95
- if (
96
- node.value?.type === 'CallExpression' &&
97
- node.value.callee.type === 'Identifier' &&
98
- node.value.callee.name === 'inject' &&
99
- node.value.arguments[0]?.type === 'Identifier' &&
100
- node.value.arguments[0].name === 'Router' &&
101
- node.key.type === 'Identifier'
102
- ) {
103
+ if (isInjectOf(node.value, 'Router') && node.key.type === 'Identifier') {
103
104
  routerBindings.add(node.key.name);
104
105
  }
105
106
  },
@@ -130,24 +131,17 @@ const noAngularRouterApi = {
130
131
  },
131
132
 
132
133
  // ── ActivatedRoute import ───────────────────────────────────────────────
133
- ImportDeclaration(node) {
134
- if (node.source.value !== '@angular/router') return;
135
- for (const specifier of node.specifiers) {
136
- if (specifier.type !== 'ImportSpecifier') continue;
137
- if (specifier.imported.name === 'ActivatedRoute') {
138
- context.report({ node, messageId: 'noActivatedRoute' });
139
- return;
140
- }
134
+ [MODULE_REFERENCE_SELECTOR](node) {
135
+ const reference = getModuleReference(node);
136
+ if (reference?.source !== ANGULAR_ROUTER) return;
137
+ if (reference.named.some((entry) => entry.name === 'ActivatedRoute')) {
138
+ context.report({ node, messageId: 'noActivatedRoute' });
141
139
  }
142
140
  },
143
141
 
144
142
  // ── inject(ActivatedRoute) ──────────────────────────────────────────────
145
143
  CallExpression(node) {
146
- const { callee } = node;
147
- if (callee.type !== 'Identifier' || callee.name !== 'inject') return;
148
- const arg = node.arguments[0];
149
- if (arg?.type !== 'Identifier') return;
150
- if (arg.name === 'ActivatedRoute') {
144
+ if (isInjectOf(node, 'ActivatedRoute')) {
151
145
  context.report({ node, messageId: 'noActivatedRoute' });
152
146
  }
153
147
  },
@@ -1,6 +1,13 @@
1
1
  // @ts-check
2
2
  'use strict';
3
3
 
4
+ const {
5
+ MODULE_REFERENCE_SELECTOR,
6
+ getImportedName,
7
+ getModuleReference,
8
+ isImportedAs,
9
+ } = require('./internals/import-resolution');
10
+
4
11
  /**
5
12
  * Disallows injecting Angular's `Title` and `Meta` services from
6
13
  * `@angular/platform-browser` and importing them from that module.
@@ -48,11 +55,11 @@ const rule = {
48
55
  */
49
56
  const injectTokenName = (node) => {
50
57
  if (node.type !== 'CallExpression') return null;
51
- if (node.callee.type !== 'Identifier' || node.callee.name !== 'inject') return null;
58
+ if (!isImportedAs(context.sourceCode, node.callee, 'inject')) return null;
52
59
  const firstArg = node.arguments[0];
53
60
  if (!firstArg) return null;
54
- if (firstArg.type === 'Identifier') return firstArg.name;
55
- return null;
61
+
62
+ return getImportedName(context.sourceCode, firstArg, '@angular/platform-browser');
56
63
  };
57
64
 
58
65
  return {
@@ -64,13 +71,11 @@ const rule = {
64
71
  },
65
72
 
66
73
  // Detect: import { Title, Meta } from '@angular/platform-browser'
67
- ImportDeclaration(node) {
68
- const decl = /** @type {any} */ (node);
69
- if (decl.source.value !== '@angular/platform-browser') return;
74
+ [MODULE_REFERENCE_SELECTOR](node) {
75
+ const reference = getModuleReference(node);
76
+ if (reference?.source !== '@angular/platform-browser') return;
70
77
 
71
- for (const specifier of decl.specifiers) {
72
- if (specifier.type !== 'ImportSpecifier') continue;
73
- const imported = specifier.imported.name ?? specifier.imported.value;
78
+ for (const { name: imported, node: specifier } of reference.named) {
74
79
  if (imported === 'Title') context.report({ node: specifier, messageId: 'noImportTitle' });
75
80
  if (imported === 'Meta') context.report({ node: specifier, messageId: 'noImportMeta' });
76
81
  }
@@ -4,6 +4,7 @@
4
4
  const { createRequire } = require('node:module');
5
5
  const path = require('node:path');
6
6
  const fs = require('node:fs');
7
+ const { MODULE_REFERENCE_SELECTOR, getModuleReference } = require('./internals/import-resolution');
7
8
 
8
9
  /**
9
10
  * Disallows importing from `@ethlete/cdk`, the maintenance-mode predecessor of `@ethlete/components`,
@@ -52,13 +53,6 @@ const loadMigrationMap = (mapPath, cwd) => {
52
53
  return map;
53
54
  };
54
55
 
55
- /** @param {any} specifier */
56
- const importedName = (specifier) => {
57
- if (specifier.type !== 'ImportSpecifier') return null;
58
-
59
- return specifier.imported.name ?? specifier.imported.value ?? null;
60
- };
61
-
62
56
  /** @type {import('eslint').Rule.RuleModule} */
63
57
  const noCdkImport = {
64
58
  meta: {
@@ -95,13 +89,13 @@ const noCdkImport = {
95
89
  const entryFor = (name) => map?.[name] ?? null;
96
90
 
97
91
  return {
98
- ImportDeclaration(node) {
99
- const declaration = /** @type {any} */ (node);
100
- const source = declaration.source.value;
92
+ [MODULE_REFERENCE_SELECTOR](node) {
93
+ const reference = getModuleReference(node);
94
+ const source = reference?.source;
101
95
 
102
- if (typeof source !== 'string' || (source !== CDK_PACKAGE && !source.startsWith(`${CDK_PACKAGE}/`))) return;
96
+ if (!reference || (source !== CDK_PACKAGE && !source?.startsWith(`${CDK_PACKAGE}/`))) return;
103
97
 
104
- const named = declaration.specifiers.filter(/** @param {any} s */ (s) => importedName(s));
98
+ const { named } = reference;
105
99
 
106
100
  if (!named.length) {
107
101
  context.report({ node, messageId: 'module', data: { cdk: CDK_PACKAGE, docs: migrationDocs } });
@@ -109,8 +103,7 @@ const noCdkImport = {
109
103
  return;
110
104
  }
111
105
 
112
- for (const specifier of named) {
113
- const name = /** @type {string} */ (importedName(specifier));
106
+ for (const { name, node: specifier } of named) {
114
107
  const entry = entryFor(name);
115
108
 
116
109
  if (!entry) {
@@ -0,0 +1,362 @@
1
+ // @ts-check
2
+ 'use strict';
3
+
4
+ /**
5
+ * Disallows code that a strict Content Security Policy without `'unsafe-inline'` / `'unsafe-eval'`
6
+ * blocks at runtime:
7
+ *
8
+ * script-src 'self' 'nonce-…'; style-src 'self' 'nonce-…'
9
+ *
10
+ * BAD:
11
+ * el.setAttribute('style', 'color: red'); // inline style attribute
12
+ * renderer.setAttribute(el, 'style', css);
13
+ * const s = document.createElement('style'); // appended without a nonce
14
+ * eval(code); new Function(code); setTimeout('tick()', 10);
15
+ * `<div style="color: red">…</div>` // HTML string parsed into the DOM
16
+ * <div style="color: red"></div> // static template attribute
17
+ * host: { style: 'display: block' }
18
+ *
19
+ * GOOD:
20
+ * el.style.color = 'red'; el.style.setProperty('--x', '1'); [style.color]="c"
21
+ * s.setAttribute('nonce', inject(CSP_NONCE)); // or s.nonce = …
22
+ * a class or data attribute styled by a shipped stylesheet
23
+ *
24
+ * In an Angular template this rule runs on `.html` files and on inline templates extracted by
25
+ * `processInlineTemplates`; in TypeScript it skips a decorator's `template` string for that reason.
26
+ */
27
+
28
+ const fs = require('node:fs');
29
+ const { isGlobalReference } = require('./internals/import-resolution');
30
+
31
+ const NONCED_ELEMENTS = new Set(['script', 'style']);
32
+ const TIMER_FUNCTIONS = new Set(['setTimeout', 'setInterval']);
33
+ const HTML_STYLE_ATTRIBUTE = /<[a-zA-Z][^<>]*\sstyle\s*=/;
34
+ const UNFIXABLE_STYLE_VALUE = /['"\\]|\/\*|!important|url\(/i;
35
+ const PLAIN_STYLE_PROPERTY = /^[a-zA-Z]+$/;
36
+ const STYLE_PROPERTY = /^-{0,2}[a-zA-Z][\w-]*$/;
37
+ const QUOTED_INLINE_TEMPLATE = /\btemplate\s*:\s*['"]/;
38
+
39
+ /**
40
+ * Turns `a: b; c-d: e` into `{ a: 'b', 'c-d': 'e' }`, or returns null when the declarations cannot be
41
+ * rewritten without changing what they mean.
42
+ *
43
+ * @param {string} css
44
+ */
45
+ const toStyleObjectLiteral = (css) => {
46
+ if (UNFIXABLE_STYLE_VALUE.test(css)) return null;
47
+
48
+ const entries = [];
49
+
50
+ for (const declaration of css.split(';')) {
51
+ if (!declaration.trim()) continue;
52
+
53
+ const colon = declaration.indexOf(':');
54
+
55
+ if (colon === -1) return null;
56
+
57
+ const property = declaration.slice(0, colon).trim();
58
+ const value = declaration
59
+ .slice(colon + 1)
60
+ .trim()
61
+ .replace(/\s+/g, ' ');
62
+
63
+ if (!STYLE_PROPERTY.test(property) || !value) return null;
64
+
65
+ entries.push(`${PLAIN_STYLE_PROPERTY.test(property) ? property : `'${property}'`}: '${value}'`);
66
+ }
67
+
68
+ return entries.length ? `{ ${entries.join(', ')} }` : null;
69
+ };
70
+
71
+ /** @param {any} node */
72
+ const stringValue = (node) => (node?.type === 'Literal' && typeof node.value === 'string' ? node.value : null);
73
+
74
+ /** @param {any} node */
75
+ const propertyName = (node) => {
76
+ if (node?.type !== 'Property' || node.computed) return null;
77
+ if (node.key.type === 'Identifier') return node.key.name;
78
+
79
+ return stringValue(node.key);
80
+ };
81
+
82
+ /** @param {any} node */
83
+ const calleeMemberName = (node) =>
84
+ node.callee.type === 'MemberExpression' && !node.callee.computed && node.callee.property.type === 'Identifier'
85
+ ? node.callee.property.name
86
+ : null;
87
+
88
+ /** @param {any} node @param {string} name */
89
+ const hasProperty = (node, name) =>
90
+ node?.type === 'ObjectExpression' && node.properties.some(/** @param {any} p */ (p) => propertyName(p) === name);
91
+
92
+ /** @param {any} node */
93
+ const isDecoratorTemplate = (node) => {
94
+ const property = node.parent;
95
+
96
+ if (propertyName(property) !== 'template' || property.value !== node) return false;
97
+
98
+ const call = property.parent?.parent;
99
+
100
+ return call?.type === 'CallExpression' && call.parent?.type === 'Decorator';
101
+ };
102
+
103
+ /** @param {any} node */
104
+ const isHostMetadata = (node) => {
105
+ const property = node.parent;
106
+
107
+ if (propertyName(property) !== 'host' || property.value !== node) return false;
108
+
109
+ const call = property.parent?.parent;
110
+
111
+ return call?.type === 'CallExpression' && call.parent?.type === 'Decorator';
112
+ };
113
+
114
+ /** @param {any} node */
115
+ const enclosingScopeNode = (node) => {
116
+ let current = node.parent;
117
+
118
+ while (current) {
119
+ if (
120
+ current.type === 'FunctionDeclaration' ||
121
+ current.type === 'FunctionExpression' ||
122
+ current.type === 'ArrowFunctionExpression' ||
123
+ current.type === 'Program'
124
+ ) {
125
+ return current;
126
+ }
127
+
128
+ current = current.parent;
129
+ }
130
+
131
+ return null;
132
+ };
133
+
134
+ /** @param {any} node */
135
+ const boundTarget = (node) => {
136
+ const parent = node.parent;
137
+
138
+ if (parent?.type === 'VariableDeclarator' && parent.init === node && parent.id.type === 'Identifier')
139
+ return parent.id;
140
+ if (parent?.type === 'AssignmentExpression' && parent.right === node) return parent.left;
141
+
142
+ return null;
143
+ };
144
+
145
+ /** @type {import('eslint').Rule.RuleModule} */
146
+ const noCspUnsafe = {
147
+ meta: {
148
+ type: 'problem',
149
+ fixable: 'code',
150
+ docs: {
151
+ description:
152
+ "Disallow code a strict Content Security Policy (no 'unsafe-inline', no 'unsafe-eval') blocks at runtime.",
153
+ recommended: true,
154
+ },
155
+ schema: [],
156
+ messages: {
157
+ styleAttribute:
158
+ "A `style` attribute is blocked by a CSP without 'unsafe-inline'. Set styles through CSSOM (`el.style.x = …`, `style.setProperty()`, a `[style.x]` binding) or use a class.",
159
+ htmlStyleAttribute:
160
+ "This HTML string sets a `style` attribute, which a CSP without 'unsafe-inline' blocks once it is parsed into the DOM. Use a class or a data attribute styled by a stylesheet.",
161
+ templateStyleAttribute:
162
+ "A static `style` attribute is blocked by a CSP without 'unsafe-inline'. Use `[style.x]` bindings or a class.",
163
+ missingNonce:
164
+ "A `<{{tag}}>` element created without a nonce is blocked by a CSP without 'unsafe-inline'. Set `nonce` from `inject(CSP_NONCE)` on it in the same function.",
165
+ dynamicCode: "`{{name}}` evaluates a string as code, which a CSP without 'unsafe-eval' blocks.",
166
+ },
167
+ },
168
+ create(context) {
169
+ const sourceCode = context.sourceCode;
170
+ const parserServices = /** @type {any} */ (sourceCode.parserServices);
171
+
172
+ if (parserServices?.convertNodeSourceSpanToLoc) {
173
+ /** @param {any} span @param {string} messageId */
174
+ const reportSpan = (span, messageId) =>
175
+ context.report({ loc: parserServices.convertNodeSourceSpanToLoc(span), messageId });
176
+
177
+ let quotedInlineTemplate = /** @type {boolean | undefined} */ (undefined);
178
+
179
+ // The fix writes single quotes, which would end a quoted `template: '…'` string in the component.
180
+ const canFixTemplate = () => {
181
+ if (context.physicalFilename === context.filename || !context.physicalFilename.endsWith('.ts')) return true;
182
+
183
+ quotedInlineTemplate ??= QUOTED_INLINE_TEMPLATE.test(fs.readFileSync(context.physicalFilename, 'utf8'));
184
+
185
+ return !quotedInlineTemplate;
186
+ };
187
+
188
+ return {
189
+ /** @param {any} node */
190
+ Element(node) {
191
+ const hasStyleBinding = (node.inputs ?? []).some(
192
+ /** @param {any} input */ (input) => input.name === 'style' && String(input.keySpan) === 'style',
193
+ );
194
+
195
+ for (const attribute of node.attributes ?? []) {
196
+ if (attribute.name !== 'style') continue;
197
+
198
+ const literal = hasStyleBinding || !canFixTemplate() ? null : toStyleObjectLiteral(attribute.value);
199
+
200
+ context.report({
201
+ loc: parserServices.convertNodeSourceSpanToLoc(attribute.sourceSpan),
202
+ messageId: 'templateStyleAttribute',
203
+ fix: literal
204
+ ? (fixer) =>
205
+ fixer.replaceTextRange(
206
+ [attribute.sourceSpan.start.offset, attribute.sourceSpan.end.offset],
207
+ `[style]="${literal}"`,
208
+ )
209
+ : null,
210
+ });
211
+ }
212
+
213
+ for (const input of node.inputs ?? []) {
214
+ if (input.name === 'style' && String(input.keySpan) === 'attr.style') {
215
+ reportSpan(input.sourceSpan, 'styleAttribute');
216
+ }
217
+ }
218
+ },
219
+ };
220
+ }
221
+
222
+ /** @param {any} scope @param {string} target */
223
+ const setsNonce = (scope, target) => {
224
+ let found = false;
225
+
226
+ /** @param {any} node */
227
+ const matches = (node) => node && sourceCode.getText(node) === target;
228
+
229
+ /** @param {any} node */
230
+ const visit = (node) => {
231
+ if (found || !node || typeof node.type !== 'string') return;
232
+
233
+ if (
234
+ node.type === 'AssignmentExpression' &&
235
+ node.left.type === 'MemberExpression' &&
236
+ !node.left.computed &&
237
+ node.left.property.name === 'nonce' &&
238
+ matches(node.left.object)
239
+ ) {
240
+ found = true;
241
+ return;
242
+ }
243
+
244
+ if (node.type === 'CallExpression') {
245
+ const method = calleeMemberName(node);
246
+ const [first, second] = node.arguments;
247
+
248
+ if (
249
+ (method === 'setAttribute' && matches(node.callee.object) && stringValue(first) === 'nonce') ||
250
+ (method === 'setAttribute' && matches(first) && stringValue(second) === 'nonce') ||
251
+ ((method === 'setAttributes' || method === 'assign') && matches(first) && hasProperty(second, 'nonce'))
252
+ ) {
253
+ found = true;
254
+ return;
255
+ }
256
+ }
257
+
258
+ for (const key of sourceCode.visitorKeys[node.type] ?? []) {
259
+ const child = node[key];
260
+
261
+ if (Array.isArray(child)) child.forEach(visit);
262
+ else visit(child);
263
+ }
264
+ };
265
+
266
+ visit(scope);
267
+
268
+ return found;
269
+ };
270
+
271
+ /** @param {any} node @param {string} text */
272
+ const checkHtmlString = (node, text) => {
273
+ if (HTML_STYLE_ATTRIBUTE.test(text) && !isDecoratorTemplate(node)) {
274
+ context.report({ node, messageId: 'htmlStyleAttribute' });
275
+ }
276
+ };
277
+
278
+ return {
279
+ /** @param {any} node */
280
+ CallExpression(node) {
281
+ const method = calleeMemberName(node);
282
+ const [first, second] = node.arguments;
283
+
284
+ if (method === 'setAttribute') {
285
+ if (stringValue(first) === 'style' || (node.arguments.length >= 3 && stringValue(second) === 'style')) {
286
+ context.report({ node, messageId: 'styleAttribute' });
287
+ }
288
+
289
+ return;
290
+ }
291
+
292
+ if (method === 'setAttributes' && hasProperty(second, 'style')) {
293
+ context.report({ node, messageId: 'styleAttribute' });
294
+ return;
295
+ }
296
+
297
+ if (method === 'createElement') {
298
+ const tag = stringValue(first)?.toLowerCase();
299
+
300
+ if (!tag || !NONCED_ELEMENTS.has(tag)) return;
301
+
302
+ const target = boundTarget(node);
303
+ const scope = enclosingScopeNode(node);
304
+
305
+ if (!target || !scope || !setsNonce(scope, sourceCode.getText(target))) {
306
+ context.report({ node, messageId: 'missingNonce', data: { tag } });
307
+ }
308
+
309
+ return;
310
+ }
311
+
312
+ const callee = node.callee;
313
+ const name = callee.type === 'Identifier' ? callee.name : callee.type === 'MemberExpression' ? method : null;
314
+
315
+ if (!name) return;
316
+
317
+ if (callee.type === 'Identifier' && (name === 'eval' || name === 'Function')) {
318
+ if (isGlobalReference(sourceCode, callee)) context.report({ node, messageId: 'dynamicCode', data: { name } });
319
+
320
+ return;
321
+ }
322
+
323
+ if (TIMER_FUNCTIONS.has(name) && (stringValue(first) !== null || first?.type === 'TemplateLiteral')) {
324
+ if (callee.type === 'MemberExpression' || isGlobalReference(sourceCode, callee)) {
325
+ context.report({ node, messageId: 'dynamicCode', data: { name: `${name}(string)` } });
326
+ }
327
+ }
328
+ },
329
+ /** @param {any} node */
330
+ NewExpression(node) {
331
+ if (
332
+ node.callee.type === 'Identifier' &&
333
+ node.callee.name === 'Function' &&
334
+ isGlobalReference(sourceCode, node.callee)
335
+ ) {
336
+ context.report({ node, messageId: 'dynamicCode', data: { name: 'new Function' } });
337
+ }
338
+ },
339
+ /** @param {any} node */
340
+ ObjectExpression(node) {
341
+ if (!isHostMetadata(node)) return;
342
+
343
+ for (const property of node.properties) {
344
+ const key = propertyName(property);
345
+
346
+ if (key === 'style' || key === '[attr.style]')
347
+ context.report({ node: property, messageId: 'styleAttribute' });
348
+ }
349
+ },
350
+ /** @param {any} node */
351
+ Literal(node) {
352
+ if (typeof node.value === 'string') checkHtmlString(node, node.value);
353
+ },
354
+ /** @param {any} node */
355
+ TemplateLiteral(node) {
356
+ checkHtmlString(node, node.quasis.map(/** @param {any} q */ (q) => q.value.cooked ?? q.value.raw).join('x'));
357
+ },
358
+ };
359
+ },
360
+ };
361
+
362
+ module.exports = noCspUnsafe;
@@ -21,6 +21,8 @@
21
21
  * deleteCookie('name');
22
22
  */
23
23
 
24
+ const { isGlobalReference } = require('./internals/import-resolution');
25
+
24
26
  /** @type {import('eslint').Rule.RuleModule} */
25
27
  const noDocumentCookie = {
26
28
  meta: {
@@ -42,6 +44,7 @@ const noDocumentCookie = {
42
44
  if (
43
45
  node.object.type === 'Identifier' &&
44
46
  node.object.name === 'document' &&
47
+ isGlobalReference(context.sourceCode, node.object) &&
45
48
  node.property.type === 'Identifier' &&
46
49
  node.property.name === 'cookie'
47
50
  ) {
@@ -1,6 +1,8 @@
1
1
  // @ts-check
2
2
  'use strict';
3
3
 
4
+ const { ANGULAR_CORE, getImportedName } = require('./internals/import-resolution');
5
+
4
6
  /**
5
7
  * Disallows returning a cleanup function from an `effect()` callback.
6
8
  *
@@ -115,7 +117,9 @@ const noEffectCleanupReturn = {
115
117
  CallExpression(node) {
116
118
  const { callee } = node;
117
119
 
118
- if (callee.type !== 'Identifier' || !EFFECT_CALLEES.has(callee.name)) {
120
+ const calleeName = getImportedName(context.sourceCode, callee, ANGULAR_CORE);
121
+
122
+ if (!calleeName || !EFFECT_CALLEES.has(calleeName)) {
119
123
  return;
120
124
  }
121
125
 
@@ -130,7 +134,7 @@ const noEffectCleanupReturn = {
130
134
  context.report({
131
135
  node: callback.body,
132
136
  messageId: 'returnedCleanup',
133
- data: { callee: callee.name },
137
+ data: { callee: calleeName },
134
138
  });
135
139
 
136
140
  return;
@@ -163,7 +167,7 @@ const noEffectCleanupReturn = {
163
167
  context.report({
164
168
  node: statement.argument,
165
169
  messageId: 'returnedCleanup',
166
- data: { callee: callee.name },
170
+ data: { callee: calleeName },
167
171
  fix:
168
172
  fixable && openParen
169
173
  ? (fixer) => [
@@ -1,6 +1,8 @@
1
1
  // @ts-check
2
2
  'use strict';
3
3
 
4
+ const { getAngularDecoratorName } = require('./internals/import-resolution');
5
+
4
6
  const { getMetadataEntryRemovalRange } = require('./internals/angular-metadata-fix');
5
7
 
6
8
  /** @typedef {'Component' | 'Directive'} TDecoratorName */
@@ -14,22 +16,6 @@ const getPropertyName = (key) => {
14
16
  return null;
15
17
  };
16
18
 
17
- /**
18
- * @param {import('eslint').Rule.Node} node
19
- * @returns {TDecoratorName | null}
20
- */
21
- const getDecoratorName = (node) => {
22
- const decorator = /** @type {any} */ (node);
23
- if (decorator.type !== 'Decorator') return null;
24
-
25
- const expression = decorator.expression;
26
- if (expression.type === 'CallExpression') {
27
- return expression.callee.type === 'Identifier' ? expression.callee.name : null;
28
- }
29
-
30
- return expression.type === 'Identifier' ? expression.name : null;
31
- };
32
-
33
19
  const MESSAGE_IDS = {
34
20
  hostDirectives: 'noEmptyHostDirectives',
35
21
  imports: 'noEmptyImports',
@@ -55,7 +41,7 @@ const noEmptyAngularMetadataArrays = {
55
41
  return {
56
42
  /** @param {import('eslint').Rule.Node} node */
57
43
  Decorator(node) {
58
- const decoratorName = getDecoratorName(node);
44
+ const decoratorName = getAngularDecoratorName(context.sourceCode, node);
59
45
  if (decoratorName !== 'Component' && decoratorName !== 'Directive') return;
60
46
 
61
47
  const decorator = /** @type {any} */ (node);
@@ -1,6 +1,8 @@
1
1
  // @ts-check
2
2
  'use strict';
3
3
 
4
+ const { isImportedAs } = require('./internals/import-resolution');
5
+
4
6
  /** @type {import('eslint').Rule.RuleModule} */
5
7
  const noInjectChain = {
6
8
  meta: {
@@ -19,7 +21,7 @@ const noInjectChain = {
19
21
  return {
20
22
  MemberExpression(node) {
21
23
  const obj = node.object;
22
- if (obj.type === 'CallExpression' && obj.callee.type === 'Identifier' && obj.callee.name === 'inject') {
24
+ if (obj.type === 'CallExpression' && isImportedAs(context.sourceCode, obj.callee, 'inject')) {
23
25
  // Allow inject(X).method() — immediately-invoked patterns like
24
26
  // inject(DestroyRef).onDestroy(...) are intentional Angular idioms.
25
27
  const parent = node.parent;