@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.
@@ -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?: boolean;
4
- readonly allowBindToStyle?: boolean;
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":"AAOA,MAAM,MAAM,OAAO,GAAG;IACpB;QACE,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;QAChC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;KACrC;CACF,CAAC;AAKF,MAAM,MAAM,UAAU,GAAG,gBAAgB,CAAC;AAC1C,eAAO,MAAM,SAAS,qBAAqB,CAAC;;;;AAM5C,wBA4EG;AAEH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
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":"AAeA,MAAM,MAAM,OAAO,GAAG,EAAE,CAAC;AACzB,MAAM,MAAM,UAAU,GAAG,cAAc,CAAC;AACxC,eAAO,MAAM,SAAS,mBAAmB,CAAC;;;;AAe1C,wBA+HG;AAoGH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
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.5",
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.5",
23
- "@angular-eslint/utils": "22.3.1-alpha.5",
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.5",
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.5",
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",