@taiga-ui/eslint-plugin-experience-next 0.507.0 → 0.508.0

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/README.md CHANGED
@@ -88,6 +88,7 @@ from third-party plugins. The exact severities and file globs live in
88
88
  | [no-duplicate-attrs](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-duplicate-attrs.md) | Disallow duplicate attributes on the same HTML element | ✅ | | |
89
89
  | [no-duplicate-id](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-duplicate-id.md) | Disallow duplicate static `id` values in HTML templates | ✅ | | |
90
90
  | [no-duplicate-in-head](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-duplicate-in-head.md) | Disallow duplicate `title`, `base`, and key metadata tags inside `<head>` | ✅ | | |
91
+ | [no-empty-style-metadata](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-empty-style-metadata.md) | Remove empty Angular component style metadata | ✅ | 🔧 | |
91
92
  | [no-fully-untracked-effect](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-fully-untracked-effect.md) | Disallow reactive callbacks where all signal reads are hidden inside `untracked()` | ✅ | | |
92
93
  | [no-href-with-router-link](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-href-with-router-link.md) | Do not use href and routerLink attributes together on the same element | ✅ | 🔧 | |
93
94
  | [no-import-assertions](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-import-assertions.md) | Replace legacy `assert { ... }` import assertions with `with { ... }` | ✅ | 🔧 | |
@@ -127,6 +128,28 @@ from third-party plugins. The exact severities and file globs live in
127
128
  | [standalone-imports-sort](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/standalone-imports-sort.md) | Auto sort names inside Angular decorators | ✅ | 🔧 | |
128
129
  | [strict-tui-doc-example](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/strict-tui-doc-example.md) | If you use the addon-doc, there will be a hint that you are importing something incorrectly | | 🔧 | |
129
130
 
131
+ ## no-empty-style-metadata
132
+
133
+ Disallows empty Angular component style metadata. The rule reports empty string and empty template literal values for
134
+ `styles` and `styleUrl`, plus `styleUrls: []`, because they do not add component styles and can be safely removed.
135
+
136
+ ```ts
137
+ // ❌ error
138
+ @Component({
139
+ selector: 'app-root',
140
+ templateUrl: './app.component.html',
141
+ styles: ``,
142
+ styleUrl: '',
143
+ styleUrls: [],
144
+ })
145
+
146
+ // ✅ after autofix
147
+ @Component({
148
+ selector: 'app-root',
149
+ templateUrl: './app.component.html',
150
+ })
151
+ ```
152
+
130
153
  ## prefer-conditional-return
131
154
 
132
155
  Prefer a single conditional return when an `if` statement returns one expression and the immediately following statement
package/index.d.ts CHANGED
@@ -18,7 +18,7 @@ declare const plugin: {
18
18
  'class-property-naming': import("@typescript-eslint/utils/ts-eslint").RuleModule<"invalidName", [import("./rules/taiga-specific/class-property-naming").RuleConfig[]], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
19
19
  name: string;
20
20
  };
21
- 'decorator-key-sort': import("eslint").Rule.RuleModule & {
21
+ 'decorator-key-sort': import("@typescript-eslint/utils/ts-eslint").RuleModule<"incorrectOrder", [Record<string, readonly string[]>], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
22
22
  name: string;
23
23
  };
24
24
  'element-newline': import("eslint").Rule.RuleModule & {
@@ -78,6 +78,9 @@ declare const plugin: {
78
78
  'no-duplicate-in-head': import("eslint").Rule.RuleModule & {
79
79
  name: string;
80
80
  };
81
+ 'no-empty-style-metadata': import("@typescript-eslint/utils/ts-eslint").RuleModule<"noEmptyStyleMetadata", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
82
+ name: string;
83
+ };
81
84
  'no-fully-untracked-effect': import("@typescript-eslint/utils/ts-eslint").RuleModule<"noTrackedReads", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
82
85
  name: string;
83
86
  };
package/index.esm.js CHANGED
@@ -1201,6 +1201,7 @@ var recommended = defineConfig([
1201
1201
  },
1202
1202
  ],
1203
1203
  '@taiga-ui/experience-next/no-deep-imports-to-indexed-packages': 'error',
1204
+ '@taiga-ui/experience-next/no-empty-style-metadata': 'error',
1204
1205
  '@taiga-ui/experience-next/no-fully-untracked-effect': 'error',
1205
1206
  '@taiga-ui/experience-next/no-implicit-public': 'error',
1206
1207
  '@taiga-ui/experience-next/no-import-assertions': 'error',
@@ -46222,7 +46223,7 @@ function buildMultilineStartTag(node, sourceText) {
46222
46223
  closing,
46223
46224
  ].join('\n');
46224
46225
  }
