@taiga-ui/eslint-plugin-experience-next 0.455.0 → 0.457.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-redundant-type-annotation | Disallow redundant type annotations when the type is already inferred from the initializer | ✅ | 🔧 | |
52
53
  | no-string-literal-concat | Disallow string literal concatenation; merge adjacent literals into one | ✅ | 🔧 | |
53
54
  | object-single-line | Enforce single-line formatting for single-property objects when it fits `printWidth` | ✅ | 🔧 | |
54
55
  | prefer-deep-imports | Allow deep imports of Taiga UI packages | | 🔧 | |
@@ -501,6 +502,42 @@ Spread elements are placed after named identifiers.
501
502
 
502
503
  ---
503
504
 
505
+ ## no-redundant-type-annotation
506
+
507
+ Disallow explicit type annotations on class properties and variable declarations when TypeScript can already infer the
508
+ same type from the initializer. Requires type information (`parserOptions.project`).
509
+
510
+ Works well in combination with `unused-imports/no-unused-imports` or `@typescript-eslint/no-unused-vars`, which will
511
+ then clean up any import that is no longer referenced after the annotation is removed.
512
+
513
+ ```ts
514
+ // ❌ error — type is already inferred from inject()
515
+ private readonly options: TuiInputNumberOptions = inject(TUI_INPUT_NUMBER_OPTIONS);
516
+
517
+ // ✅ after autofix
518
+ private readonly options = inject(TUI_INPUT_NUMBER_OPTIONS);
519
+ ```
520
+
521
+ ```ts
522
+ // ❌ error — variable declaration
523
+ const service: MyService = inject(MyService);
524
+
525
+ // ✅ after autofix
526
+ const service = inject(MyService);
527
+ ```
528
+
529
+ The rule does **not** report when the annotation intentionally widens or changes the type:
530
+
531
+ ```ts
532
+ // ✅ ok — annotation widens Dog to Animal
533
+ x: Animal = new Dog();
534
+
535
+ // ✅ ok — annotation adds null to the union
536
+ x: MyService | null = inject(MyService);
537
+ ```
538
+
539
+ ---
540
+
504
541
  ## strict-tui-doc-example
505
542
 
506
543
  Validates that properties of a `TuiDocExample`-typed object have keys matching known file-type names (`TypeScript`,
package/index.d.ts CHANGED
@@ -40,6 +40,9 @@ 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> & {
44
+ name: string;
45
+ };
43
46
  'no-string-literal-concat': import("@typescript-eslint/utils/ts-eslint").RuleModule<"flattenTemplate" | "mergeLiterals" | "useTemplate", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
44
47
  name: string;
45
48
  };
package/index.esm.js CHANGED
@@ -29,7 +29,7 @@ import tseslint from 'typescript-eslint';
29
29
  import { createRequire } from 'node:module';
30
30
  import { globSync } from 'glob';
31
31
  import { ESLintUtils, AST_NODE_TYPES } from '@typescript-eslint/utils';
32
- import ts from 'typescript';
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
35
 
