@angular-eslint/eslint-plugin-template 19.2.2-alpha.3 → 19.2.2-alpha.4

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,3 +1,4 @@
1
+ import type { TSESLint } from '@typescript-eslint/utils';
1
2
  export declare enum OrderType {
2
3
  TemplateReferenceVariable = "TEMPLATE_REFERENCE",
3
4
  StructuralDirective = "STRUCTURAL_DIRECTIVE",
@@ -14,6 +15,6 @@ export type Options = [
14
15
  ];
15
16
  export type MessageIds = 'attributesOrder';
16
17
  export declare const RULE_NAME = "attributes-order";
17
- declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"attributesOrder", Options, import("../utils/create-eslint-rule").RuleDocs, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
18
+ declare const _default: TSESLint.RuleModule<"attributesOrder", Options, import("../utils/create-eslint-rule").RuleDocs, TSESLint.RuleListener>;
18
19
  export default _default;
19
20
  //# sourceMappingURL=attributes-order.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"attributes-order.d.ts","sourceRoot":"","sources":["../../src/rules/attributes-order.ts"],"names":[],"mappings":"AAiBA,oBAAY,SAAS;IACnB,yBAAyB,uBAAuB;IAChD,mBAAmB,yBAAyB;IAC5C,gBAAgB,sBAAsB;IACtC,YAAY,kBAAkB;IAC9B,aAAa,mBAAmB;IAChC,aAAa,oBAAoB;CAClC;AAED,MAAM,MAAM,OAAO,GAAG;IACpB;QACE,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;QAC/B,QAAQ,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,CAAC;KACtC;CACF,CAAC;AA6BF,MAAM,MAAM,UAAU,GAAG,iBAAiB,CAAC;AAC3C,eAAO,MAAM,SAAS,qBAAqB,CAAC;;AAgB5C,wBAmJG"}
1
+ {"version":3,"file":"attributes-order.d.ts","sourceRoot":"","sources":["../../src/rules/attributes-order.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAY,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAGnE,oBAAY,SAAS;IACnB,yBAAyB,uBAAuB;IAChD,mBAAmB,yBAAyB;IAC5C,gBAAgB,sBAAsB;IACtC,YAAY,kBAAkB;IAC9B,aAAa,mBAAmB;IAChC,aAAa,oBAAoB;CAClC;AAED,MAAM,MAAM,OAAO,GAAG;IACpB;QACE,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;QAC/B,QAAQ,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,CAAC;KACtC;CACF,CAAC;AAkCF,MAAM,MAAM,UAAU,GAAG,iBAAiB,CAAC;AAC3C,eAAO,MAAM,SAAS,qBAAqB,CAAC;;AAgB5C,wBA4HG"}
@@ -64,38 +64,14 @@ exports.default = (0, create_eslint_rule_1.createESLintRule)({
64
64
  create(context, [{ alphabetical, order }]) {
65
65
  const parserServices = (0, utils_1.getTemplateParserServices)(context);
66
66
  function getLocation(attr) {
67
- const loc = parserServices.convertNodeSourceSpanToLoc(attr.sourceSpan);
68
- switch (attr.orderType) {
69
- case OrderType.StructuralDirective:
70
- return {
71
- start: {
72
- line: loc.start.line,
73
- column: loc.start.column - 1,
74
- },
75
- end: {
76
- line: loc.end.line,
77
- column: loc.end.column + (isValuelessStructuralDirective(attr) ? 0 : 1),
78
- },
79
- };
80
- default:
81
- return loc;
82
- }
67
+ return adjustLocation(parserServices.convertNodeSourceSpanToLoc(attr.sourceSpan), 'location', attr);
83
68
  }
84
69
  return {
85
70
  ['Element$1, Template'](node) {
86
71
  if (isImplicitTemplate(node)) {
87
72
  return;
88
73
  }
89
- const { attributes, inputs, outputs, references } = node;
90
- const { extractedBananaBoxes, extractedInputs, extractedOutputs } = normalizeInputsOutputs(inputs.map(toInputBindingOrderType), outputs.map(toOutputBindingOrderType));
91
- const allAttributes = [
92
- ...extractTemplateAttrs(node),
93
- ...attributes.map(toAttributeBindingOrderType),
94
- ...references.map(toTemplateReferenceVariableOrderType),
95
- ...extractedBananaBoxes,
96
- ...extractedInputs,
97
- ...extractedOutputs,
98
- ];
74
+ const allAttributes = getAllAttributes(node, context.filename, context.sourceCode);
99
75
  if (allAttributes.length < 2) {
100
76
  return;
101
77
  }
@@ -174,6 +150,50 @@ function byOrder(order, alphabetical) {
174
150
  function getOrderIndex(attr, order) {
175
151
  return order.indexOf(attr.orderType);
176
152
  }
153
+ function getAllAttributes(node, filename, sourceCode) {
154
+ // If there are any i18n attributes (either associated with the
155
+ // element itself, or with any attribute or input), then we need
156
+ // to use the HTML parser to get the attributes because the i18n
157
+ // metadata does not contain the spans of the i18n attributes.
158
+ if (node.i18n) {
159
+ return getAttributesFromHtmlParser(node, filename, node.inputs);
160
+ }
161
+ const { attributes, inputs, outputs, references } = node;
162
+ const extendedInputs = [];
163
+ const attributeBindings = [];
164
+ for (const input of inputs) {
165
+ if (input.i18n) {
166
+ return getAttributesFromHtmlParser(node, filename, node.inputs);
167
+ }
168
+ // Some attributes are parsed as inputs by the Angular template parser,
169
+ // but they don't have square brackets. We don't want those attributes
170
+ // to be classified as inputs because they look like regular attributes.
171
+ // The name of the input will never include the square brackets, so we
172
+ // need to look at the source. Unfortunately, the key span also doesn't
173
+ // include the square brackets, so the source span is what we need to use.
174
+ if (sourceCode.text.at(input.sourceSpan.start.offset) === '[') {
175
+ extendedInputs.push(toInputBindingOrderType(input));
176
+ }
177
+ else {
178
+ attributeBindings.push(toAttributeBindingOrderType(input));
179
+ }
180
+ }
181
+ for (const attribute of attributes) {
182
+ if (attribute.i18n) {
183
+ return getAttributesFromHtmlParser(node, filename, node.inputs);
184
+ }
185
+ attributeBindings.push(toAttributeBindingOrderType(attribute));
186
+ }
187
+ const { extractedBananaBoxes, extractedInputs, extractedOutputs } = normalizeInputsOutputs(extendedInputs, outputs.map(toOutputBindingOrderType));
188
+ return [
189
+ ...extractTemplateAttrs(node),
190
+ ...attributeBindings,
191
+ ...references.map(toTemplateReferenceVariableOrderType),
192
+ ...extractedBananaBoxes,
193
+ ...extractedInputs,
194
+ ...extractedOutputs,
195
+ ];
196
+ }
177
197
  function toAttributeBindingOrderType(attribute) {
178
198
  return {
179
199
  ...attribute,
@@ -286,7 +306,7 @@ function getMessageName(expected) {
286
306
  case OrderType.TemplateReferenceVariable:
287
307
  return `#${fullName}`;
288
308
  case OrderType.InputBinding:
289
- return `[${fullName}]`;
309
+ return expected.isI18nForAttribute ? fullName : `[${fullName}]`;
290
310
  case OrderType.OutputBinding:
291
311
  return `(${fullName})`;
292
312
  case OrderType.TwoWayBinding:
@@ -314,19 +334,116 @@ function isValuelessStructuralDirective(attr) {
314
334
  attrSpan.end.col === keySpan.end.col);
315
335
  }
316
336
  function getStartPos(expected) {
317
- switch (expected.orderType) {
318
- case OrderType.StructuralDirective:
319
- return expected.sourceSpan.start.offset - 1;
320
- default:
321
- return expected.sourceSpan.start.offset;
322
- }
337
+ return adjustLocation(expected.sourceSpan.start.offset, 'start', expected);
323
338
  }
324
339
  function getEndPos(expected) {
325
- switch (expected.orderType) {
326
- case OrderType.StructuralDirective:
327
- return (expected.sourceSpan.end.offset +
328
- (isValuelessStructuralDirective(expected) ? 0 : 1));
329
- default:
330
- return expected.sourceSpan.end.offset;
340
+ return adjustLocation(expected.sourceSpan.end.offset, 'end', expected);
341
+ }
342
+ function adjustLocation(locOrOffset, kind, attr) {
343
+ // Spans for structural directives created from the
344
+ // template parser will exclude the leading "*", so
345
+ // we need to move the start back to include it.
346
+ if (!attr.fromHtmlParser &&
347
+ attr.orderType === OrderType.StructuralDirective) {
348
+ if (typeof locOrOffset === 'number') {
349
+ if (kind === 'start') {
350
+ return locOrOffset - 1;
351
+ }
352
+ else {
353
+ return locOrOffset + (isValuelessStructuralDirective(attr) ? 0 : 1);
354
+ }
355
+ }
356
+ else {
357
+ return {
358
+ start: {
359
+ line: locOrOffset.start.line,
360
+ column: locOrOffset.start.column - 1,
361
+ },
362
+ end: {
363
+ line: locOrOffset.end.line,
364
+ column: locOrOffset.end.column +
365
+ (isValuelessStructuralDirective(attr) ? 0 : 1),
366
+ },
367
+ };
368
+ }
369
+ }
370
+ return locOrOffset;
371
+ }
372
+ function getAttributesFromHtmlParser(node, filename, inputs) {
373
+ // The template AST does not include the spans for i18n attributes.
374
+ // To get their spans, we can re-parse just the element as HTML.
375
+ // We only need the spans of the attributes, so take only the
376
+ // start element and the end element (if there is one) so that
377
+ // we don't waste time parsing the element's content.
378
+ let html = node.startSourceSpan.toString();
379
+ if (node.endSourceSpan) {
380
+ html += node.endSourceSpan.toString();
381
+ }
382
+ const parser = new bundled_angular_compiler_1.HtmlParser();
383
+ const tree = parser.parse(html, filename, { preserveLineEndings: true });
384
+ const element = tree.rootNodes.find((n) => n instanceof bundled_angular_compiler_1.Element);
385
+ if (element) {
386
+ return element.attrs.map((attribute) => ({
387
+ ...getHtmlAttributeNameAndOrderType(attribute, inputs),
388
+ fromHtmlParser: true,
389
+ // The HTML element was at the start of the string which means the
390
+ // offset of each element will be relative to the start of the element.
391
+ // To get the offset of the attribute in the template, we need to
392
+ // move each span forward by the offset of the span in the template.
393
+ sourceSpan: new bundled_angular_compiler_1.ParseSourceSpan(node.startSourceSpan.start.moveBy(attribute.sourceSpan.start.offset), node.startSourceSpan.start.moveBy(attribute.sourceSpan.end.offset)),
394
+ keySpan: attribute.keySpan
395
+ ? new bundled_angular_compiler_1.ParseSourceSpan(node.startSourceSpan.start.moveBy(attribute.keySpan.start.offset), node.startSourceSpan.start.moveBy(attribute.keySpan.end.offset))
396
+ : undefined,
397
+ }));
398
+ }
399
+ return [];
400
+ }
401
+ function getHtmlAttributeNameAndOrderType(attr, inputs) {
402
+ if (attr.name.startsWith('#')) {
403
+ return {
404
+ name: attr.name.slice(1),
405
+ orderType: OrderType.TemplateReferenceVariable,
406
+ };
407
+ }
408
+ if (attr.name.startsWith('*')) {
409
+ return {
410
+ name: attr.name.slice(1),
411
+ orderType: OrderType.StructuralDirective,
412
+ };
331
413
  }
414
+ if (attr.name.startsWith('[(') && attr.name.endsWith(')]')) {
415
+ return {
416
+ name: attr.name.slice(2, -2),
417
+ orderType: OrderType.TwoWayBinding,
418
+ };
419
+ }
420
+ if (attr.name.startsWith('[') && attr.name.endsWith(']')) {
421
+ return {
422
+ name: attr.name.slice(1, -1),
423
+ orderType: OrderType.InputBinding,
424
+ };
425
+ }
426
+ if (attr.name.startsWith('(') && attr.name.endsWith(')')) {
427
+ return {
428
+ name: attr.name.slice(1, -1),
429
+ orderType: OrderType.OutputBinding,
430
+ };
431
+ }
432
+ const isI18nForAttribute = attr.name.startsWith('i18n-');
433
+ let orderType = OrderType.AttributeBinding;
434
+ // If the attribute is an i18n attribute, and it is associated with
435
+ // an input binding, then treat it as an input binding for ordering,
436
+ // even though it is a regular attribute. This will keep the i18n
437
+ // attribute immediately after its corresponding input binding.
438
+ if (inputs.length > 0 && isI18nForAttribute) {
439
+ const correspondingName = attr.name.slice(5);
440
+ if (inputs.some((input) => input.name === correspondingName)) {
441
+ orderType = OrderType.InputBinding;
442
+ }
443
+ }
444
+ return {
445
+ name: attr.name,
446
+ orderType,
447
+ isI18nForAttribute,
448
+ };
332
449
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular-eslint/eslint-plugin-template",
3
- "version": "19.2.2-alpha.3",
3
+ "version": "19.2.2-alpha.4",
4
4
  "description": "ESLint plugin for Angular Templates",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -20,13 +20,13 @@
20
20
  "dependencies": {
21
21
  "aria-query": "5.3.2",
22
22
  "axobject-query": "4.1.0",
23
- "@angular-eslint/bundled-angular-compiler": "19.2.2-alpha.3",
24
- "@angular-eslint/utils": "19.2.2-alpha.3"
23
+ "@angular-eslint/utils": "19.2.2-alpha.4",
24
+ "@angular-eslint/bundled-angular-compiler": "19.2.2-alpha.4"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/aria-query": "5.0.4",
28
- "@angular-eslint/template-parser": "19.2.2-alpha.3",
29
- "@angular-eslint/test-utils": "19.2.2-alpha.3"
28
+ "@angular-eslint/template-parser": "19.2.2-alpha.4",
29
+ "@angular-eslint/test-utils": "19.2.2-alpha.4"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "@typescript-eslint/types": "^7.11.0 || ^8.0.0",