@angular-eslint/eslint-plugin-template 22.6.1-alpha.2 → 22.6.1-alpha.3

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 +1 @@
1
- {"version":3,"file":"no-interpolation-in-attributes.d.ts","sourceRoot":"","sources":["../../src/rules/no-interpolation-in-attributes.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,OAAO,GAAG,CAAC;IAAE,2BAA2B,EAAE,OAAO,CAAA;CAAE,CAAC,CAAC;AACjE,MAAM,MAAM,UAAU,GAAG,6BAA6B,CAAC;AACvD,eAAO,MAAM,SAAS,mCAAmC,CAAC;;;;AAY1D,wBAmFG;AAEH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
1
+ {"version":3,"file":"no-interpolation-in-attributes.d.ts","sourceRoot":"","sources":["../../src/rules/no-interpolation-in-attributes.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,OAAO,GAAG,CAAC;IAAE,2BAA2B,EAAE,OAAO,CAAA;CAAE,CAAC,CAAC;AACjE,MAAM,MAAM,UAAU,GAAG,6BAA6B,CAAC;AACvD,eAAO,MAAM,SAAS,mCAAmC,CAAC;;;;AAY1D,wBAoFG;AAEH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
@@ -57,7 +57,7 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
57
57
  end: sourceCode.getLocFromIndex(end),
58
58
  },
59
59
  messageId: 'noInterpolationInAttributes',
60
- fix: isFullInterpolation
60
+ fix: isFullInterpolation && interpolation.expressions.length === 1
61
61
  ? (fixer) => {
62
62
  const attrStart = boundAttribute.keySpan.start.offset;
63
63
  const attrEnd = boundAttribute.keySpan.end.offset;
@@ -1 +1 @@
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"}
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,wBAkLG;AAoGH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
@@ -37,6 +37,10 @@ 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
+ // `@if` blocks that a fix in this pass turns into an else branch.
41
+ // Merging one of them into the next `@if` as well would edit a separate
42
+ // range, so ESLint would apply both fixes and write two else branches.
43
+ const ifBlocksTurnedIntoElse = new Set();
40
44
  function wouldPreventContentProjection(nodes) {
41
45
  let rootNodeCount = 0;
42
46
  let hasProjectableNode = false;
@@ -125,10 +129,16 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
125
129
  if (current) {
126
130
  const previous = previousNodeStack.at(-1);
127
131
  if (previous && canCombine(previous, current)) {
132
+ const fix = ifBlocksTurnedIntoElse.has(previous.node)
133
+ ? null
134
+ : getFix(previous, current);
135
+ if (fix) {
136
+ ifBlocksTurnedIntoElse.add(current.node);
137
+ }
128
138
  context.report({
129
139
  loc: parserServices.convertNodeSourceSpanToLoc(current.node.nameSpan),
130
140
  messageId: 'preferAtElse',
131
- fix: getFix(previous, current),
141
+ fix,
132
142
  });
133
143
  }
134
144
  }
@@ -1,6 +1,11 @@
1
- export type MessageIds = 'preferStyleBinding';
1
+ export type Options = [
2
+ {
3
+ readonly bindUnits?: boolean;
4
+ }
5
+ ];
6
+ export type MessageIds = 'preferStyleBinding' | 'preferStyleUnitBinding' | 'suggestStyleUnitBinding';
2
7
  export declare const RULE_NAME = "prefer-style-binding";
3
- declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferStyleBinding", [], import("../utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
8
+ declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<MessageIds, Options, import("../utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
4
9
  name: string;
5
10
  };
6
11
  export default _default;
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-style-binding.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-style-binding.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,UAAU,GAAG,oBAAoB,CAAC;AAC9C,eAAO,MAAM,SAAS,yBAAyB,CAAC;;;;AAEhD,wBA4BG;AAEH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
1
+ {"version":3,"file":"prefer-style-binding.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-style-binding.ts"],"names":[],"mappings":"AAgBA,MAAM,MAAM,OAAO,GAAG;IACpB;QACE,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;KAC9B;CACF,CAAC;AAKF,MAAM,MAAM,UAAU,GACpB,oBAAoB,GAAG,wBAAwB,GAAG,yBAAyB,CAAC;AAC9E,eAAO,MAAM,SAAS,yBAAyB,CAAC;;;;AA2EhD,wBAgFG;AAkIH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
@@ -3,7 +3,70 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RULE_DOCS_EXTENSION = exports.RULE_NAME = void 0;
4
4
  const utils_1 = require("@angular-eslint/utils");
5
5
  const create_eslint_rule_1 = require("../utils/create-eslint-rule");
6
+ const literal_primitive_1 = require("../utils/literal-primitive");
7
+ const unwrap_parenthesized_expression_1 = require("../utils/unwrap-parenthesized-expression");
8
+ const DEFAULT_OPTIONS = {
9
+ bindUnits: false,
10
+ };
6
11
  exports.RULE_NAME = 'prefer-style-binding';
12
+ const CSS_UNITS = [
13
+ 'px',
14
+ 'em',
15
+ 'rem',
16
+ '%',
17
+ 'vh',
18
+ 'vw',
19
+ 'vmin',
20
+ 'vmax',
21
+ 'cm',
22
+ 'mm',
23
+ 'in',
24
+ 'pt',
25
+ 'pc',
26
+ 'ex',
27
+ 'ch',
28
+ 'fr',
29
+ 'deg',
30
+ 'rad',
31
+ 'grad',
32
+ 'turn',
33
+ 's',
34
+ 'ms',
35
+ 'Hz',
36
+ 'kHz',
37
+ 'dpi',
38
+ 'dpcm',
39
+ 'dppx',
40
+ 'cap',
41
+ 'ic',
42
+ 'lh',
43
+ 'rlh',
44
+ 'vi',
45
+ 'vb',
46
+ 'svw',
47
+ 'svh',
48
+ 'lvw',
49
+ 'lvh',
50
+ 'dvw',
51
+ 'dvh',
52
+ 'cqw',
53
+ 'cqh',
54
+ 'cqi',
55
+ 'cqb',
56
+ 'cqmin',
57
+ 'cqmax',
58
+ 'svmin',
59
+ 'svmax',
60
+ 'lvmin',
61
+ 'lvmax',
62
+ 'dvmin',
63
+ 'dvmax',
64
+ ];
65
+ const CSS_UNITS_PATTERN = CSS_UNITS.join('|');
66
+ const CSS_NUMBER_PATTERN = '[+-]?(?:\\d+(?:\\.\\d+)?|\\.\\d+)';
67
+ const NUMBER_WITH_UNIT = new RegExp('^(?<value>' + CSS_NUMBER_PATTERN + ')(?<unit>' + CSS_UNITS_PATTERN + ')$', 'i');
68
+ const UNIT_ONLY = new RegExp('^(?:' + CSS_UNITS_PATTERN + ')$', 'i');
69
+ const STYLE_KEY_PREFIX = 'style.';
7
70
  exports.default = (0, create_eslint_rule_1.createESLintRule)({
8
71
  name: exports.RULE_NAME,
9
72
  meta: {
@@ -11,13 +74,29 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
11
74
  docs: {
12
75
  description: 'Suggests using [style] bindings over ngStyle where applicable',
13
76
  },
14
- schema: [],
77
+ fixable: 'code',
78
+ hasSuggestions: true,
79
+ schema: [
80
+ {
81
+ type: 'object',
82
+ properties: {
83
+ bindUnits: {
84
+ type: 'boolean',
85
+ description: 'Whether to also report `[style.property]` bindings whose bound value embeds the CSS unit, such as `[style.width]="\'30px\'"`, or a template literal or interpolation ending in the unit after a single expression, which are better expressed with the unit in the binding name (e.g. `[style.width.px]="30"`). Literal values are autofixed; dynamic values are only offered as a suggestion, because a unit binding removes the style when the value is null, undefined or an empty string.',
86
+ default: DEFAULT_OPTIONS.bindUnits,
87
+ },
88
+ },
89
+ additionalProperties: false,
90
+ },
91
+ ],
15
92
  messages: {
16
93
  preferStyleBinding: 'Consider using [style] or [style.property] bindings instead of [ngStyle] where applicable.',
94
+ preferStyleUnitBinding: 'Consider using [style.{{property}}.{{unit}}] instead of embedding the unit in the bound value.',
95
+ suggestStyleUnitBinding: 'Use [style.{{property}}.{{unit}}]. Check null, undefined and empty-string values first: the unit binding removes the style for them instead of appending the unit.',
17
96
  },
18
- defaultOptions: [],
97
+ defaultOptions: [DEFAULT_OPTIONS],
19
98
  },
20
- create(context) {
99
+ create(context, [{ bindUnits }]) {
21
100
  const parserServices = (0, utils_1.getTemplateParserServices)(context);
22
101
  return {
23
102
  'BoundAttribute[name="ngStyle"]'(node) {
@@ -27,9 +106,122 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
27
106
  loc,
28
107
  });
29
108
  },
109
+ ...(bindUnits
110
+ ? {
111
+ BoundAttribute(node) {
112
+ const property = getStyleProperty(node);
113
+ if (!property) {
114
+ return;
115
+ }
116
+ const unitBinding = getUnitBinding(node, context.sourceCode.text);
117
+ if (!unitBinding) {
118
+ return;
119
+ }
120
+ const data = { property, unit: unitBinding.unit };
121
+ const fix = (fixer) => createUnitBindingFix(fixer, node, unitBinding);
122
+ context.report({
123
+ messageId: 'preferStyleUnitBinding',
124
+ loc: parserServices.convertNodeSourceSpanToLoc(node.sourceSpan),
125
+ data,
126
+ ...(unitBinding.value === null
127
+ ? {}
128
+ : unitBinding.isLiteral
129
+ ? { fix }
130
+ : {
131
+ suggest: [
132
+ { messageId: 'suggestStyleUnitBinding', data, fix },
133
+ ],
134
+ }),
135
+ });
136
+ },
137
+ }
138
+ : {}),
30
139
  };
31
140
  },
32
141
  });
142
+ function getStyleProperty(node) {
143
+ const details = node.keySpan?.details;
144
+ if (node.unit || !details?.startsWith(STYLE_KEY_PREFIX)) {
145
+ return null;
146
+ }
147
+ return details.slice(STYLE_KEY_PREFIX.length) || null;
148
+ }
149
+ function getUnitBinding(node, templateText) {
150
+ const withSource = asASTWithSource(node.value);
151
+ const value = (0, unwrap_parenthesized_expression_1.unwrapParenthesizedExpression)(withSource?.ast ?? node.value);
152
+ if ((0, literal_primitive_1.isStringLiteralPrimitive)(value)) {
153
+ const groups = NUMBER_WITH_UNIT.exec(value.value)?.groups;
154
+ return groups?.['unit'] && groups['value']
155
+ ? {
156
+ unit: groups['unit'].toLowerCase(),
157
+ value: groups['value'],
158
+ isLiteral: true,
159
+ }
160
+ : null;
161
+ }
162
+ const verbatim = getVerbatimValue(node, withSource, templateText);
163
+ if ((0, literal_primitive_1.isTemplateLiteral)(value)) {
164
+ const unit = getTrailingUnit(value.elements.map(({ text }) => text), value.expressions.length);
165
+ if (!unit) {
166
+ return null;
167
+ }
168
+ const [expression] = value.expressions;
169
+ const { sourceSpan } = expression;
170
+ return {
171
+ unit: unit.toLowerCase(),
172
+ value: verbatim === null
173
+ ? null
174
+ : templateText.slice(sourceSpan.start, sourceSpan.end),
175
+ isLiteral: isNonNullishLiteral(expression),
176
+ };
177
+ }
178
+ if ((0, literal_primitive_1.isInterpolation)(value)) {
179
+ const unit = getTrailingUnit(value.strings, value.expressions.length);
180
+ if (!unit) {
181
+ return null;
182
+ }
183
+ return {
184
+ unit: unit.toLowerCase(),
185
+ value: verbatim === null ? null : verbatim.slice(0, -unit.length),
186
+ isLiteral: isNonNullishLiteral(value.expressions[0]),
187
+ };
188
+ }
189
+ return null;
190
+ }
191
+ function isNonNullishLiteral(expression) {
192
+ const value = (0, unwrap_parenthesized_expression_1.unwrapParenthesizedExpression)(expression);
193
+ return ((0, literal_primitive_1.isLiteralPrimitive)(value) &&
194
+ (typeof value.value === 'number' ||
195
+ (typeof value.value === 'string' && value.value !== '')));
196
+ }
197
+ function getTrailingUnit(strings, expressionCount) {
198
+ return expressionCount === 1 &&
199
+ strings.length === 2 &&
200
+ strings[0] === '' &&
201
+ UNIT_ONLY.test(strings[1])
202
+ ? strings[1]
203
+ : null;
204
+ }
205
+ function getVerbatimValue({ valueSpan }, withSource, templateText) {
206
+ if (!valueSpan) {
207
+ return null;
208
+ }
209
+ const raw = templateText.slice(valueSpan.fullStart.offset, valueSpan.end.offset);
210
+ return raw === withSource?.source ? raw : null;
211
+ }
212
+ function asASTWithSource(value) {
213
+ return 'ast' in value && 'source' in value ? value : null;
214
+ }
215
+ function createUnitBindingFix(fixer, node, { unit, value }) {
216
+ const { keySpan, valueSpan } = node;
217
+ if (!valueSpan || value === null) {
218
+ return null;
219
+ }
220
+ return [
221
+ fixer.insertTextAfterRange([keySpan.start.offset, keySpan.end.offset], `.${unit}`),
222
+ fixer.replaceTextRange([valueSpan.fullStart.offset, valueSpan.end.offset], value),
223
+ ];
224
+ }
33
225
  exports.RULE_DOCS_EXTENSION = {
34
- rationale: 'For simple cases, [style] and [style.property] bindings offer a more straightforward syntax with better performance than ngStyle, and the Angular style guide recommends them over the NgStyle directive. However, ngStyle should still be used when you need mutations on objects, as style bindings compare the bound object by reference and only apply updates when a new object instance is provided. Note that ngStyle also accepts unit suffixes on object keys (for example `{ "max-width.px": width }`), which [style] object bindings do not support; express these as [style.max-width.px] bindings instead. See https://angular.dev/style-guide#prefer-class-and-style-over-ngclass-and-ngstyle and https://angular.dev/guide/templates/binding#css-class-and-style-property-bindings for more information. This rule helps identify potential simplification opportunities but should be applied judiciously based on your specific needs.',
226
+ rationale: 'For simple cases, [style] and [style.property] bindings offer a more straightforward syntax with better performance than ngStyle, and the Angular style guide recommends them over the NgStyle directive. However, ngStyle should still be used when you need mutations on objects, as style bindings compare the bound object by reference and only apply updates when a new object instance is provided. Note that ngStyle also accepts unit suffixes on object keys (for example `{ "max-width.px": width }`), which [style] object bindings do not support; express these as [style.max-width.px] bindings instead. See https://angular.dev/style-guide#prefer-class-and-style-over-ngclass-and-ngstyle and https://angular.dev/guide/templates/binding#css-class-and-style-property-bindings for more information. This rule helps identify potential simplification opportunities but should be applied judiciously based on your specific needs. The opt-in `bindUnits` option additionally flags `[style.property]` bindings that embed the CSS unit in the bound value, such as `[style.width]="\'30px\'"`, or a template literal or interpolation ending in the unit after a single expression, which Angular can express as `[style.property.unit]` bindings instead, keeping the unit in the template rather than in the bound value. Literal values are autofixed. Dynamic values are only offered as a suggestion, because the two forms differ when the value is null, undefined or an empty string: the unit binding removes the style, letting CSS fallbacks such as `var(--gap, 16px)` or other style bindings apply, while the original binding still appends the unit (for example `nullpx` from a template literal, or just `px` from an interpolation).',
35
227
  };
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-template-literal.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-template-literal.ts"],"names":[],"mappings":"AAeA,QAAA,MAAM,SAAS,0BAA0B,CAAC;AAE1C,MAAM,MAAM,OAAO,GAAG,EAAE,CAAC;AACzB,MAAM,MAAM,UAAU,GAAG,OAAO,SAAS,CAAC;AAC1C,eAAO,MAAM,SAAS,4BAA4B,CAAC;;;;AA6FnD,wBAiKG;AAEH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
1
+ {"version":3,"file":"prefer-template-literal.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-template-literal.ts"],"names":[],"mappings":"AAeA,QAAA,MAAM,SAAS,0BAA0B,CAAC;AAE1C,MAAM,MAAM,OAAO,GAAG,EAAE,CAAC;AACzB,MAAM,MAAM,UAAU,GAAG,OAAO,SAAS,CAAC;AAC1C,eAAO,MAAM,SAAS,4BAA4B,CAAC;;;;AAoGnD,wBAiKG;AAEH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
@@ -75,6 +75,12 @@ function flattenBinaryConcat(node) {
75
75
  // Any other expression - use the unwrapped node (without parentheses)
76
76
  return [{ type: 'expression', node: unwrapped }];
77
77
  }
78
+ function escapeLiteral(value, quote) {
79
+ const escaped = value
80
+ .replaceAll('\\', '\\\\')
81
+ .replaceAll(quote, `\\${quote}`);
82
+ return quote === '`' ? escaped.replaceAll('${', '\\${') : escaped;
83
+ }
78
84
  exports.default = (0, create_eslint_rule_1.createESLintRule)({
79
85
  name: exports.RULE_NAME,
80
86
  meta: {
@@ -168,17 +174,20 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
168
174
  // Build the replacement string from all parts
169
175
  const effectiveQuote = quote === '' ? '`' : quote;
170
176
  let replacement = '';
177
+ let pendingLiteral = '';
171
178
  for (const part of parts) {
172
179
  if (part.type === 'literal') {
173
- // Escape the quote character in the value
174
- replacement += part.value.replaceAll(effectiveQuote, `\\${effectiveQuote}`);
180
+ pendingLiteral += part.value;
175
181
  }
176
182
  else {
183
+ replacement += escapeLiteral(pendingLiteral, effectiveQuote);
184
+ pendingLiteral = '';
177
185
  // Expression - wrap in ${}
178
186
  const exprText = sourceCode.text.slice(part.node.sourceSpan.start, part.node.sourceSpan.end);
179
187
  replacement += `\${${exprText}}`;
180
188
  }
181
189
  }
190
+ replacement += escapeLiteral(pendingLiteral, effectiveQuote);
182
191
  // Add quotes if not inside a template literal
183
192
  if (!parentIsTemplateLiteral) {
184
193
  replacement = `${quote}${replacement}${quote}`;
@@ -1,5 +1,5 @@
1
1
  export type Options = [];
2
- export type MessageIds = 'validAria' | 'validAriaValue' | 'suggestRemoveInvalidAria';
2
+ export type MessageIds = 'validAria' | 'validAriaValue' | 'suggestRemoveInvalidAria' | 'invalidAttrBindingName' | 'suggestRenameAria';
3
3
  export declare const RULE_NAME = "valid-aria";
4
4
  declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<MessageIds, [], import("../utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
5
5
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"valid-aria.d.ts","sourceRoot":"","sources":["../../src/rules/valid-aria.ts"],"names":[],"mappings":"AAgBA,MAAM,MAAM,OAAO,GAAG,EAAE,CAAC;AACzB,MAAM,MAAM,UAAU,GACpB,WAAW,GAAG,gBAAgB,GAAG,0BAA0B,CAAC;AAC9D,eAAO,MAAM,SAAS,eAAe,CAAC;;;;AAEtC,wBA8EG;AA+FH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
1
+ {"version":3,"file":"valid-aria.d.ts","sourceRoot":"","sources":["../../src/rules/valid-aria.ts"],"names":[],"mappings":"AAiBA,MAAM,MAAM,OAAO,GAAG,EAAE,CAAC;AACzB,MAAM,MAAM,UAAU,GAClB,WAAW,GACX,gBAAgB,GAChB,0BAA0B,GAC1B,wBAAwB,GACxB,mBAAmB,CAAC;AACxB,eAAO,MAAM,SAAS,eAAe,CAAC;;;;AAMtC,wBAgIG;AAyGH,eAAO,MAAM,mBAAmB;;CAG/B,CAAC"}
@@ -21,6 +21,8 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
21
21
  validAria: 'The `{{attribute}}` is an invalid ARIA attribute',
22
22
  validAriaValue: 'The `{{attribute}}` has an invalid value. Check the valid values at https://raw.githack.com/w3c/aria/stable/#roles',
23
23
  suggestRemoveInvalidAria: 'Remove attribute `{{attribute}}`',
24
+ invalidAttrBindingName: '`attr.{{attribute}}` sets a literal attribute named `{{attribute}}`; use the hyphenated `attr.{{suggested}}` instead',
25
+ suggestRenameAria: 'Rename to `{{suggested}}`',
24
26
  },
25
27
  defaultOptions: [],
26
28
  },
@@ -32,27 +34,59 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
32
34
  ...domElements,
33
35
  ...uppercaseDomElements,
34
36
  ]);
37
+ function reportInvalidAria(node) {
38
+ const { name: attribute, sourceSpan } = node;
39
+ context.report({
40
+ loc: parserServices.convertNodeSourceSpanToLoc(sourceSpan),
41
+ messageId: 'validAria',
42
+ data: { attribute },
43
+ suggest: [
44
+ {
45
+ messageId: 'suggestRemoveInvalidAria',
46
+ data: { attribute },
47
+ fix: (fixer) => fixer.removeRange([
48
+ sourceSpan.start.offset - 1,
49
+ sourceSpan.end.offset,
50
+ ]),
51
+ },
52
+ ],
53
+ });
54
+ }
35
55
  return {
36
- [`Element[name=${elementNamePattern}] > :matches(BoundAttribute, TextAttribute)[name=/^aria-.+/]`](node) {
56
+ 'Element > BoundAttribute[name=/^aria[A-Z]/]'(node) {
57
+ if (!isAttributeBinding(node)) {
58
+ return;
59
+ }
60
+ const { name: attribute, sourceSpan, keySpan } = node;
61
+ const suggested = `aria-${attribute.slice('aria'.length).toLowerCase()}`;
62
+ // Only suggest a rename when the hyphenated name is a real ARIA attribute.
63
+ if (!aria_query_1.aria.get(suggested)) {
64
+ reportInvalidAria(node);
65
+ return;
66
+ }
67
+ context.report({
68
+ loc: parserServices.convertNodeSourceSpanToLoc(sourceSpan),
69
+ messageId: 'invalidAttrBindingName',
70
+ data: { attribute, suggested },
71
+ suggest: [
72
+ {
73
+ messageId: 'suggestRenameAria',
74
+ data: { suggested: `attr.${suggested}` },
75
+ fix: (fixer) => fixer.replaceTextRange([keySpan.end.offset - attribute.length, keySpan.end.offset], suggested),
76
+ },
77
+ ],
78
+ });
79
+ },
80
+ [`Element[name=${elementNamePattern}] > :matches(BoundAttribute, TextAttribute)[name=/^[aA][rR][iI][aA]-.+/]`](node) {
37
81
  const { name: attribute, sourceSpan } = node;
38
- const ariaPropertyDefinition = aria_query_1.aria.get(attribute);
82
+ // Only `attr.` bindings are matched case-insensitively.
83
+ if (!attribute.startsWith('aria-') && !isAttributeBinding(node)) {
84
+ return;
85
+ }
86
+ const ariaPropertyDefinition = aria_query_1.aria.get(attribute.toLowerCase());
39
87
  const loc = parserServices.convertNodeSourceSpanToLoc(sourceSpan);
40
88
  if (!ariaPropertyDefinition) {
41
- context.report({
42
- loc,
43
- messageId: 'validAria',
44
- data: { attribute },
45
- suggest: [
46
- {
47
- messageId: 'suggestRemoveInvalidAria',
48
- data: { attribute },
49
- fix: (fixer) => fixer.removeRange([
50
- sourceSpan.start.offset - 1,
51
- sourceSpan.end.offset,
52
- ]),
53
- },
54
- ],
55
- });
89
+ reportInvalidAria(node);
56
90
  return;
57
91
  }
58
92
  const ast = extractASTFrom(node);
@@ -69,6 +103,11 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
69
103
  };
70
104
  },
71
105
  });
106
+ function isAttributeBinding(node) {
107
+ return (node instanceof bundled_angular_compiler_1.TmplAstBoundAttribute &&
108
+ node.__originalType ===
109
+ bundled_angular_compiler_1.BindingType.Attribute);
110
+ }
72
111
  function isLiteralCollection(ast) {
73
112
  return ast instanceof bundled_angular_compiler_1.LiteralArray || ast instanceof bundled_angular_compiler_1.LiteralMap;
74
113
  }
@@ -1,8 +1,10 @@
1
- import { AST, LiteralPrimitive } from '@angular-eslint/bundled-angular-compiler';
1
+ import { AST, Interpolation, LiteralPrimitive, TemplateLiteral } from '@angular-eslint/bundled-angular-compiler';
2
2
  export type Quote = "'" | '"' | '`';
3
3
  export declare function isLiteralPrimitive(node: AST): node is LiteralPrimitive;
4
4
  export declare function isStringLiteralPrimitive(node: AST): node is Omit<LiteralPrimitive, 'value'> & {
5
5
  value: string;
6
6
  };
7
+ export declare function isTemplateLiteral(node: AST): node is TemplateLiteral;
8
+ export declare function isInterpolation(node: AST): node is Interpolation;
7
9
  export declare function getLiteralPrimitiveStringValue(node: LiteralPrimitive, quote: Quote): string;
8
10
  //# sourceMappingURL=literal-primitive.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"literal-primitive.d.ts","sourceRoot":"","sources":["../../src/utils/literal-primitive.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,GAAG,EACH,gBAAgB,EACjB,MAAM,0CAA0C,CAAC;AAElD,MAAM,MAAM,KAAK,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAEpC,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,IAAI,gBAAgB,CAStE;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,GAAG,GACR,IAAI,IAAI,IAAI,CAAC,gBAAgB,EAAE,OAAO,CAAC,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAE7D;AAED,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,gBAAgB,EACtB,KAAK,EAAE,KAAK,GACX,MAAM,CAIR"}
1
+ {"version":3,"file":"literal-primitive.d.ts","sourceRoot":"","sources":["../../src/utils/literal-primitive.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,GAAG,EACH,aAAa,EACb,gBAAgB,EAChB,eAAe,EAChB,MAAM,0CAA0C,CAAC;AAElD,MAAM,MAAM,KAAK,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAEpC,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,IAAI,gBAAgB,CAStE;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,GAAG,GACR,IAAI,IAAI,IAAI,CAAC,gBAAgB,EAAE,OAAO,CAAC,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAE7D;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,IAAI,eAAe,CAMpE;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,IAAI,aAAa,CAMhE;AAED,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,gBAAgB,EACtB,KAAK,EAAE,KAAK,GACX,MAAM,CAIR"}
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isLiteralPrimitive = isLiteralPrimitive;
4
4
  exports.isStringLiteralPrimitive = isStringLiteralPrimitive;
5
+ exports.isTemplateLiteral = isTemplateLiteral;
6
+ exports.isInterpolation = isInterpolation;
5
7
  exports.getLiteralPrimitiveStringValue = getLiteralPrimitiveStringValue;
6
8
  const bundled_angular_compiler_1 = require("@angular-eslint/bundled-angular-compiler");
7
9
  function isLiteralPrimitive(node) {
@@ -15,6 +17,16 @@ function isLiteralPrimitive(node) {
15
17
  function isStringLiteralPrimitive(node) {
16
18
  return isLiteralPrimitive(node) && typeof node.value === 'string';
17
19
  }
20
+ function isTemplateLiteral(node) {
21
+ return (!!node &&
22
+ (node.type === 'TemplateLiteral' ||
23
+ node instanceof bundled_angular_compiler_1.TemplateLiteral));
24
+ }
25
+ function isInterpolation(node) {
26
+ return (!!node &&
27
+ (node.type === 'Interpolation' ||
28
+ node instanceof bundled_angular_compiler_1.Interpolation));
29
+ }
18
30
  function getLiteralPrimitiveStringValue(node, quote) {
19
31
  return typeof node.value === 'string'
20
32
  ? `${node.value.replaceAll(quote, `\\${quote}`)}`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular-eslint/eslint-plugin-template",
3
- "version": "22.6.1-alpha.2",
3
+ "version": "22.6.1-alpha.3",
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.6.1-alpha.2",
23
- "@angular-eslint/utils": "22.6.1-alpha.2",
22
+ "@angular-eslint/bundled-angular-compiler": "22.6.1-alpha.3",
23
+ "@angular-eslint/utils": "22.6.1-alpha.3",
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.6.1-alpha.2",
28
+ "@angular-eslint/test-utils": "22.6.1-alpha.3",
29
29
  "@types/aria-query": "5.0.4"
30
30
  },
31
31
  "peerDependencies": {
32
- "@angular-eslint/template-parser": "22.6.1-alpha.2",
32
+ "@angular-eslint/template-parser": "22.6.1-alpha.3",
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",