@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.
- package/CHANGELOG.md +26 -0
- package/migrations.json +12 -0
- package/package.json +13 -3
- package/src/configs/recommended.js +33 -11
- package/src/generators/generators.json +10 -0
- package/src/generators/restore-module-augmentation-interfaces/migration.js +50 -0
- package/src/generators/restore-module-augmentation-interfaces/restore-module-augmentation-interfaces.js +103 -0
- package/src/generators/restore-module-augmentation-interfaces/schema.json +14 -0
- package/src/index.js +10 -2
- package/src/rules/angular-decorator-property-order.js +20 -20
- package/src/rules/class-constant-property.js +8 -14
- package/src/rules/class-member-order.js +77 -46
- package/src/rules/consistent-type-definitions.js +60 -0
- package/src/rules/inject-member-accessibility.js +5 -3
- package/src/rules/internals/angular-io.js +12 -13
- package/src/rules/internals/angular-member-visibility.js +11 -7
- package/src/rules/internals/import-resolution.js +191 -0
- package/src/rules/no-angular-router-api.js +23 -29
- package/src/rules/no-angular-seo-services.js +14 -9
- package/src/rules/no-cdk-import.js +7 -14
- package/src/rules/no-csp-unsafe.js +362 -0
- package/src/rules/no-document-cookie.js +3 -0
- package/src/rules/no-effect-cleanup-return.js +7 -3
- package/src/rules/no-empty-angular-metadata-arrays.js +3 -17
- package/src/rules/no-inject-chain.js +3 -1
- package/src/rules/no-leading-underscore-class-member.js +26 -3
- package/src/rules/no-legacy-angular-decorators.js +9 -21
- package/src/rules/no-legacy-query-import.js +6 -15
- package/src/rules/no-locale-id.js +4 -4
- package/src/rules/no-member-alias.js +11 -7
- package/src/rules/no-native-html-input-name.js +1 -1
- package/src/rules/no-pipe-logic.js +10 -17
- package/src/rules/no-redundant-on-push-change-detection.js +9 -23
- package/src/rules/no-rxjs-in-effect.js +12 -8
- package/src/rules/no-screaming-case-local.js +1 -0
- package/src/rules/no-standalone-flag.js +3 -17
- package/src/rules/no-subscribe-in-pipe.js +3 -2
- package/src/rules/no-template-literal-before-inline-template.js +3 -14
- package/src/rules/no-trivial-return-type.js +1 -0
- package/src/rules/no-trivial-wrapper-method.js +10 -16
- package/src/rules/no-typed-injected-element-ref.js +14 -17
- package/src/rules/no-unused-class-member.js +6 -9
- package/src/rules/prefer-clone-equal.js +8 -6
- package/src/rules/prefer-concise-angular-host-directives.js +23 -29
- package/src/rules/prefer-concise-angular-style-metadata.js +8 -16
- package/src/rules/prefer-element-dimensions.js +9 -10
- package/src/rules/prefer-linked-signal.js +42 -41
- package/src/rules/prefer-match-media.js +10 -14
- package/src/rules/prefer-present-tense-output.js +1 -1
- package/src/rules/prefer-rxjs-timer.js +3 -1
- package/src/rules/require-dollar-suffix.js +7 -4
- package/src/rules/require-on-push-change-detection.js +17 -25
- package/src/rules/require-view-encapsulation-none.js +17 -25
- package/src/rules/take-until-destroyed-last.js +64 -0
- 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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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 (
|
|
58
|
+
if (!isImportedAs(context.sourceCode, node.callee, 'inject')) return null;
|
|
52
59
|
const firstArg = node.arguments[0];
|
|
53
60
|
if (!firstArg) return null;
|
|
54
|
-
|
|
55
|
-
return
|
|
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
|
-
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
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
|
|
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
|
-
|
|
99
|
-
const
|
|
100
|
-
const source =
|
|
92
|
+
[MODULE_REFERENCE_SELECTOR](node) {
|
|
93
|
+
const reference = getModuleReference(node);
|
|
94
|
+
const source = reference?.source;
|
|
101
95
|
|
|
102
|
-
if (
|
|
96
|
+
if (!reference || (source !== CDK_PACKAGE && !source?.startsWith(`${CDK_PACKAGE}/`))) return;
|
|
103
97
|
|
|
104
|
-
const named
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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 =
|
|
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' &&
|
|
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;
|