46225
- const rule$R = createRule({
46226
+ const rule$S = createRule({
46226
46227
  name: 'attrs-newline',
46227
46228
  rule: {
46228
46229
  create(context) {
@@ -46308,54 +46309,151 @@ const rule$R = createRule({
46308
46309
  },
46309
46310
  });
46310
46311
 
46312
+ function isObject(node) {
46313
+ return node?.type === dist$2.AST_NODE_TYPES.ObjectExpression;
46314
+ }
46315
+
46316
+ /**
46317
+ * Extracts the metadata object from a class decorator such as
46318
+ * `@Component()`, `@Directive()`, `@NgModule()`, or `@Pipe()`.
46319
+ *
46320
+ * Returns the first argument of the decorator call *if and only if*
46321
+ * it is an `ObjectExpression`.
46322
+ *
46323
+ * @example
46324
+ * // Given:
46325
+ * @Component({
46326
+ * selector: 'x',
46327
+ * imports: [A, B],
46328
+ * })
46329
+ * class MyCmp {}
46330
+ *
46331
+ * // In the AST for @Component(...)
46332
+ * getDecoratorMetadata(decorator, allowed) returns
46333
+ * ObjectExpression({ selector: ..., imports: ... })
46334
+ *
46335
+ * @param decorator - The decorator node attached to a class declaration.
46336
+ * @param allowedNames - A set of decorator names to consider
46337
+ * (e.g., Component, Directive, NgModule, Pipe).
46338
+ *
46339
+ * @returns The metadata `ObjectExpression` if present and valid,
46340
+ * otherwise `null`.
46341
+ */
46342
+ function getDecoratorMetadata(decorator, allowedNames) {
46343
+ return getDecoratorMetadataWithName(decorator, allowedNames)?.metadata ?? null;
46344
+ }
46345
+ function getDecoratorMetadataWithName(decorator, allowedNames) {
46346
+ const expr = decorator.expression;
46347
+ if (expr.type !== dist$2.AST_NODE_TYPES.CallExpression) {
46348
+ return null;
46349
+ }
46350
+ const callee = expr.callee;
46351
+ if (callee.type !== dist$2.AST_NODE_TYPES.Identifier || !allowedNames.has(callee.name)) {
46352
+ return null;
46353
+ }
46354
+ const arg = expr.arguments[0];
46355
+ return isObject(arg) ? { expression: expr, metadata: arg, name: callee.name } : null;
46356
+ }
46357
+
46358
+ function isStringLiteral(node) {
46359
+ return node?.type === dist$3.AST_NODE_TYPES.Literal && typeof node.value === 'string';
46360
+ }
46361
+ function isStaticTemplateLiteral(node) {
46362
+ return (node?.type === dist$3.AST_NODE_TYPES.TemplateLiteral &&
46363
+ node.expressions.length === 0 &&
46364
+ node.quasis.length === 1);
46365
+ }
46366
+ function getStaticStringValue(node) {
46367
+ if (isStringLiteral(node)) {
46368
+ return node.value;
46369
+ }
46370
+ return isStaticTemplateLiteral(node)
46371
+ ? (node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw ?? '')
46372
+ : null;
46373
+ }
46374
+ function isEmptyStaticString(node) {
46375
+ return getStaticStringValue(node) === '';
46376
+ }
46377
+
46378
+ function getStaticPropertyName(key) {
46379
+ if (key.type === dist$3.AST_NODE_TYPES.Identifier) {
46380
+ return key.name;
46381
+ }
46382
+ return key.type === dist$3.AST_NODE_TYPES.Literal &&
46383
+ (typeof key.value === 'string' || typeof key.value === 'number')
46384
+ ? String(key.value)
46385
+ : getStaticStringValue(key);
46386
+ }
46387
+ function getObjectPropertyName(node) {
46388
+ return node.computed ? null : getStaticPropertyName(node.key);
46389
+ }
46390
+ function getMemberExpressionPropertyName(node) {
46391
+ if (!node.computed && node.property.type === dist$3.AST_NODE_TYPES.Identifier) {
46392
+ return node.property.name;
46393
+ }
46394
+ return node.computed ? getStaticStringValue(node.property) : null;
46395
+ }
46396
+ function getClassMemberName(member) {
46397
+ return member.key.type === dist$3.AST_NODE_TYPES.PrivateIdentifier
46398
+ ? null
46399
+ : getStaticPropertyName(member.key);
46400
+ }
46401
+
46311
46402
  function sameOrder(a, b) {
46312
46403
  return a.length === b.length && a.every((value, index) => value === b[index]);
46313
46404
  }
46314
46405
 
46315
- const config$5 = {
46316
- create(context) {
46317
- const order = context.options[0] || {};
46406
+ const rule$R = createRule({
46407
+ create(context, [order]) {
46408
+ const decorators = new Set(Object.keys(order));
46318
46409
  return {
46319
46410
  ClassDeclaration(node) {
46320
- const decorators = Array.from(node.decorators ?? []);
46321
- for (const decorator of decorators) {
46322
- const { expression } = decorator;
46323
- const decoratorName = expression.callee?.name ?? '';
46324
- if (decoratorName in (order || {})) {
46325
- const orderList = order[decoratorName];
46326
- const decoratorArguments = Array.from(expression.arguments ?? []);
46327
- for (const argument of decoratorArguments) {
46328
- const properties = Array.from(argument.properties ?? []);
46329
- const current = properties
46330
- .map((prop) => prop.key?.name)
46331
- .filter(Boolean);
46332
- const correct = getCorrectOrderRelative(orderList, current);
46333
- if (!sameOrder(correct, current.filter((item) => correct.includes(item)))) {
46334
- context.report({
46335
- fix: (fixer) => {
46336
- const fileContent = context.sourceCode.text;
46337
- const forgottenProps = current.filter((key) => !orderList.includes(key));
46338
- const sortedDecoratorProperties = [
46339
- ...correct,
46340
- ...forgottenProps,
46341
- ].map((key) => properties.find((prop) => prop.key.name === key));
46342
- const newDecoratorArgument = `{${sortedDecoratorProperties
46343
- .map(({ range }) => fileContent.slice(...range))
46344
- .toString()}}`;
46345
- return fixer.replaceTextRange(argument.range, newDecoratorArgument);
46346
- },
46347
- message: `Incorrect order keys in @${decoratorName} decorator, please sort by [${correct.join(' -> ')}]`,
46348
- node: expression,
46349
- });
46350
- }
46351
- }
46411
+ for (const decorator of node?.decorators ?? []) {
46412
+ const metadata = getDecoratorMetadataWithName(decorator, decorators);
46413
+ if (!metadata) {
46414
+ continue;
46415
+ }
46416
+ const orderList = order[metadata.name] ?? [];
46417
+ const properties = getDecoratorProperties(metadata.metadata);
46418
+ if (!properties) {
46419
+ continue;
46420
+ }
46421
+ const current = properties.map(({ name }) => name);
46422
+ const correct = getCorrectOrderRelative(orderList, current);
46423
+ const sortableCurrent = current.filter((item) => correct.includes(item));
46424
+ if (sameOrder(correct, sortableCurrent)) {
46425
+ continue;
46352
46426
  }
46427
+ context.report({
46428
+ data: {
46429
+ decorator: metadata.name,
46430
+ order: correct.join(' -> '),
46431
+ },
46432
+ fix: (fixer) => {
46433
+ const forgottenProps = properties.filter(({ name }) => !orderList.includes(name));
46434
+ const sortedDecoratorProperties = [
46435
+ ...correct.flatMap((key) => properties.filter(({ name }) => name === key)),
46436
+ ...forgottenProps,
46437
+ ];
46438
+ const newDecoratorArgument = `{${sortedDecoratorProperties
46439
+ .map(({ node }) => context.sourceCode.text.slice(...node.range))
46440
+ .join(',')}}`;
46441
+ return fixer.replaceTextRange(metadata.metadata.range, newDecoratorArgument);
46442
+ },
46443
+ messageId: 'incorrectOrder',
46444
+ node: metadata.expression,
46445
+ });
46353
46446
  }
46354
46447
  },
46355
46448
  };
46356
46449
  },
46357
46450
  meta: {
46451
+ defaultOptions: [{}],
46452
+ docs: { description: 'Sorts keys of Angular decorator metadata.' },
46358
46453
  fixable: 'code',
46454
+ messages: {
46455
+ incorrectOrder: 'Incorrect order keys in @{{decorator}} decorator, please sort by [{{order}}]',
46456
+ },
46359
46457
  schema: [
46360
46458
  {
46361
46459
  additionalProperties: true,
@@ -46365,14 +46463,25 @@ const config$5 = {
46365
46463
  ],
46366
46464
  type: 'problem',
46367
46465
  },
46368
- };
46466
+ name: 'decorator-key-sort',
46467
+ });
46369
46468
  function getCorrectOrderRelative(correct, current) {
46370
46469
  return correct.filter((item) => current.includes(item));
46371
46470
  }
46372
- const rule$Q = createRule({
46373
- name: 'decorator-key-sort',
46374
- rule: config$5,
46375
- });
46471
+ function getDecoratorProperties(metadata) {
46472
+ const properties = [];
46473
+ for (const property of metadata.properties) {
46474
+ if (property.type !== dist$3.AST_NODE_TYPES.Property) {
46475
+ return null;
46476
+ }
46477
+ const name = getObjectPropertyName(property);
46478
+ if (!name) {
46479
+ return null;
46480
+ }
46481
+ properties.push({ name, node: property });
46482
+ }
46483
+ return properties;
46484
+ }
46376
46485
 
46377
46486
  const INLINE_HTML_ELEMENTS = new Set([
46378
46487
  'a',
@@ -46430,7 +46539,7 @@ function getNodeLabel(node) {
46430
46539
  }
46431
46540
  return node instanceof dist$4.TmplAstBoundText ? 'binding' : 'text';
46432
46541
  }
46433
- const rule$P = createRule({
46542
+ const rule$Q = createRule({
46434
46543
  name: 'element-newline',
46435
46544
  rule: {
46436
46545
  create(context) {
@@ -46504,93 +46613,6 @@ const rule$P = createRule({
46504
46613
  },
46505
46614
  });
46506
46615
 
46507
- function isObject(node) {
46508
- return node?.type === dist$2.AST_NODE_TYPES.ObjectExpression;
46509
- }
46510
-
46511
- /**
46512
- * Extracts the metadata object from a class decorator such as
46513
- * `@Component()`, `@Directive()`, `@NgModule()`, or `@Pipe()`.
46514
- *
46515
- * Returns the first argument of the decorator call *if and only if*
46516
- * it is an `ObjectExpression`.
46517
- *
46518
- * @example
46519
- * // Given:
46520
- * @Component({
46521
- * selector: 'x',
46522
- * imports: [A, B],
46523
- * })
46524
- * class MyCmp {}
46525
- *
46526
- * // In the AST for @Component(...)
46527
- * getDecoratorMetadata(decorator, allowed) →
46528
- * ObjectExpression({ selector: ..., imports: ... })
46529
- *
46530
- * @param decorator - The decorator node attached to a class declaration.
46531
- * @param allowedNames - A set of decorator names to consider
46532
- * (e.g., Component, Directive, NgModule, Pipe).
46533
- *
46534
- * @returns The metadata `ObjectExpression` if present and valid,
46535
- * otherwise `null`.
46536
- */
46537
- function getDecoratorMetadata(decorator, allowedNames) {
46538
- const expr = decorator.expression;
46539
- if (expr.type !== dist$2.AST_NODE_TYPES.CallExpression) {
46540
- return null;
46541
- }
46542
- const callee = expr.callee;
46543
- if (callee.type !== dist$2.AST_NODE_TYPES.Identifier || !allowedNames.has(callee.name)) {
46544
- return null;
46545
- }
46546
- const arg = expr.arguments[0];
46547
- return isObject(arg) ? arg : null;
46548
- }
46549
-
46550
- function isStringLiteral(node) {
46551
- return node?.type === dist$3.AST_NODE_TYPES.Literal && typeof node.value === 'string';
46552
- }
46553
- function isStaticTemplateLiteral(node) {
46554
- return (node?.type === dist$3.AST_NODE_TYPES.TemplateLiteral &&
46555
- node.expressions.length === 0 &&
46556
- node.quasis.length === 1);
46557
- }
46558
- function getStaticStringValue(node) {
46559
- if (isStringLiteral(node)) {
46560
- return node.value;
46561
- }
46562
- return isStaticTemplateLiteral(node)
46563
- ? (node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw ?? '')
46564
- : null;
46565
- }
46566
- function isEmptyStaticString(node) {
46567
- return getStaticStringValue(node) === '';
46568
- }
46569
-
46570
- function getStaticPropertyName(key) {
46571
- if (key.type === dist$3.AST_NODE_TYPES.Identifier) {
46572
- return key.name;
46573
- }
46574
- return key.type === dist$3.AST_NODE_TYPES.Literal &&
46575
- (typeof key.value === 'string' || typeof key.value === 'number')
46576
- ? String(key.value)
46577
- : getStaticStringValue(key);
46578
- }
46579
- function getObjectPropertyName(node) {
46580
- return node.computed ? null : getStaticPropertyName(node.key);
46581
- }
46582
- function getMemberExpressionPropertyName(node) {
46583
- if (!node.computed && node.property.type === dist$3.AST_NODE_TYPES.Identifier) {
46584
- return node.property.name;
46585
- }
46586
- return node.computed ? getStaticStringValue(node.property) : null;
46587
- }
46588
- function getClassMemberName(member) {
46589
- return member.key.type === dist$3.AST_NODE_TYPES.PrivateIdentifier
46590
- ? null
46591
- : getStaticPropertyName(member.key);
46592
- }
46593
-
46594
46616
  const DEFAULT_GROUP = '$DEFAULT';
46595
46617
  const DEFAULT_ATTRIBUTE_GROUPS = [
46596
46618
  '$ANGULAR_STRUCTURAL_DIRECTIVE',
@@ -46656,7 +46678,7 @@ const PRESETS = {
46656
46678
  $VUE: ['$CLASS', '$ID', '$VUE_ATTRIBUTE'],
46657
46679
  $VUE_ATTRIBUTE: /^v-/,
46658
46680
  };
46659
- const rule$O = createRule({
46681
+ const rule$P = createRule({
46660
46682
  create(context, [options]) {
46661
46683
  const sourceCode = context.sourceCode;
46662
46684
  const settings = {
@@ -46987,7 +47009,7 @@ const config$4 = {
46987
47009
  type: 'suggestion',
46988
47010
  },
46989
47011
  };
46990
- const rule$N = createRule({
47012
+ const rule$O = createRule({
46991
47013
  name: 'html-logical-properties',
46992
47014
  rule: config$4,
46993
47015
  });
@@ -248226,7 +248248,7 @@ function isImportUsedOnlyAsAngularDiFirstArg(node, sourceCode) {
248226
248248
  }
248227
248249
  return hasSafeRuntimeUsage;
248228
248250
  }
248229
- const rule$M = createRule({
248251
+ const rule$N = createRule({
248230
248252
  create(context) {
248231
248253
  const { checker, esTreeNodeToTSNodeMap, sourceCode, tsProgram } = getTypeAwareRuleContext(context);
248232
248254
  const checkCycles = context.options[0]?.checkCycles ?? true;
@@ -248944,7 +248966,7 @@ function getNgDevModeDeclarationFix(program, fixer) {
248944
248966
  ? fixer.insertTextBefore(firstStatement, 'declare const ngDevMode: boolean;\n\n')
248945
248967
  : fixer.insertTextBeforeRange([0, 0], 'declare const ngDevMode: boolean;\n');
248946
248968
  }
248947
- const rule$L = createRule({
248969
+ const rule$M = createRule({
248948
248970
  create(context) {
248949
248971
  const { sourceCode } = context;
248950
248972
  const program = sourceCode.ast;
@@ -248993,7 +249015,7 @@ const rule$L = createRule({
248993
249015
  name: 'injection-token-description',
248994
249016
  });
248995
249017
 
248996
- const rule$K = createRule({
249018
+ const rule$L = createRule({
248997
249019
  create(context) {
248998
249020
  const { sourceCode } = context;
248999
249021
  const namespaceImports = new Map();
@@ -249088,7 +249110,7 @@ const DEFAULT_OPTIONS = {
249088
249110
  importDeclaration: '^@taiga-ui*',
249089
249111
  projectName: String.raw `(?<=^@taiga-ui/)([-\w]+)`,
249090
249112
  };
249091
- const rule$J = createRule({
249113
+ const rule$K = createRule({
249092
249114
  create(context) {
249093
249115
  const { currentProject, deepImport, ignoreImports, importDeclaration, projectName, } = { ...DEFAULT_OPTIONS, ...context.options[0] };
249094
249116
  const hasNonCodeExtension = (source) => {
@@ -249180,7 +249202,7 @@ const nearestFileUpCache = new Map();
249180
249202
  const markerCache = new Map();
249181
249203
  const indexFileCache = new Map();
249182
249204
  const indexExportsCache = new Map();
249183
- const rule$I = createRule({
249205
+ const rule$J = createRule({
249184
249206
  create(context) {
249185
249207
  const parserServices = dist$3.ESLintUtils.getParserServices(context);
249186
249208
  const program = parserServices.program;
@@ -249374,13 +249396,13 @@ const noDuplicateAttributesRule = angular.templatePlugin.rules?.['no-duplicate-a
249374
249396
  if (!noDuplicateAttributesRule) {
249375
249397
  throw new Error('angular-eslint template rule "no-duplicate-attributes" is not available');
249376
249398
  }
249377
- const rule$H = createRule({
249399
+ const rule$I = createRule({
249378
249400
  name: 'no-duplicate-attrs',
249379
249401
  rule: noDuplicateAttributesRule,
249380
249402
  });
249381
249403
 
249382
249404
  const MESSAGE_ID$c = 'duplicateId';
249383
- const rule$G = createRule({
249405
+ const rule$H = createRule({
249384
249406
  name: 'no-duplicate-id',
249385
249407
  rule: {
249386
249408
  create(context) {
@@ -249441,7 +249463,7 @@ function getTrackingKey(node) {
249441
249463
  ? 'link[rel=canonical]'
249442
249464
  : null;
249443
249465
  }
249444
- const rule$F = createRule({
249466
+ const rule$G = createRule({
249445
249467
  name: 'no-duplicate-in-head',
249446
249468
  rule: {
249447
249469
  create(context) {
@@ -249496,6 +249518,133 @@ const rule$F = createRule({
249496
249518
  },
249497
249519
  });
249498
249520
 
249521
+ const COMPONENT_DECORATORS = new Set(['Component']);
249522
+ const rule$F = createRule({
249523
+ create(context) {
249524
+ const { sourceCode } = context;
249525
+ return {
249526
+ ClassDeclaration(node) {
249527
+ for (const decorator of node?.decorators ?? []) {
249528
+ const metadata = getDecoratorMetadata(decorator, COMPONENT_DECORATORS);
249529
+ if (!metadata) {
249530
+ continue;
249531
+ }
249532
+ const emptyProperties = metadata.properties.filter((property) => property.type === dist$3.AST_NODE_TYPES.Property &&
249533
+ isEmptyStyleMetadata(getObjectPropertyName(property), property, sourceCode));
249534
+ if (emptyProperties.length === 0) {
249535
+ continue;
249536
+ }
249537
+ const [firstEmptyProperty] = emptyProperties;
249538
+ if (!firstEmptyProperty) {
249539
+ continue;
249540
+ }
249541
+ context.report({
249542
+ fix: (fixer) => {
249543
+ const ranges = emptyProperties.map((property) => getPropertyRemovalRange(sourceCode, property));
249544
+ return ranges.some((range) => hasCommentsInRange(sourceCode, range))
249545
+ ? null
249546
+ : fixer.replaceTextRange(metadata.range, removeRanges(sourceCode.text.slice(...metadata.range), ranges.map(([start, end]) => [
249547
+ start - metadata.range[0],
249548
+ end - metadata.range[0],
249549
+ ])));
249550
+ },
249551
+ messageId: 'noEmptyStyleMetadata',
249552
+ node: firstEmptyProperty,
249553
+ });
249554
+ }
249555
+ },
249556
+ };
249557
+ },
249558
+ meta: {
249559
+ docs: {
249560
+ description: 'Disallow empty `styles`, `styleUrl`, and `styleUrls` metadata in Angular components.',
249561
+ },
249562
+ fixable: 'code',
249563
+ messages: {
249564
+ noEmptyStyleMetadata: 'Empty style metadata should be removed from @Component decorator.',
249565
+ },
249566
+ schema: [],
249567
+ type: 'problem',
249568
+ },
249569
+ name: 'no-empty-style-metadata',
249570
+ });
249571
+ function isEmptyStyleMetadata(name, property, sourceCode) {
249572
+ return (((name === 'styles' || name === 'styleUrl') &&
249573
+ isEmptyStringExpression(property.value)) ||
249574
+ (name === 'styleUrls' &&
249575
+ property.value.type === dist$3.AST_NODE_TYPES.ArrayExpression &&
249576
+ property.value.elements.length === 0 &&
249577
+ sourceCode.getText(property.value).replaceAll(/\s/g, '') === '[]'));
249578
+ }
249579
+ function isEmptyStringExpression(node) {
249580
+ if (node.type === dist$3.AST_NODE_TYPES.Literal) {
249581
+ return node.value === '';
249582
+ }
249583
+ if (node.type !== dist$3.AST_NODE_TYPES.TemplateLiteral || node.expressions.length > 0) {
249584
+ return false;
249585
+ }
249586
+ const [quasi] = node.quasis;
249587
+ return quasi?.value.raw === '';
249588
+ }
249589
+ function getPropertyRemovalRange(sourceCode, property) {
249590
+ const nextToken = sourceCode.getTokenAfter(property);
249591
+ if (nextToken?.value === ',') {
249592
+ return getRangeWithFollowingComma(sourceCode.text, property, nextToken.range);
249593
+ }
249594
+ const previousToken = sourceCode.getTokenBefore(property);
249595
+ return previousToken?.value === ','
249596
+ ? [previousToken.range[0], property.range[1]]
249597
+ : getSinglePropertyRange(sourceCode.text, property);
249598
+ }
249599
+ function getRangeWithFollowingComma(text, property, commaRange) {
249600
+ const lineStart = getLineStart(text, property.range[0]);
249601
+ const nextLineStart = getNextLineStart(text, commaRange[1]);
249602
+ return text.slice(lineStart, property.range[0]).trim() === '' &&
249603
+ text.slice(commaRange[1], nextLineStart).trim() === '' &&
249604
+ nextLineStart > commaRange[1]
249605
+ ? [lineStart, nextLineStart]
249606
+ : [property.range[0], commaRange[1]];
249607
+ }
249608
+ function getSinglePropertyRange(text, property) {
249609
+ const lineStart = getLineStart(text, property.range[0]);
249610
+ const nextLineStart = getNextLineStart(text, property.range[1]);
249611
+ return text.slice(lineStart, property.range[0]).trim() === '' &&
249612
+ text.slice(property.range[1], nextLineStart).trim() === '' &&
249613
+ nextLineStart > property.range[1]
249614
+ ? [lineStart, nextLineStart]
249615
+ : [property.range[0], property.range[1]];
249616
+ }
249617
+ function getLineStart(text, index) {
249618
+ return text.lastIndexOf('\n', index - 1) + 1;
249619
+ }
249620
+ function getNextLineStart(text, index) {
249621
+ const lineEnd = text.indexOf('\n', index);
249622
+ return lineEnd === -1 ? index : lineEnd + 1;
249623
+ }
249624
+ function hasCommentsInRange(sourceCode, [start, end]) {
249625
+ return sourceCode
249626
+ .getAllComments()
249627
+ .some((comment) => comment.range[0] >= start && comment.range[1] <= end);
249628
+ }
249629
+ function removeRanges(text, ranges) {
249630
+ return mergeRanges(ranges)
249631
+ .sort((left, right) => right[0] - left[0])
249632
+ .reduce((result, [start, end]) => `${result.slice(0, start)}${result.slice(end)}`, text);
249633
+ }
249634
+ function mergeRanges(ranges) {
249635
+ const sorted = [...ranges].sort((left, right) => left[0] - right[0]);
249636
+ const merged = [];
249637
+ for (const range of sorted) {
249638
+ const last = merged[merged.length - 1];
249639
+ if (!last || range[0] > last[1]) {
249640
+ merged.push([...range]);
249641
+ continue;
249642
+ }
249643
+ last[1] = Math.max(last[1], range[1]);
249644
+ }
249645
+ return merged;
249646
+ }
249647
+
249499
249648
  function isFunctionLike(node) {
249500
249649
  return (node.type === dist$3.AST_NODE_TYPES.ArrowFunctionExpression ||
249501
249650
  node.type === dist$3.AST_NODE_TYPES.FunctionDeclaration ||
@@ -253819,10 +253968,17 @@ function isArray(node) {
253819
253968
  }
253820
253969
 
253821
253970
  function isImportsArrayProperty(property) {
253822
- const isProperty = property?.type === dist$3.AST_NODE_TYPES.Property;
253823
- const hasIdentifierKey = property?.key.type === dist$3.AST_NODE_TYPES.Identifier &&
253971
+ if (property?.type !== dist$3.AST_NODE_TYPES.Property) {
253972
+ return false;
253973
+ }
253974
+ const hasIdentifierKey = property.key.type === dist$3.AST_NODE_TYPES.Identifier &&
253824
253975
  property.key.name === 'imports';
253825
- return isProperty && hasIdentifierKey && isArray(property.value);
253976
+ return hasIdentifierKey && isArray(property.value);
253977
+ }
253978
+
253979
+ function getImportsArray(meta) {
253980
+ const property = meta.properties.find(isImportsArrayProperty);
253981
+ return property?.value ?? null;
253826
253982
  }
253827
253983
 
253828
253984
  function getImportedName$1(spec) {
@@ -253843,28 +253999,19 @@ const DEFAULT_EXCEPTIONS = [
253843
253999
  const rule$9 = createRule({
253844
254000
  create(context, [{ decorators = DEFAULT_DECORATORS, exceptions = DEFAULT_EXCEPTIONS }]) {
253845
254001
  const sourceCode = context.getSourceCode();
254002
+ const allowedDecorators = new Set(decorators);
253846
254003
  const importedFromTaiga = {};
253847
- const decoratorSelector = `Decorator[expression.callee.name=/^(${decorators.join('|')})$/]`;
253848
254004
  return {
253849
- [decoratorSelector](decorator) {
253850
- const expression = decorator.expression;
253851
- const isInvalidExpression = expression.type !== dist$3.AST_NODE_TYPES.CallExpression ||
253852
- expression.arguments.length === 0;
253853
- if (isInvalidExpression) {
253854
- return;
253855
- }
253856
- const [arg] = expression.arguments;
253857
- const isNotObject = arg?.type !== dist$3.AST_NODE_TYPES.ObjectExpression;
253858
- if (isNotObject) {
254005
+ Decorator(decorator) {
254006
+ const metadata = getDecoratorMetadata(decorator, allowedDecorators);
254007
+ if (!metadata) {
253859
254008
  return;
253860
254009
  }
253861
- const importsProperty = arg.properties
253862
- .filter((literal) => literal.type === dist$3.AST_NODE_TYPES.Property)
253863
- .find((literal) => isImportsArrayProperty(literal));
253864
- if (!importsProperty) {
254010
+ const importsArray = getImportsArray(metadata);
254011
+ if (!importsArray) {
253865
254012
  return;
253866
254013
  }
253867
- const imports = importsProperty.value.elements.filter((el) => {
254014
+ const imports = importsArray.elements.filter((el) => {
253868
254015
  const isIdentifier = el?.type === dist$3.AST_NODE_TYPES.Identifier;
253869
254016
  return Boolean(el && isIdentifier);
253870
254017
  });
@@ -254275,14 +254422,6 @@ const rule$7 = createRule({
254275
254422
  name: 'single-line-variable-spacing',
254276
254423
  });
254277
254424
 
254278
- function getImportsArray(meta) {
254279
- const property = meta.properties.find((literal) => literal.type === dist$2.AST_NODE_TYPES.Property &&
254280
- literal.key.type === dist$2.AST_NODE_TYPES.Identifier &&
254281
- literal.key.name === 'imports' &&
254282
- isArray(literal.value));
254283
- return property ? property.value : null;
254284
- }
254285
-
254286
254425
  function isSpread(node) {
254287
254426
  return node.type === dist$2.AST_NODE_TYPES.SpreadElement;
254288
254427
  }
@@ -254322,11 +254461,12 @@ function nameOf(node, source) {
254322
254461
  return source.getText(node);
254323
254462
  }
254324
254463
 
254464
+ const IMPORT_NAME_COLLATOR = new Intl.Collator('en', { numeric: true });
254325
254465
  /**
254326
- * Sorts Angular standalone import elements into a deterministic, alphabetical order.
254466
+ * Sorts Angular standalone import elements into a deterministic, natural order.
254327
254467
  *
254328
254468
  * The sorting rules:
254329
- * 1. Regular elements (Identifiers, MemberExpressions, etc.) are sorted alphabetically.
254469
+ * 1. Regular elements (Identifiers, MemberExpressions, etc.) are sorted naturally.
254330
254470
  * 2. Spread elements (e.g. `...A`) are sorted separately and placed after regular ones.
254331
254471
  * 3. Sorting is based on the string value returned by `nameOf()`.
254332
254472
  *
@@ -254345,10 +254485,13 @@ function nameOf(node, source) {
254345
254485
  function getSortedNames(elements, source) {
254346
254486
  const regular = elements.filter((e) => !isSpread(e));
254347
254487
  const spreads = elements.filter((e) => isSpread(e));
254348
- const sortedRegular = [...regular].sort((a, b) => nameOf(a, source).localeCompare(nameOf(b, source)));
254349
- const sortedSpreads = [...spreads].sort((a, b) => nameOf(a.argument, source).localeCompare(nameOf(b.argument, source)));
254488
+ const sortedRegular = [...regular].sort((a, b) => compareImportNames(nameOf(a, source), nameOf(b, source)));
254489
+ const sortedSpreads = [...spreads].sort((a, b) => compareImportNames(nameOf(a.argument, source), nameOf(b.argument, source)));
254350
254490
  return [...sortedRegular, ...sortedSpreads].map((n) => nameOf(n, source));
254351
254491
  }
254492
+ function compareImportNames(x, y) {
254493
+ return IMPORT_NAME_COLLATOR.compare(x, y);
254494
+ }
254352
254495
 
254353
254496
  const rule$6 = createRule({
254354
254497
  create(context, [options]) {
@@ -255277,21 +255420,22 @@ const plugin = {
255277
255420
  },
255278
255421
  rules: {
255279
255422
  'array-as-const': rule$5,
255280
- 'attrs-newline': rule$R,
255423
+ 'attrs-newline': rule$S,
255281
255424
  'class-property-naming': rule$4,
255282
- 'decorator-key-sort': rule$Q,
255283
- 'element-newline': rule$P,
255425
+ 'decorator-key-sort': rule$R,
255426
+ 'element-newline': rule$Q,
255284
255427
  'flat-exports': rule$3,
255285
- 'host-attributes-sort': rule$O,
255286
- 'html-logical-properties': rule$N,
255287
- 'import-integrity': rule$M,
255288
- 'injection-token-description': rule$L,
255289
- 'no-commonjs-import-patterns': rule$K,
255290
- 'no-deep-imports': rule$J,
255291
- 'no-deep-imports-to-indexed-packages': rule$I,
255292
- 'no-duplicate-attrs': rule$H,
255293
- 'no-duplicate-id': rule$G,
255294
- 'no-duplicate-in-head': rule$F,
255428
+ 'host-attributes-sort': rule$P,
255429
+ 'html-logical-properties': rule$O,
255430
+ 'import-integrity': rule$N,
255431
+ 'injection-token-description': rule$M,
255432
+ 'no-commonjs-import-patterns': rule$L,
255433
+ 'no-deep-imports': rule$K,
255434
+ 'no-deep-imports-to-indexed-packages': rule$J,
255435
+ 'no-duplicate-attrs': rule$I,
255436
+ 'no-duplicate-id': rule$H,
255437
+ 'no-duplicate-in-head': rule$G,
255438
+ 'no-empty-style-metadata': rule$F,
255295
255439
  'no-fully-untracked-effect': rule$E,
255296
255440
  'no-href-with-router-link': rule$D,
255297
255441
  'no-implicit-public': rule$C,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ui/eslint-plugin-experience-next",
3
- "version": "0.507.0",
3
+ "version": "0.508.0",
4
4
  "description": "An ESLint plugin to enforce a consistent code styles across taiga-ui projects",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,5 +1,5 @@
1
- import { type Rule } from 'eslint';
2
- export declare const rule: Rule.RuleModule & {
1
+ type Options = [Record<string, readonly string[]>];
2
+ export declare const rule: import("@typescript-eslint/utils/ts-eslint").RuleModule<"incorrectOrder", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
3
3
  name: string;
4
4
  };
5
5
  export default rule;
@@ -0,0 +1,5 @@
1
+ import { type TSESLint } from '@typescript-eslint/utils';
2
+ export declare const rule: TSESLint.RuleModule<"noEmptyStyleMetadata", [], unknown, TSESLint.RuleListener> & {
3
+ name: string;
4
+ };
5
+ export default rule;
@@ -1,4 +1,9 @@
1
1
  import { type TSESTree } from '@typescript-eslint/utils';
2
+ export interface DecoratorMetadataWithName {
3
+ readonly expression: TSESTree.CallExpression;
4
+ readonly metadata: TSESTree.ObjectExpression;
5
+ readonly name: string;
6
+ }
2
7
  /**
3
8
  * Extracts the metadata object from a class decorator such as
4
9
  * `@Component()`, `@Directive()`, `@NgModule()`, or `@Pipe()`.
@@ -15,7 +20,7 @@ import { type TSESTree } from '@typescript-eslint/utils';
15
20
  * class MyCmp {}
16
21
  *
17
22
  * // In the AST for @Component(...)
18
- * getDecoratorMetadata(decorator, allowed)
23
+ * getDecoratorMetadata(decorator, allowed) returns
19
24
  * ObjectExpression({ selector: ..., imports: ... })
20
25
  *
21
26
  * @param decorator - The decorator node attached to a class declaration.
@@ -25,4 +30,5 @@ import { type TSESTree } from '@typescript-eslint/utils';
25
30
  * @returns The metadata `ObjectExpression` if present and valid,
26
31
  * otherwise `null`.
27
32
  */
28
- export declare function getDecoratorMetadata(decorator: TSESTree.Decorator, allowedNames: Set<string>): TSESTree.ObjectExpression | null;
33
+ export declare function getDecoratorMetadata(decorator: TSESTree.Decorator, allowedNames: ReadonlySet<string>): TSESTree.ObjectExpression | null;
34
+ export declare function getDecoratorMetadataWithName(decorator: TSESTree.Decorator, allowedNames: ReadonlySet<string>): DecoratorMetadataWithName | null;
@@ -1,4 +1,4 @@
1
1
  import { type TSESTree } from '@typescript-eslint/utils';
2
- export declare function isImportsArrayProperty(property?: TSESTree.Property): property is TSESTree.Property & {
2
+ export declare function isImportsArrayProperty(property?: TSESTree.ObjectExpression['properties'][number]): property is TSESTree.Property & {
3
3
  value: TSESTree.ArrayExpression;
4
4
  };
@@ -1,9 +1,9 @@
1
1
  import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
2
2
  /**
3
- * Sorts Angular standalone import elements into a deterministic, alphabetical order.
3
+ * Sorts Angular standalone import elements into a deterministic, natural order.
4
4
  *
5
5
  * The sorting rules:
6
- * 1. Regular elements (Identifiers, MemberExpressions, etc.) are sorted alphabetically.
6
+ * 1. Regular elements (Identifiers, MemberExpressions, etc.) are sorted naturally.
7
7
  * 2. Spread elements (e.g. `...A`) are sorted separately and placed after regular ones.
8
8
  * 3. Sorting is based on the string value returned by `nameOf()`.
9
9
  *