@angular-eslint/eslint-plugin-template 22.3.1-alpha.5 → 22.3.1-alpha.7
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/dist/rules/no-inline-styles.d.ts +10 -2
- package/dist/rules/no-inline-styles.d.ts.map +1 -1
- package/dist/rules/no-inline-styles.js +15 -6
- package/dist/rules/prefer-at-else.d.ts.map +1 -1
- package/dist/rules/prefer-at-else.js +32 -0
- package/dist/utils/is-constant-expression.d.ts +18 -0
- package/dist/utils/is-constant-expression.d.ts.map +1 -0
- package/dist/utils/is-constant-expression.js +58 -0
- package/package.json +5 -5
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* - `false`: never allowed.
|
|
3
|
+
* - `true`: always allowed.
|
|
4
|
+
* - `'dynamic'`: allowed only when the bound value is not a constant
|
|
5
|
+
* (a literal, or an expression built exclusively from literals). Constant
|
|
6
|
+
* values could live in a stylesheet, so they are still reported.
|
|
7
|
+
*/
|
|
8
|
+
export type AllowOption = boolean | 'dynamic';
|
|
1
9
|
export type Options = [
|
|
2
10
|
{
|
|
3
|
-
readonly allowNgStyle?:
|
|
4
|
-
readonly allowBindToStyle?:
|
|
11
|
+
readonly allowNgStyle?: AllowOption;
|
|
12
|
+
readonly allowBindToStyle?: AllowOption;
|
|
5
13
|
}
|
|
6
14
|
];
|
|
7
15
|
export type MessageIds = 'noInlineStyles';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-inline-styles.d.ts","sourceRoot":"","sources":["../../src/rules/no-inline-styles.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"no-inline-styles.d.ts","sourceRoot":"","sources":["../../src/rules/no-inline-styles.ts"],"names":[],"mappings":"AASA;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,SAAS,CAAC;AAE9C,MAAM,MAAM,OAAO,GAAG;IACpB;QACE,QAAQ,CAAC,YAAY,CAAC,EAAE,WAAW,CAAC;QACpC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,WAAW,CAAC;KACzC;CACF,CAAC;AAKF,MAAM,MAAM,UAAU,GAAG,gBAAgB,CAAC;AAC1C,eAAO,MAAM,SAAS,qBAAqB,CAAC;;;;AAM5C,wBAgFG;AASH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
|
|
@@ -4,6 +4,7 @@ exports.RULE_DOCS_EXTENSION = exports.RULE_NAME = void 0;
|
|
|
4
4
|
const bundled_angular_compiler_1 = require("@angular-eslint/bundled-angular-compiler");
|
|
5
5
|
const utils_1 = require("@angular-eslint/utils");
|
|
6
6
|
const create_eslint_rule_1 = require("../utils/create-eslint-rule");
|
|
7
|
+
const is_constant_expression_1 = require("../utils/is-constant-expression");
|
|
7
8
|
const DEFAULT_OPTIONS = {
|
|
8
9
|
allowNgStyle: false,
|
|
9
10
|
allowBindToStyle: false,
|
|
@@ -21,11 +22,13 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
21
22
|
type: 'object',
|
|
22
23
|
properties: {
|
|
23
24
|
allowNgStyle: {
|
|
24
|
-
type: 'boolean',
|
|
25
|
+
oneOf: [{ type: 'boolean' }, { type: 'string', enum: ['dynamic'] }],
|
|
26
|
+
description: 'Whether `[ngStyle]` bindings are allowed. `true` allows them unconditionally. `"dynamic"` allows them only when the bound value is not a constant (e.g. `{ color: textColor }` is allowed, but `{ color: \'red\' }` is reported because it could be a class).',
|
|
25
27
|
default: DEFAULT_OPTIONS.allowNgStyle,
|
|
26
28
|
},
|
|
27
29
|
allowBindToStyle: {
|
|
28
|
-
type: 'boolean',
|
|
30
|
+
oneOf: [{ type: 'boolean' }, { type: 'string', enum: ['dynamic'] }],
|
|
31
|
+
description: 'Whether `[style]`, `[style.property]` and `style="{{ }}"` bindings are allowed. `true` allows them unconditionally. `"dynamic"` allows them only when the bound value is not a constant (e.g. `[style.width.px]="column.width"` is allowed, but `[style.color]="\'red\'"` is reported because it could be a class).',
|
|
29
32
|
default: DEFAULT_OPTIONS.allowBindToStyle,
|
|
30
33
|
},
|
|
31
34
|
},
|
|
@@ -47,13 +50,13 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
47
50
|
case bundled_angular_compiler_1.BindingType.Attribute:
|
|
48
51
|
return input.name === 'style';
|
|
49
52
|
case bundled_angular_compiler_1.BindingType.Style:
|
|
50
|
-
return !allowBindToStyle;
|
|
53
|
+
return !isAllowed(allowBindToStyle, input.value);
|
|
51
54
|
case bundled_angular_compiler_1.BindingType.Property:
|
|
52
55
|
if (input.name === 'style') {
|
|
53
|
-
return !allowBindToStyle;
|
|
56
|
+
return !isAllowed(allowBindToStyle, input.value);
|
|
54
57
|
}
|
|
55
58
|
if (input.name === 'ngStyle') {
|
|
56
|
-
return !allowNgStyle;
|
|
59
|
+
return !isAllowed(allowNgStyle, input.value);
|
|
57
60
|
}
|
|
58
61
|
return false;
|
|
59
62
|
default:
|
|
@@ -76,6 +79,12 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
76
79
|
};
|
|
77
80
|
},
|
|
78
81
|
});
|
|
82
|
+
function isAllowed(option, value) {
|
|
83
|
+
if (option === 'dynamic') {
|
|
84
|
+
return !(0, is_constant_expression_1.isConstantExpression)(value);
|
|
85
|
+
}
|
|
86
|
+
return option === true;
|
|
87
|
+
}
|
|
79
88
|
exports.RULE_DOCS_EXTENSION = {
|
|
80
|
-
rationale: 'Inline styles in templates (style attribute, ngStyle directive, [style] bindings, or [style.property] bindings) make it difficult to maintain consistent styling across an application and can violate Content Security Policy (CSP) restrictions. Styles should be defined in component stylesheets or CSS classes where they can be managed centrally, reused, cached by browsers, and easily modified. Inline styles also mix presentation concerns with template structure, making templates harder to read. Using CSS classes with [class] or [ngClass] bindings provides the same dynamic styling capabilities while keeping styles organized and maintainable. This rule can be configured to allow ngStyle or style bindings if needed for specific use cases.',
|
|
89
|
+
rationale: 'Inline styles in templates (style attribute, ngStyle directive, [style] bindings, or [style.property] bindings) make it difficult to maintain consistent styling across an application and can violate Content Security Policy (CSP) restrictions. Styles should be defined in component stylesheets or CSS classes where they can be managed centrally, reused, cached by browsers, and easily modified. Inline styles also mix presentation concerns with template structure, making templates harder to read. Using CSS classes with [class] or [ngClass] bindings provides the same dynamic styling capabilities while keeping styles organized and maintainable. This rule can be configured to allow ngStyle or style bindings if needed for specific use cases. Setting an option to "dynamic" allows a binding only when its value cannot be known ahead of time (for example `[style.width.px]="column.width"`), while still reporting bindings whose value is a constant (for example `[style.color]="\'red\'"`), since those could be expressed as a class instead.',
|
|
81
90
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-at-else.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-at-else.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"prefer-at-else.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-at-else.ts"],"names":[],"mappings":"AAmBA,MAAM,MAAM,OAAO,GAAG,EAAE,CAAC;AACzB,MAAM,MAAM,UAAU,GAAG,cAAc,CAAC;AACxC,eAAO,MAAM,SAAS,mBAAmB,CAAC;;;;AAe1C,wBAwKG;AAoGH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
|
|
@@ -37,6 +37,22 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
37
37
|
create(context) {
|
|
38
38
|
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
39
39
|
const previousNodeStack = [undefined];
|
|
40
|
+
function wouldPreventContentProjection(nodes) {
|
|
41
|
+
let rootNodeCount = 0;
|
|
42
|
+
let hasProjectableNode = false;
|
|
43
|
+
for (const node of nodes) {
|
|
44
|
+
if (node instanceof bundled_angular_compiler_1.TmplAstText &&
|
|
45
|
+
context.sourceCode.text
|
|
46
|
+
.slice(node.sourceSpan.start.offset, node.sourceSpan.end.offset)
|
|
47
|
+
.trim() === '') {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
rootNodeCount++;
|
|
51
|
+
hasProjectableNode ||=
|
|
52
|
+
node instanceof bundled_angular_compiler_1.TmplAstElement || node instanceof bundled_angular_compiler_1.TmplAstTemplate;
|
|
53
|
+
}
|
|
54
|
+
return rootNodeCount > 1 && hasProjectableNode;
|
|
55
|
+
}
|
|
40
56
|
function getFix(previous, current) {
|
|
41
57
|
const previousIf = previous.node.branches[0];
|
|
42
58
|
const currentIf = current.node.branches[0];
|
|
@@ -48,6 +64,22 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
|
48
64
|
if (currentIf.expressionAlias) {
|
|
49
65
|
return null;
|
|
50
66
|
}
|
|
67
|
+
// Angular's content projection through control flow only works reliably
|
|
68
|
+
// when a branch has a single root node. If fixing this violation would
|
|
69
|
+
// merge multiple roots into an existing branch, keep reporting the
|
|
70
|
+
// violation but do not offer an unsafe automatic fix.
|
|
71
|
+
if ((previousElse &&
|
|
72
|
+
wouldPreventContentProjection([
|
|
73
|
+
...previousElse.children,
|
|
74
|
+
...currentIf.children,
|
|
75
|
+
])) ||
|
|
76
|
+
(currentElse &&
|
|
77
|
+
wouldPreventContentProjection([
|
|
78
|
+
...previousIf.children,
|
|
79
|
+
...currentElse.children,
|
|
80
|
+
]))) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
51
83
|
return function* fix(fixer) {
|
|
52
84
|
if (!previousElse) {
|
|
53
85
|
// The previous `@if` block has no `@else` block,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type AST } from '@angular-eslint/bundled-angular-compiler';
|
|
2
|
+
/**
|
|
3
|
+
* Determines whether a template expression is a constant, i.e. its value is
|
|
4
|
+
* fully determined by the template source alone and can never change at
|
|
5
|
+
* runtime.
|
|
6
|
+
*
|
|
7
|
+
* An expression is constant when it is built exclusively from literals
|
|
8
|
+
* (strings, numbers, booleans, `null`, `undefined`, literal maps, literal
|
|
9
|
+
* arrays and template literals) combined with pure operators (arithmetic,
|
|
10
|
+
* logical, comparison, conditional, `typeof`, `void`, `!`, `!!`, parentheses).
|
|
11
|
+
*
|
|
12
|
+
* Anything that reads component state (property reads, keyed reads), performs
|
|
13
|
+
* a call, or applies a pipe is dynamic. Unknown node types are also treated as
|
|
14
|
+
* dynamic, so that this helper fails open rather than reporting false
|
|
15
|
+
* positives.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isConstantExpression(node: AST): boolean;
|
|
18
|
+
//# sourceMappingURL=is-constant-expression.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"is-constant-expression.d.ts","sourceRoot":"","sources":["../../src/utils/is-constant-expression.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,GAAG,EAgBT,MAAM,0CAA0C,CAAC;AAElD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAoDvD"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isConstantExpression = isConstantExpression;
|
|
4
|
+
const bundled_angular_compiler_1 = require("@angular-eslint/bundled-angular-compiler");
|
|
5
|
+
/**
|
|
6
|
+
* Determines whether a template expression is a constant, i.e. its value is
|
|
7
|
+
* fully determined by the template source alone and can never change at
|
|
8
|
+
* runtime.
|
|
9
|
+
*
|
|
10
|
+
* An expression is constant when it is built exclusively from literals
|
|
11
|
+
* (strings, numbers, booleans, `null`, `undefined`, literal maps, literal
|
|
12
|
+
* arrays and template literals) combined with pure operators (arithmetic,
|
|
13
|
+
* logical, comparison, conditional, `typeof`, `void`, `!`, `!!`, parentheses).
|
|
14
|
+
*
|
|
15
|
+
* Anything that reads component state (property reads, keyed reads), performs
|
|
16
|
+
* a call, or applies a pipe is dynamic. Unknown node types are also treated as
|
|
17
|
+
* dynamic, so that this helper fails open rather than reporting false
|
|
18
|
+
* positives.
|
|
19
|
+
*/
|
|
20
|
+
function isConstantExpression(node) {
|
|
21
|
+
if (node instanceof bundled_angular_compiler_1.ASTWithSource) {
|
|
22
|
+
return isConstantExpression(node.ast);
|
|
23
|
+
}
|
|
24
|
+
if (node instanceof bundled_angular_compiler_1.LiteralPrimitive || node instanceof bundled_angular_compiler_1.EmptyExpr) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (node instanceof bundled_angular_compiler_1.LiteralMap) {
|
|
28
|
+
return node.values.every(isConstantExpression);
|
|
29
|
+
}
|
|
30
|
+
if (node instanceof bundled_angular_compiler_1.LiteralArray) {
|
|
31
|
+
return node.expressions.every(isConstantExpression);
|
|
32
|
+
}
|
|
33
|
+
if (node instanceof bundled_angular_compiler_1.Interpolation || node instanceof bundled_angular_compiler_1.TemplateLiteral) {
|
|
34
|
+
return node.expressions.every(isConstantExpression);
|
|
35
|
+
}
|
|
36
|
+
if (node instanceof bundled_angular_compiler_1.ParenthesizedExpression) {
|
|
37
|
+
return isConstantExpression(node.expression);
|
|
38
|
+
}
|
|
39
|
+
// `Unary` extends `Binary`, so it must be checked first.
|
|
40
|
+
if (node instanceof bundled_angular_compiler_1.Unary) {
|
|
41
|
+
return isConstantExpression(node.expr);
|
|
42
|
+
}
|
|
43
|
+
if (node instanceof bundled_angular_compiler_1.Binary) {
|
|
44
|
+
return isConstantExpression(node.left) && isConstantExpression(node.right);
|
|
45
|
+
}
|
|
46
|
+
if (node instanceof bundled_angular_compiler_1.Conditional) {
|
|
47
|
+
return (isConstantExpression(node.condition) &&
|
|
48
|
+
isConstantExpression(node.trueExp) &&
|
|
49
|
+
isConstantExpression(node.falseExp));
|
|
50
|
+
}
|
|
51
|
+
if (node instanceof bundled_angular_compiler_1.PrefixNot ||
|
|
52
|
+
node instanceof bundled_angular_compiler_1.TypeofExpression ||
|
|
53
|
+
node instanceof bundled_angular_compiler_1.VoidExpression ||
|
|
54
|
+
node instanceof bundled_angular_compiler_1.NonNullAssert) {
|
|
55
|
+
return isConstantExpression(node.expression);
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@angular-eslint/eslint-plugin-template",
|
|
3
|
-
"version": "22.3.1-alpha.
|
|
3
|
+
"version": "22.3.1-alpha.7",
|
|
4
4
|
"description": "ESLint plugin for Angular Templates",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -19,17 +19,17 @@
|
|
|
19
19
|
"LICENSE"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@angular-eslint/bundled-angular-compiler": "22.3.1-alpha.
|
|
23
|
-
"@angular-eslint/utils": "22.3.1-alpha.
|
|
22
|
+
"@angular-eslint/bundled-angular-compiler": "22.3.1-alpha.7",
|
|
23
|
+
"@angular-eslint/utils": "22.3.1-alpha.7",
|
|
24
24
|
"aria-query": "5.3.2",
|
|
25
25
|
"axobject-query": "4.1.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@angular-eslint/test-utils": "22.3.1-alpha.
|
|
28
|
+
"@angular-eslint/test-utils": "22.3.1-alpha.7",
|
|
29
29
|
"@types/aria-query": "5.0.4"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@angular-eslint/template-parser": "22.3.1-alpha.
|
|
32
|
+
"@angular-eslint/template-parser": "22.3.1-alpha.7",
|
|
33
33
|
"@typescript-eslint/types": "^8.0.0",
|
|
34
34
|
"@typescript-eslint/utils": "^8.0.0",
|
|
35
35
|
"eslint": "^9.0.0 || ^10.0.0",
|