@taiga-ui/eslint-plugin-experience-next 0.459.0 → 0.460.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
@@ -49,6 +49,7 @@ export default [
49
49
  | no-href-with-router-link | Do not use href and routerLink attributes together on the same element | | 🔧 | |
50
50
  | no-implicit-public | Require explicit `public` modifier for class members and parameter properties | ✅ | 🔧 | |
51
51
  | no-playwright-empty-fill | Enforce `clear()` over `fill('')` in Playwright tests | ✅ | 🔧 | |
52
+ | no-project-as-in-ng-template | `ngProjectAs` has no effect inside `<ng-template>` or dynamic outlets | | | |
52
53
  | no-redundant-type-annotation | Disallow redundant type annotations when the type is already inferred from the initializer | ✅ | 🔧 | |
53
54
  | no-string-literal-concat | Disallow string literal concatenation; merge adjacent literals into one | ✅ | 🔧 | |
54
55
  | object-single-line | Enforce single-line formatting for single-property objects when it fits `printWidth` | ✅ | 🔧 | |
@@ -285,6 +286,36 @@ await page.getByLabel('Name').clear();
285
286
 
286
287
  ---
287
288
 
289
+ ## no-project-as-in-ng-template
290
+
291
+ `ngProjectAs` has no effect when the element is inside an `<ng-template>`, `*ngTemplateOutlet`, `*ngComponentOutlet`, or
292
+ `*polymorpheusOutlet`. Content instantiated through these dynamic outlets does not participate in Angular's static
293
+ content projection, so the attribute is silently ignored at runtime.
294
+
295
+ ```html
296
+ <!-- ❌ error — inside <ng-template> -->
297
+ <ng-template #tpl>
298
+ <div ngProjectAs="[someSlot]">content</div>
299
+ </ng-template>
300
+
301
+ <!-- ❌ error — on the outlet host itself -->
302
+ <ng-container
303
+ *ngTemplateOutlet="tpl"
304
+ ngProjectAs="[someSlot]"
305
+ ></ng-container>
306
+
307
+ <!-- ❌ error — polymorpheusOutlet -->
308
+ <ng-container
309
+ *polymorpheusOutlet="content"
310
+ ngProjectAs="someSlot"
311
+ ></ng-container>
312
+
313
+ <!-- ✅ ok — static content projection -->
314
+ <div ngProjectAs="[someSlot]">content</div>
315
+ ```
316
+
317
+ ---
318
+
288
319
  ## no-string-literal-concat
289
320
 
290
321
  Disallows concatenating string literals with `+`. Adjacent string literals are always mergeable into one — splitting
@@ -536,6 +567,32 @@ x: Animal = new Dog();
536
567
  x: MyService | null = inject(MyService);
537
568
  ```
538
569
 
570
+ The rule does **not** report when the annotation provides contextual typing that narrows an array literal to a tuple.
571
+ Without the annotation TypeScript would infer `number[]` instead of the required tuple type, widening the type and
572
+ breaking compilation:
573
+
574
+ ```ts
575
+ type SelectionRange = readonly [from: number, to: number];
576
+ interface ElementState {
577
+ readonly value: string;
578
+ readonly selection: SelectionRange;
579
+ }
580
+
581
+ // ✅ ok — [0, 0] is inferred as SelectionRange only because of the annotation;
582
+ // removing it would widen the type to ElementState | {value: string; selection: number[]}
583
+ const state: ElementState = flag ? {value: '', selection: [0, 0]} : existingState;
584
+ ```
585
+
586
+ ```json
587
+ {
588
+ "@taiga-ui/experience-next/no-redundant-type-annotation": ["error", {"ignoreTupleContextualTyping": true}]
589
+ }
590
+ ```
591
+
592
+ | Option | Type | Default | Description |
593
+ | ----------------------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------ |
594
+ | `ignoreTupleContextualTyping` | `boolean` | `true` | Preserve annotations when they provide contextual typing that narrows an array literal to a tuple type |
595
+
539
596
  ---
540
597
 
541
598
  ## strict-tui-doc-example
package/index.d.ts CHANGED
@@ -40,7 +40,10 @@ declare const plugin: {
40
40
  'no-playwright-empty-fill': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useClear", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
41
41
  name: string;
42
42
  };
43
- 'no-redundant-type-annotation': import("@typescript-eslint/utils/ts-eslint").RuleModule<"redundantTypeAnnotation", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
43
+ 'no-project-as-in-ng-template': import("eslint").Rule.RuleModule;
44
+ 'no-redundant-type-annotation': import("@typescript-eslint/utils/ts-eslint").RuleModule<"redundantTypeAnnotation", [({
45
+ ignoreTupleContextualTyping?: boolean;
46
+ } | undefined)?], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
44
47
  name: string;
45
48
  };
46
49
  'no-string-literal-concat': import("@typescript-eslint/utils/ts-eslint").RuleModule<"flattenTemplate" | "mergeLiterals" | "useTemplate", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
package/index.esm.js CHANGED
@@ -32,6 +32,7 @@ import { ESLintUtils, AST_NODE_TYPES } from '@typescript-eslint/utils';
32
32
  import ts, { isCallExpression } from 'typescript';
33
33
  import { AST_NODE_TYPES as AST_NODE_TYPES$1 } from '@typescript-eslint/types';
34
34
  import path from 'node:path';
35
+ import { TmplAstTemplate } from '@angular-eslint/bundled-angular-compiler';
35
36
 
36
37
  var htmlEslint = defineConfig([
37
38
  {
@@ -989,6 +990,7 @@ var recommended = defineConfig([
989
990
  '@angular-eslint/template/prefer-control-flow': angularVersion >= modernAngularRules.preferControlFlow ? 'error' : 'off',
990
991
  '@angular-eslint/template/prefer-self-closing-tags': 'error',
991
992
  '@angular-eslint/template/prefer-template-literal': angularVersion >= modernAngularRules.templateLiteral ? 'error' : 'off',
993
+ '@taiga-ui/experience-next/no-project-as-in-ng-template': 'error',
992
994
  },
993
995
  },
994
996
  {
@@ -1238,7 +1240,7 @@ function readJSON(path) {
1238
1240
  }
1239
1241
  }
1240
1242
 
1241
- const config$2 = {
1243
+ const config$3 = {
1242
1244
  create(context) {
1243
1245
  const classesInFile = new Map();
1244
1246
  return {
@@ -1397,7 +1399,7 @@ var classPropertyNaming = createRule$e({
1397
1399
  name: 'class-property-naming',
1398
1400
  });
1399
1401
 
1400
- const config$1 = {
1402
+ const config$2 = {
1401
1403
  create(context) {
1402
1404
  const order = context.options[0] || {};
1403
1405
  return {
@@ -1500,7 +1502,7 @@ function isExternalPureTuple(typeChecker, type) {
1500
1502
  }
1501
1503
 
1502
1504
  const createRule$d = ESLintUtils.RuleCreator((name) => name);
1503
- const MESSAGE_ID$5 = 'spreadArrays';
1505
+ const MESSAGE_ID$6 = 'spreadArrays';
1504
1506
  var flatExports = createRule$d({
1505
1507
  create(context) {
1506
1508
  const parserServices = ESLintUtils.getParserServices(context);
@@ -1566,7 +1568,7 @@ var flatExports = createRule$d({
1566
1568
  fix(fixer) {
1567
1569
  return fixer.replaceText(elementNode, `...${meta.name}`);
1568
1570
  },
1569
- messageId: MESSAGE_ID$5,
1571
+ messageId: MESSAGE_ID$6,
1570
1572
  node: elementNode,
1571
1573
  });
1572
1574
  }
@@ -1617,7 +1619,7 @@ var flatExports = createRule$d({
1617
1619
  },
1618
1620
  fixable: 'code',
1619
1621
  messages: {
1620
- [MESSAGE_ID$5]: 'Spread "{{ name }}" to avoid nested arrays in exported entities.',
1622
+ [MESSAGE_ID$6]: 'Spread "{{ name }}" to avoid nested arrays in exported entities.',
1621
1623
  },
1622
1624
  schema: [],
1623
1625
  type: 'suggestion',
@@ -1625,7 +1627,7 @@ var flatExports = createRule$d({
1625
1627
  name: 'flat-exports',
1626
1628
  });
1627
1629
 
1628
- const MESSAGE_ID$4 = 'invalid-injection-token-description';
1630
+ const MESSAGE_ID$5 = 'invalid-injection-token-description';
1629
1631
  const ERROR_MESSAGE$3 = "InjectionToken's description should contain token's name";
1630
1632
  const createRule$c = ESLintUtils.RuleCreator((name) => name);
1631
1633
  const rule$9 = createRule$c({
@@ -1659,7 +1661,7 @@ const rule$9 = createRule$c({
1659
1661
  const [start, end] = description.range;
1660
1662
  return fixer.insertTextBeforeRange([start + 1, end], `[${name}]: `);
1661
1663
  },
1662
- messageId: MESSAGE_ID$4,
1664
+ messageId: MESSAGE_ID$5,
1663
1665
  node: description,
1664
1666
  });
1665
1667
  }
@@ -1669,14 +1671,14 @@ const rule$9 = createRule$c({
1669
1671
  meta: {
1670
1672
  docs: { description: ERROR_MESSAGE$3 },
1671
1673
  fixable: 'code',
1672
- messages: { [MESSAGE_ID$4]: ERROR_MESSAGE$3 },
1674
+ messages: { [MESSAGE_ID$5]: ERROR_MESSAGE$3 },
1673
1675
  schema: [],
1674
1676
  type: 'problem',
1675
1677
  },
1676
1678
  name: 'injection-token-description',
1677
1679
  });
1678
1680
 
1679
- const MESSAGE_ID$3 = 'no-deep-imports';
1681
+ const MESSAGE_ID$4 = 'no-deep-imports';
1680
1682
  const ERROR_MESSAGE$2 = 'Deep imports of Taiga UI packages are prohibited';
1681
1683
  const CODE_EXTENSIONS = new Set([
1682
1684
  '.cjs',
@@ -1738,7 +1740,7 @@ const rule$8 = createRule$b({
1738
1740
  const [start, end] = node.source.range;
1739
1741
  return fixer.replaceTextRange([start + 1, end - 1], importSource.replaceAll(new RegExp(deepImport, 'g'), ''));
1740
1742
  },
1741
- messageId: MESSAGE_ID$3,
1743
+ messageId: MESSAGE_ID$4,
1742
1744
  node: node.source,
1743
1745
  });
1744
1746
  },
@@ -1748,7 +1750,7 @@ const rule$8 = createRule$b({
1748
1750
  defaultOptions: [DEFAULT_OPTIONS],
1749
1751
  docs: { description: ERROR_MESSAGE$2 },
1750
1752
  fixable: 'code',
1751
- messages: { [MESSAGE_ID$3]: ERROR_MESSAGE$2 },
1753
+ messages: { [MESSAGE_ID$4]: ERROR_MESSAGE$2 },
1752
1754
  schema: [
1753
1755
  {
1754
1756
  additionalProperties: false,
@@ -1984,9 +1986,9 @@ function stripKnownExtensions(filePathOrSpecifier) {
1984
1986
  return filePathOrSpecifier.replace(/\.(?:d\.ts|ts|tsx|js|jsx|mjs|cjs)$/, '');
1985
1987
  }
1986
1988
 
1987
- const MESSAGE_ID$2 = 'no-href-with-router-link';
1989
+ const MESSAGE_ID$3 = 'no-href-with-router-link';
1988
1990
  const ERROR_MESSAGE$1 = 'Do not use href and routerLink attributes together on the same element';
1989
- const config = {
1991
+ const config$1 = {
1990
1992
  create(context) {
1991
1993
  return {
1992
1994
  Tag(node) {
@@ -2014,7 +2016,7 @@ const config = {
2014
2016
  fix: (fixer) => hrefAttribute?.range
2015
2017
  ? fixer.removeRange(hrefAttribute.range)
2016
2018
  : null,
2017
- messageId: MESSAGE_ID$2,
2019
+ messageId: MESSAGE_ID$3,
2018
2020
  node: (routerLinkAttribute ??
2019
2021
  hrefAttribute ??
2020
2022
  htmlNode),
@@ -2026,7 +2028,7 @@ const config = {
2026
2028
  meta: {
2027
2029
  docs: { description: ERROR_MESSAGE$1 },
2028
2030
  fixable: 'code',
2029
- messages: { [MESSAGE_ID$2]: ERROR_MESSAGE$1 },
2031
+ messages: { [MESSAGE_ID$3]: ERROR_MESSAGE$1 },
2030
2032
  type: 'problem',
2031
2033
  },
2032
2034
  };
@@ -2191,11 +2193,115 @@ function isPlaywrightLocatorType(type) {
2191
2193
  });
2192
2194
  }
2193
2195
 
2196
+ const MESSAGE_ID$2 = 'no-project-as-in-ng-template';
2197
+ const NESTED_TEMPLATE_MESSAGE_ID = 'no-nested-template-in-dynamic-outlet';
2198
+ const PROJECT_AS_ERROR_MESSAGE = 'ngProjectAs on a dynamic outlet breaks SSR hydration. Use static content projection instead.';
2199
+ const NESTED_TEMPLATE_ERROR_MESSAGE = 'Avoid nesting ng-template inside dynamic outlet containers. Move the template outside the ng-container.';
2200
+ const DYNAMIC_OUTLET_DIRECTIVES = new Set([
2201
+ 'ngComponentOutlet',
2202
+ 'ngTemplateOutlet',
2203
+ 'polymorpheusOutlet',
2204
+ ]);
2205
+ function toLoc(span) {
2206
+ return {
2207
+ end: {
2208
+ column: span.end.col,
2209
+ line: span.end.line + 1,
2210
+ },
2211
+ start: {
2212
+ column: span.start.col,
2213
+ line: span.start.line + 1,
2214
+ },
2215
+ };
2216
+ }
2217
+ function isInsideDynamicOutlet(node) {
2218
+ let parent = node.parent;
2219
+ while (parent) {
2220
+ const hasDynamicOutletDirective = parent instanceof TmplAstTemplate &&
2221
+ parent.templateAttrs.some((attr) => DYNAMIC_OUTLET_DIRECTIVES.has(attr.name));
2222
+ if (hasDynamicOutletDirective) {
2223
+ return true;
2224
+ }
2225
+ parent = parent.parent;
2226
+ }
2227
+ return false;
2228
+ }
2229
+ function checkProjectAsOnDynamicOutlet(context, node) {
2230
+ const ngProjectAsAttr = node.attributes.find((attr) => attr.name === 'ngProjectAs') ??
2231
+ node.inputs.find((input) => input.name === 'ngProjectAs');
2232
+ if (ngProjectAsAttr && isInsideDynamicOutlet(node)) {
2233
+ context.report({
2234
+ loc: toLoc(ngProjectAsAttr.sourceSpan),
2235
+ messageId: MESSAGE_ID$2,
2236
+ });
2237
+ }
2238
+ }
2239
+ function checkNestedTemplateInDynamicOutlet(context, node) {
2240
+ const hasDynamicOutletInput = node.inputs.some((input) => DYNAMIC_OUTLET_DIRECTIVES.has(input.name));
2241
+ if (!hasDynamicOutletInput) {
2242
+ return;
2243
+ }
2244
+ for (const child of node.children) {
2245
+ if (child instanceof TmplAstTemplate && child.tagName === 'ng-template') {
2246
+ context.report({
2247
+ loc: toLoc(child.sourceSpan),
2248
+ messageId: NESTED_TEMPLATE_MESSAGE_ID,
2249
+ });
2250
+ }
2251
+ }
2252
+ }
2253
+ const config = {
2254
+ create(context) {
2255
+ return {
2256
+ Element(rawNode) {
2257
+ const node = rawNode;
2258
+ checkProjectAsOnDynamicOutlet(context, node);
2259
+ checkNestedTemplateInDynamicOutlet(context, node);
2260
+ },
2261
+ };
2262
+ },
2263
+ meta: {
2264
+ docs: { description: PROJECT_AS_ERROR_MESSAGE },
2265
+ messages: {
2266
+ [MESSAGE_ID$2]: PROJECT_AS_ERROR_MESSAGE,
2267
+ [NESTED_TEMPLATE_MESSAGE_ID]: NESTED_TEMPLATE_ERROR_MESSAGE,
2268
+ },
2269
+ schema: [],
2270
+ type: 'problem',
2271
+ },
2272
+ };
2273
+
2194
2274
  const createRule$7 = ESLintUtils.RuleCreator((name) => name);
2275
+ function collectArrayExpressions(node) {
2276
+ const result = [];
2277
+ if (node.type === AST_NODE_TYPES.ArrayExpression) {
2278
+ result.push(node);
2279
+ }
2280
+ switch (node.type) {
2281
+ case AST_NODE_TYPES.BinaryExpression:
2282
+ case AST_NODE_TYPES.LogicalExpression:
2283
+ result.push(...collectArrayExpressions(node.left));
2284
+ result.push(...collectArrayExpressions(node.right));
2285
+ break;
2286
+ case AST_NODE_TYPES.ConditionalExpression:
2287
+ result.push(...collectArrayExpressions(node.consequent));
2288
+ result.push(...collectArrayExpressions(node.alternate));
2289
+ break;
2290
+ case AST_NODE_TYPES.ObjectExpression:
2291
+ for (const property of node.properties) {
2292
+ if (property.type === AST_NODE_TYPES.Property) {
2293
+ result.push(...collectArrayExpressions(property.value));
2294
+ }
2295
+ }
2296
+ break;
2297
+ }
2298
+ return result;
2299
+ }
2195
2300
  const rule$5 = createRule$7({
2196
2301
  create(context) {
2197
2302
  const parserServices = ESLintUtils.getParserServices(context);
2198
2303
  const typeChecker = parserServices.program.getTypeChecker();
2304
+ const ignoreTupleContextualTyping = context.options[0]?.ignoreTupleContextualTyping ?? true;
2199
2305
  function check(node, typeAnnotation, value) {
2200
2306
  if (!typeAnnotation || !value) {
2201
2307
  return;
@@ -2218,12 +2324,18 @@ const rule$5 = createRule$7({
2218
2324
  !value.returnType) {
2219
2325
  return;
2220
2326
  }
2221
- // If the declared type is a tuple and the initializer is an array literal,
2222
- // the annotation provides contextual typing that narrows the inferred type
2223
- // from T[] to [T1, T2, ...]. Removing it would widen the type back to T[].
2224
- if (value.type === AST_NODE_TYPES.ArrayExpression &&
2225
- typeChecker.isTupleType(declaredType)) {
2226
- return;
2327
+ // If the annotation provides contextual typing that narrows an array
2328
+ // literal to a tuple (e.g. [0, 0] readonly [number, number]), removing
2329
+ // it would widen the inferred type. This covers both direct array literals
2330
+ // and arrays nested inside object literals or conditional expressions.
2331
+ if (ignoreTupleContextualTyping) {
2332
+ const arrayExpressions = collectArrayExpressions(value);
2333
+ for (const arrayExpression of arrayExpressions) {
2334
+ const tsArrayNode = parserServices.esTreeNodeToTSNodeMap.get(arrayExpression);
2335
+ if (typeChecker.isTupleType(typeChecker.getTypeAtLocation(tsArrayNode))) {
2336
+ return;
2337
+ }
2338
+ }
2227
2339
  }
2228
2340
  // If the initializer is a call to a generic function with no explicit
2229
2341
  // type arguments, the type parameters may be inferred from the
@@ -2264,7 +2376,18 @@ const rule$5 = createRule$7({
2264
2376
  messages: {
2265
2377
  redundantTypeAnnotation: 'Type annotation is redundant — the type is already inferred from the initializer',
2266
2378
  },
2267
- schema: [],
2379
+ schema: [
2380
+ {
2381
+ additionalProperties: false,
2382
+ properties: {
2383
+ ignoreTupleContextualTyping: {
2384
+ description: 'Preserve annotations when they provide contextual typing that narrows an array literal to a tuple type. Defaults to true.',
2385
+ type: 'boolean',
2386
+ },
2387
+ },
2388
+ type: 'object',
2389
+ },
2390
+ ],
2268
2391
  type: 'suggestion',
2269
2392
  },
2270
2393
  name: 'no-redundant-type-annotation',
@@ -3627,16 +3750,17 @@ const plugin = {
3627
3750
  version: pkg.version,
3628
3751
  },
3629
3752
  rules: {
3630
- 'array-as-const': config$2,
3753
+ 'array-as-const': config$3,
3631
3754
  'class-property-naming': classPropertyNaming,
3632
- 'decorator-key-sort': config$1,
3755
+ 'decorator-key-sort': config$2,
3633
3756
  'flat-exports': flatExports,
3634
3757
  'injection-token-description': rule$9,
3635
3758
  'no-deep-imports': rule$8,
3636
3759
  'no-deep-imports-to-indexed-packages': noDeepImportsToIndexedPackages,
3637
- 'no-href-with-router-link': config,
3760
+ 'no-href-with-router-link': config$1,
3638
3761
  'no-implicit-public': rule$7,
3639
3762
  'no-playwright-empty-fill': rule$6,
3763
+ 'no-project-as-in-ng-template': config,
3640
3764
  'no-redundant-type-annotation': rule$5,
3641
3765
  'no-string-literal-concat': rule$4,
3642
3766
  'object-single-line': rule$3,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ui/eslint-plugin-experience-next",
3
- "version": "0.459.0",
3
+ "version": "0.460.0",
4
4
  "description": "An ESLint plugin to enforce a consistent code styles across taiga-ui projects",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,23 +27,23 @@
27
27
  ],
28
28
  "type": "module",
29
29
  "devDependencies": {
30
- "@typescript-eslint/rule-tester": "8.58.0",
30
+ "@typescript-eslint/rule-tester": "8.58.1",
31
31
  "glob": "13.0.6"
32
32
  },
33
33
  "peerDependencies": {
34
- "@eslint/compat": "^2.0.4",
34
+ "@eslint/compat": "^2.0.5",
35
35
  "@eslint/markdown": "^8.0.1",
36
36
  "@html-eslint/eslint-plugin": "^0.59.0",
37
37
  "@html-eslint/parser": "^0.59.0",
38
38
  "@smarttools/eslint-plugin-rxjs": "^1.0.22",
39
39
  "@stylistic/eslint-plugin": "^5.10.0",
40
40
  "@types/glob": "*",
41
- "@typescript-eslint/eslint-plugin": "^8.58.0",
41
+ "@typescript-eslint/eslint-plugin": "^8.58.1",
42
42
  "angular-eslint": "^20.7.0",
43
43
  "eslint": "^9.39.2",
44
44
  "eslint-config-prettier": "^10.1.7",
45
45
  "eslint-plugin-compat": "^7.0.1",
46
- "eslint-plugin-cypress": "^6.2.3",
46
+ "eslint-plugin-cypress": "^6.3.0",
47
47
  "eslint-plugin-de-morgan": "^2.1.1",
48
48
  "eslint-plugin-decorator-position": "^6.0.0",
49
49
  "eslint-plugin-file-progress": "^3.0.2",
@@ -61,7 +61,7 @@
61
61
  "eslint-plugin-unused-imports": "^4.4.1",
62
62
  "glob": "*",
63
63
  "globals": "^17.4.0",
64
- "typescript-eslint": "^8.58.0"
64
+ "typescript-eslint": "^8.58.1"
65
65
  },
66
66
  "publishConfig": {
67
67
  "access": "public"
@@ -0,0 +1,3 @@
1
+ import { type Rule } from 'eslint';
2
+ declare const config: Rule.RuleModule;
3
+ export default config;
@@ -1,5 +1,10 @@
1
1
  import { ESLintUtils } from '@typescript-eslint/utils';
2
- export declare const rule: ESLintUtils.RuleModule<"redundantTypeAnnotation", [], unknown, ESLintUtils.RuleListener> & {
2
+ type Options = [
3
+ {
4
+ ignoreTupleContextualTyping?: boolean;
5
+ }?
6
+ ];
7
+ export declare const rule: ESLintUtils.RuleModule<"redundantTypeAnnotation", Options, unknown, ESLintUtils.RuleListener> & {
3
8
  name: string;
4
9
  };
5
10
  export default rule;