@@ -916,6 +916,7 @@ var recommended = defineConfig([
916
916
  ],
917
917
  '@taiga-ui/experience-next/no-deep-imports-to-indexed-packages': 'error',
918
918
  '@taiga-ui/experience-next/no-implicit-public': 'error',
919
+ '@taiga-ui/experience-next/no-redundant-type-annotation': 'error',
919
920
  '@taiga-ui/experience-next/object-single-line': ['error', { printWidth: 90 }],
920
921
  '@taiga-ui/experience-next/prefer-multi-arg-push': 'error',
921
922
  '@taiga-ui/experience-next/short-tui-imports': 'error',
@@ -1329,8 +1330,8 @@ function intersect(a, b) {
1329
1330
  return a.some((type) => origin.has(type));
1330
1331
  }
1331
1332
 
1332
- const createRule$d = ESLintUtils.RuleCreator((name) => name);
1333
- var classPropertyNaming = createRule$d({
1333
+ const createRule$e = ESLintUtils.RuleCreator((name) => name);
1334
+ var classPropertyNaming = createRule$e({
1334
1335
  create(context, [configs]) {
1335
1336
  const parserServices = ESLintUtils.getParserServices(context);
1336
1337
  const typeChecker = parserServices.program.getTypeChecker();
@@ -1499,9 +1500,9 @@ function isExternalPureTuple(typeChecker, type) {
1499
1500
  return typeArgs.every((item) => isClassType(item));
1500
1501
  }
1501
1502
 
1502
- const createRule$c = ESLintUtils.RuleCreator((name) => name);
1503
+ const createRule$d = ESLintUtils.RuleCreator((name) => name);
1503
1504
  const MESSAGE_ID$5 = 'spreadArrays';
1504
- var flatExports = createRule$c({
1505
+ var flatExports = createRule$d({
1505
1506
  create(context) {
1506
1507
  const parserServices = ESLintUtils.getParserServices(context);
1507
1508
  const typeChecker = parserServices.program.getTypeChecker();
@@ -1627,8 +1628,8 @@ var flatExports = createRule$c({
1627
1628
 
1628
1629
  const MESSAGE_ID$4 = 'invalid-injection-token-description';
1629
1630
  const ERROR_MESSAGE$3 = "InjectionToken's description should contain token's name";
1630
- const createRule$b = ESLintUtils.RuleCreator((name) => name);
1631
- const rule$8 = createRule$b({
1631
+ const createRule$c = ESLintUtils.RuleCreator((name) => name);
1632
+ const rule$9 = createRule$c({
1632
1633
  create(context) {
1633
1634
  return {
1634
1635
  'NewExpression[callee.name="InjectionToken"]'(node) {
@@ -1695,8 +1696,8 @@ const DEFAULT_OPTIONS = {
1695
1696
  importDeclaration: '^@taiga-ui*',
1696
1697
  projectName: String.raw `(?<=^@taiga-ui/)([-\w]+)`,
1697
1698
  };
1698
- const createRule$a = ESLintUtils.RuleCreator((name) => name);
1699
- const rule$7 = createRule$a({
1699
+ const createRule$b = ESLintUtils.RuleCreator((name) => name);
1700
+ const rule$8 = createRule$b({
1700
1701
  create(context) {
1701
1702
  const { currentProject, deepImport, ignoreImports, importDeclaration, projectName, } = { ...DEFAULT_OPTIONS, ...context.options[0] };
1702
1703
  const hasNonCodeExtension = (source) => {
@@ -1783,13 +1784,13 @@ const rule$7 = createRule$a({
1783
1784
  name: 'no-deep-imports',
1784
1785
  });
1785
1786
 
1786
- const createRule$9 = ESLintUtils.RuleCreator((name) => name);
1787
+ const createRule$a = ESLintUtils.RuleCreator((name) => name);
1787
1788
  const resolveCacheByOptions = new WeakMap();
1788
1789
  const nearestFileUpCache = new Map();
1789
1790
  const markerCache = new Map();
1790
1791
  const indexFileCache = new Map();
1791
1792
  const indexExportsCache = new Map();
1792
- var noDeepImportsToIndexedPackages = createRule$9({
1793
+ var noDeepImportsToIndexedPackages = createRule$a({
1793
1794
  create(context) {
1794
1795
  const parserServices = ESLintUtils.getParserServices(context);
1795
1796
  const program = parserServices.program;
@@ -2031,8 +2032,8 @@ const config = {
2031
2032
  },
2032
2033
  };
2033
2034
 
2034
- const createRule$8 = ESLintUtils.RuleCreator((name) => name);
2035
- const rule$6 = createRule$8({
2035
+ const createRule$9 = ESLintUtils.RuleCreator((name) => name);
2036
+ const rule$7 = createRule$9({
2036
2037
  create(context) {
2037
2038
  const checkImplicitPublic = (node) => {
2038
2039
  const classRef = getClass(node);
@@ -2103,8 +2104,8 @@ function getClass(node) {
2103
2104
  return getClass(node.parent);
2104
2105
  }
2105
2106
 
2106
- const createRule$7 = ESLintUtils.RuleCreator((name) => name);
2107
- const rule$5 = createRule$7({
2107
+ const createRule$8 = ESLintUtils.RuleCreator((name) => name);
2108
+ const rule$6 = createRule$8({
2108
2109
  create(context) {
2109
2110
  const services = ESLintUtils.getParserServices(context);
2110
2111
  const checker = services.program.getTypeChecker();
@@ -2191,6 +2192,68 @@ function isPlaywrightLocatorType(type) {
2191
2192
  });
2192
2193
  }
2193
2194
 
2195
+ const createRule$7 = ESLintUtils.RuleCreator((name) => name);
2196
+ const rule$5 = createRule$7({
2197
+ create(context) {
2198
+ const parserServices = ESLintUtils.getParserServices(context);
2199
+ const typeChecker = parserServices.program.getTypeChecker();
2200
+ function check(node, typeAnnotation, value) {
2201
+ if (!typeAnnotation || !value) {
2202
+ return;
2203
+ }
2204
+ const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
2205
+ const tsValueNode = parserServices.esTreeNodeToTSNodeMap.get(value);
2206
+ const declaredType = typeChecker.getTypeAtLocation(tsNode);
2207
+ const inferredType = typeChecker.getTypeAtLocation(tsValueNode);
2208
+ if (typeChecker.typeToString(declaredType) !==
2209
+ typeChecker.typeToString(inferredType)) {
2210
+ return;
2211
+ }
2212
+ // If the initializer is a call to a generic function with no explicit
2213
+ // type arguments, the type parameters may be inferred from the
2214
+ // contextual return type provided by this annotation. Removing the
2215
+ // annotation could change the inferred type (e.g., T → unknown).
2216
+ if (isCallExpression(tsValueNode) && !tsValueNode.typeArguments?.length) {
2217
+ const sig = typeChecker.getResolvedSignature(tsValueNode);
2218
+ const decl = sig?.declaration;
2219
+ if (decl && 'typeParameters' in decl && decl.typeParameters?.length) {
2220
+ return;
2221
+ }
2222
+ }
2223
+ context.report({
2224
+ fix(fixer) {
2225
+ return fixer.remove(typeAnnotation);
2226
+ },
2227
+ messageId: 'redundantTypeAnnotation',
2228
+ node: typeAnnotation,
2229
+ });
2230
+ }
2231
+ return {
2232
+ PropertyDefinition(node) {
2233
+ check(node, node.typeAnnotation, node.value);
2234
+ },
2235
+ VariableDeclarator(node) {
2236
+ if (node.id.type !== AST_NODE_TYPES.Identifier) {
2237
+ return;
2238
+ }
2239
+ check(node, node.id.typeAnnotation, node.init);
2240
+ },
2241
+ };
2242
+ },
2243
+ meta: {
2244
+ docs: {
2245
+ description: 'Disallow redundant type annotations when the type is already inferred from the initializer',
2246
+ },
2247
+ fixable: 'code',
2248
+ messages: {
2249
+ redundantTypeAnnotation: 'Type annotation is redundant — the type is already inferred from the initializer',
2250
+ },
2251
+ schema: [],
2252
+ type: 'suggestion',
2253
+ },
2254
+ name: 'no-redundant-type-annotation',
2255
+ });
2256
+
2194
2257
  const createRule$6 = ESLintUtils.RuleCreator((name) => name);
2195
2258
  function isStringLiteral(node) {
2196
2259
  return (node.type === AST_NODE_TYPES.Literal &&
@@ -3552,12 +3615,13 @@ const plugin = {
3552
3615
  'class-property-naming': classPropertyNaming,
3553
3616
  'decorator-key-sort': config$1,
3554
3617
  'flat-exports': flatExports,
3555
- 'injection-token-description': rule$8,
3556
- 'no-deep-imports': rule$7,
3618
+ 'injection-token-description': rule$9,
3619
+ 'no-deep-imports': rule$8,
3557
3620
  'no-deep-imports-to-indexed-packages': noDeepImportsToIndexedPackages,
3558
3621
  'no-href-with-router-link': config,
3559
- 'no-implicit-public': rule$6,
3560
- 'no-playwright-empty-fill': rule$5,
3622
+ 'no-implicit-public': rule$7,
3623
+ 'no-playwright-empty-fill': rule$6,
3624
+ 'no-redundant-type-annotation': rule$5,
3561
3625
  'no-string-literal-concat': rule$4,
3562
3626
  'object-single-line': rule$3,
3563
3627
  'prefer-deep-imports': preferDeepImports,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ui/eslint-plugin-experience-next",
3
- "version": "0.455.0",
3
+ "version": "0.457.0",
4
4
  "description": "An ESLint plugin to enforce a consistent code styles across taiga-ui projects",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,5 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const rule: ESLintUtils.RuleModule<"redundantTypeAnnotation", [], unknown, ESLintUtils.RuleListener> & {
3
+ name: string;
4
+ };
5
+ export default rule;
@@ -10,7 +10,7 @@ export type Options = [
10
10
  }
11
11
  ];
12
12
  export type MessageIds = 'replaceTuiImport';
13
- export declare const MESSAGE_ID: MessageIds;
13
+ export declare const MESSAGE_ID = "replaceTuiImport";
14
14
  export declare const rule: ESLintUtils.RuleModule<"replaceTuiImport", Options, unknown, ESLintUtils.RuleListener> & {
15
15
  name: string;
16
16
  };