Skip to content

Commit 90c0e91

Browse files
fix(eslint-plugin-template): [attributes-order] fixes for structural directives and "dotted" names (#1448)
1 parent 02c2a97 commit 90c0e91

File tree

3 files changed

+319
-13
lines changed

3 files changed

+319
-13
lines changed

packages/eslint-plugin-template/docs/rules/attributes-order.md

+168
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,36 @@ interface Options {
109109

110110
<br>
111111

112+
#### Custom Config
113+
114+
```json
115+
{
116+
"rules": {
117+
"@angular-eslint/template/attributes-order": [
118+
"error",
119+
{
120+
"alphabetical": true
121+
}
122+
]
123+
}
124+
}
125+
```
126+
127+
<br>
128+
129+
#### ❌ Invalid Code
130+
131+
```html
132+
<li><input type="text" id="input"></li>
133+
~~~~~~~~~~~~~~~~~~~~~~
134+
```
135+
136+
<br>
137+
138+
---
139+
140+
<br>
141+
112142
#### Default Config
113143

114144
```json
@@ -339,6 +369,144 @@ interface Options {
339369
~~~~~~~~~~~~~~~~
340370
```
341371

372+
<br>
373+
374+
---
375+
376+
<br>
377+
378+
#### Default Config
379+
380+
```json
381+
{
382+
"rules": {
383+
"@angular-eslint/template/attributes-order": [
384+
"error"
385+
]
386+
}
387+
}
388+
```
389+
390+
<br>
391+
392+
#### ❌ Invalid Code
393+
394+
```html
395+
<ng-container (click)="bar = []" id="issue" *ngFor="let foo of bar"></ng-container>
396+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
397+
```
398+
399+
<br>
400+
401+
---
402+
403+
<br>
404+
405+
#### Default Config
406+
407+
```json
408+
{
409+
"rules": {
410+
"@angular-eslint/template/attributes-order": [
411+
"error"
412+
]
413+
}
414+
}
415+
```
416+
417+
<br>
418+
419+
#### ❌ Invalid Code
420+
421+
```html
422+
<ng-container (click)="bar = []" id="issue" *ngFor="let foo of bar; index as i; first as isFirst"></ng-container>
423+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
424+
```
425+
426+
<br>
427+
428+
---
429+
430+
<br>
431+
432+
#### Default Config
433+
434+
```json
435+
{
436+
"rules": {
437+
"@angular-eslint/template/attributes-order": [
438+
"error"
439+
]
440+
}
441+
}
442+
```
443+
444+
<br>
445+
446+
#### ❌ Invalid Code
447+
448+
```html
449+
<div id="id" *ngIf="bar as foo"></div>
450+
~~~~~~~~~~~~~~~~~~~~~~~~~~
451+
```
452+
453+
<br>
454+
455+
---
456+
457+
<br>
458+
459+
#### Default Config
460+
461+
```json
462+
{
463+
"rules": {
464+
"@angular-eslint/template/attributes-order": [
465+
"error"
466+
]
467+
}
468+
}
469+
```
470+
471+
<br>
472+
473+
#### ❌ Invalid Code
474+
475+
```html
476+
<div id="id" *ngIf="condition then foo else bar"></div>
477+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
478+
```
479+
480+
<br>
481+
482+
---
483+
484+
<br>
485+
486+
#### Custom Config
487+
488+
```json
489+
{
490+
"rules": {
491+
"@angular-eslint/template/attributes-order": [
492+
"error",
493+
{
494+
"alphabetical": true
495+
}
496+
]
497+
}
498+
}
499+
```
500+
501+
<br>
502+
503+
#### ❌ Invalid Code
504+
505+
```html
506+
<div [disabled]="disabled" [class.disabled]="disabled"></div>
507+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
508+
```
509+
342510
</details>
343511

344512
<br>

packages/eslint-plugin-template/src/rules/attributes-order.ts

+45-11
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import type {
66
TmplAstTextAttribute,
77
TmplAstNode,
88
} from '@angular-eslint/bundled-angular-compiler';
9-
import { TmplAstTemplate } from '@angular-eslint/bundled-angular-compiler';
9+
import {
10+
ParseSourceSpan,
11+
TmplAstTemplate,
12+
} from '@angular-eslint/bundled-angular-compiler';
1013
import { getTemplateParserServices } from '@angular-eslint/utils';
1114
import type { TSESTree } from '@typescript-eslint/utils';
1215
import { createESLintRule } from '../utils/create-eslint-rule';
@@ -227,7 +230,10 @@ function byOrder(order: readonly OrderType[], alphabetical: boolean) {
227230
getOrderIndex(one, order) - getOrderIndex(other, order);
228231

229232
if (alphabetical && orderComparison === 0) {
230-
return one.name > other.name ? 1 : -1;
233+
return (one.keySpan?.details ?? one.name) >
234+
(other.keySpan?.details ?? other.name)
235+
? 1
236+
: -1;
231237
}
232238

233239
return orderComparison;
@@ -280,9 +286,36 @@ function toTemplateReferenceVariableOrderType(reference: TmplAstReference) {
280286
function extractTemplateAttrs(
281287
node: ExtendedTmplAstElement,
282288
): (ExtendedTmplAstBoundAttribute | ExtendedTmplAstTextAttribute)[] {
283-
return isTmplAstTemplate(node.parent)
284-
? node.parent.templateAttrs.map(toStructuralDirectiveOrderType)
285-
: [];
289+
if (!isTmplAstTemplate(node.parent)) {
290+
return [];
291+
}
292+
293+
/*
294+
* There may be multiple "attributes" for a structural directive even though
295+
* there is only a single HTML attribute:
296+
* e.g. `<ng-container *ngFor="let foo of bar"></ng-container>`
297+
* will parsed as two attributes (`ngFor` and `ngForOf`)
298+
*/
299+
300+
const attrs = node.parent.templateAttrs.map(toStructuralDirectiveOrderType);
301+
302+
// Pick up on any subsequent `let` bindings, e.g. `index as i`
303+
let sourceEnd = attrs[attrs.length - 1].sourceSpan.end;
304+
node.parent.variables.forEach((v) => {
305+
if (
306+
v.sourceSpan.start.offset <= sourceEnd.offset &&
307+
sourceEnd.offset < v.sourceSpan.end.offset
308+
) {
309+
sourceEnd = v.sourceSpan.end;
310+
}
311+
});
312+
313+
return [
314+
{
315+
...attrs[0],
316+
sourceSpan: new ParseSourceSpan(attrs[0].sourceSpan.start, sourceEnd),
317+
} as ExtendedTmplAstBoundAttribute | ExtendedTmplAstTextAttribute,
318+
];
286319
}
287320

288321
function normalizeInputsOutputs(
@@ -333,19 +366,20 @@ function isOnSameLocation(
333366
}
334367

335368
function getMessageName(expected: ExtendedAttribute): string {
369+
const fullName = expected.keySpan?.details ?? expected.name;
336370
switch (expected.orderType) {
337371
case OrderType.StructuralDirective:
338-
return `*${expected.name}`;
372+
return `*${fullName}`;
339373
case OrderType.TemplateReferenceVariable:
340-
return `#${expected.name}`;
374+
return `#${fullName}`;
341375
case OrderType.InputBinding:
342-
return `[${expected.name}]`;
376+
return `[${fullName}]`;
343377
case OrderType.OutputBinding:
344-
return `(${expected.name})`;
378+
return `(${fullName})`;
345379
case OrderType.TwoWayBinding:
346-
return `[(${expected.name})]`;
380+
return `[(${fullName})]`;
347381
default:
348-
return expected.name;
382+
return fullName;
349383
}
350384
}
351385

packages/eslint-plugin-template/tests/rules/attributes-order/cases.ts

+106-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { convertAnnotatedSourceToFailureCase } from '@angular-eslint/utils';
2-
import type { MessageIds } from '../../../src/rules/attributes-order';
3-
import { OrderType } from '../../../src/rules/attributes-order';
2+
import {
3+
OrderType,
4+
type MessageIds,
5+
} from '../../../src/rules/attributes-order';
46

57
const messageId: MessageIds = 'attributesOrder';
68

@@ -14,6 +16,27 @@ export const valid = [
1416
];
1517

1618
export const invalid = [
19+
convertAnnotatedSourceToFailureCase({
20+
messageId,
21+
description: 'should work for simple attributes',
22+
annotatedSource: `
23+
<li><input type="text" id="input"></li>
24+
~~~~~~~~~~~~~~~~~~~~~~
25+
`,
26+
options: [
27+
{
28+
alphabetical: true,
29+
},
30+
],
31+
data: {
32+
expected: '`id`, `type`',
33+
actual: '`type`, `id`',
34+
},
35+
annotatedOutput: `
36+
<li><input id="input" type="text"></li>
37+
38+
`,
39+
}),
1740
convertAnnotatedSourceToFailureCase({
1841
messageId,
1942
description: 'should fail if structural directive is not first',
@@ -195,4 +218,85 @@ export const invalid = [
195218
196219
`,
197220
}),
221+
convertAnnotatedSourceToFailureCase({
222+
messageId,
223+
description: 'should work for ngFor',
224+
annotatedSource: `
225+
<ng-container (click)="bar = []" id="issue" *ngFor="let foo of bar"></ng-container>
226+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
227+
`,
228+
data: {
229+
expected: '`*ngFor`, `id`, `(click)`',
230+
actual: '`(click)`, `id`, `*ngFor`',
231+
},
232+
annotatedOutput: `
233+
<ng-container *ngFor="let foo of bar" id="issue" (click)="bar = []"></ng-container>
234+
235+
`,
236+
}),
237+
convertAnnotatedSourceToFailureCase({
238+
messageId,
239+
description: 'should work for ngFor with let',
240+
annotatedSource: `
241+
<ng-container (click)="bar = []" id="issue" *ngFor="let foo of bar; index as i; first as isFirst"></ng-container>
242+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
243+
`,
244+
data: {
245+
expected: '`*ngFor`, `id`, `(click)`',
246+
actual: '`(click)`, `id`, `*ngFor`',
247+
},
248+
annotatedOutput: `
249+
<ng-container *ngFor="let foo of bar; index as i; first as isFirst" id="issue" (click)="bar = []"></ng-container>
250+
251+
`,
252+
}),
253+
convertAnnotatedSourceToFailureCase({
254+
messageId,
255+
description: 'should work for ngIf as',
256+
annotatedSource: `
257+
<div id="id" *ngIf="bar as foo"></div>
258+
~~~~~~~~~~~~~~~~~~~~~~~~~~
259+
`,
260+
data: {
261+
expected: '`*ngIf`, `id`',
262+
actual: '`id`, `*ngIf`',
263+
},
264+
annotatedOutput: `
265+
<div *ngIf="bar as foo" id="id"></div>
266+
267+
`,
268+
}),
269+
convertAnnotatedSourceToFailureCase({
270+
messageId,
271+
description: 'should work for ngIfThenElse',
272+
annotatedSource: `
273+
<div id="id" *ngIf="condition then foo else bar"></div>
274+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
275+
`,
276+
data: {
277+
expected: '`*ngIf`, `id`',
278+
actual: '`id`, `*ngIf`',
279+
},
280+
annotatedOutput: `
281+
<div *ngIf="condition then foo else bar" id="id"></div>
282+
283+
`,
284+
}),
285+
convertAnnotatedSourceToFailureCase({
286+
messageId,
287+
description: 'should work for dotted attributes',
288+
annotatedSource: `
289+
<div [disabled]="disabled" [class.disabled]="disabled"></div>
290+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
291+
`,
292+
options: [{ alphabetical: true }],
293+
data: {
294+
expected: '`[class.disabled]`, `[disabled]`',
295+
actual: '`[disabled]`, `[class.disabled]`',
296+
},
297+
annotatedOutput: `
298+
<div [class.disabled]="disabled" [disabled]="disabled"></div>
299+
300+
`,
301+
}),
198302
];

0 commit comments

Comments
 (0)