@taiga-ui/eslint-plugin-experience-next 0.499.0 → 0.501.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/index.esm.js +146 -11
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -80,7 +80,7 @@ from third-party plugins. The exact severities and file globs live in
80
80
  | [flat-exports](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/flat-exports.md) | Spread nested arrays when exporting Angular entity collections | | 🔧 | |
81
81
  | [host-attributes-sort](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/host-attributes-sort.md) | Sort Angular host metadata attributes using configurable attribute groups | ✅ | 🔧 | |
82
82
  | [html-logical-properties](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/html-logical-properties.md) | Enforce logical CSS properties over directional ones in Angular template style bindings | ✅ | 🔧 | |
83
- | [import-integrity](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/import-integrity.md) | Fast default import, namespace export, named-as-default, and import cycle checks | ✅ | | |
83
+ | [import-integrity](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/import-integrity.md) | Fast default import, namespace export, named-as-default, and import cycle checks | ✅ | 🔧 | |
84
84
  | [injection-token-description](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/injection-token-description.md) | Require `InjectionToken` descriptions to include the token name | ✅ | 🔧 | |
85
85
  | [no-commonjs-import-patterns](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-commonjs-import-patterns.md) | Disallow legacy CommonJS interop import patterns | ✅ | | |
86
86
  | [no-deep-imports](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-deep-imports.md) | Disables deep imports of Taiga UI packages | ✅ | 🔧 | |
package/index.esm.js CHANGED
@@ -247671,6 +247671,15 @@ const moduleExportNamesCache = new WeakMap();
247671
247671
  const resolutionStateByProgram = new WeakMap();
247672
247672
  const sourceFileCacheByProgram = new WeakMap();
247673
247673
  const codeFileExtensionRegExp = /\.[cm]?[jt]sx?$/;
247674
+ // Angular DI functions resolve tokens at instantiation time, not at module load time,
247675
+ // so cycles where all usages are DI-only are safe and should not be reported.
247676
+ const ANGULAR_DI_FIRST_ARG_FUNCTIONS = new Set([
247677
+ 'contentChild',
247678
+ 'contentChildren',
247679
+ 'inject',
247680
+ 'viewChild',
247681
+ 'viewChildren',
247682
+ ]);
247674
247683
  function createCanonicalFileName() {
247675
247684
  const useCaseSensitiveFileNames = ts.sys.useCaseSensitiveFileNames;
247676
247685
  return (fileName) => {
@@ -248094,6 +248103,43 @@ function getExportSpecifierName(name) {
248094
248103
  function hasNamedValueExport(exportNames, exportedName) {
248095
248104
  return exportedName !== 'default' && (exportNames?.has(exportedName) ?? false);
248096
248105
  }
248106
+ function isImportUsedOnlyAsAngularDiFirstArg(node, sourceCode) {
248107
+ const valueSpecifiers = node.specifiers.filter((specifier) => {
248108
+ if (specifier.type === dist$3.AST_NODE_TYPES.ImportDefaultSpecifier ||
248109
+ specifier.type === dist$3.AST_NODE_TYPES.ImportNamespaceSpecifier) {
248110
+ return true;
248111
+ }
248112
+ return specifier.importKind !== 'type';
248113
+ });
248114
+ if (valueSpecifiers.length === 0) {
248115
+ return false;
248116
+ }
248117
+ for (const specifier of valueSpecifiers) {
248118
+ const [variable] = sourceCode.getDeclaredVariables(specifier);
248119
+ if (!variable || variable.references.length === 0) {
248120
+ return false;
248121
+ }
248122
+ for (const ref of variable.references) {
248123
+ const { identifier } = ref;
248124
+ const parent = identifier.parent;
248125
+ const callee = parent.type === dist$3.AST_NODE_TYPES.CallExpression ? parent.callee : null;
248126
+ const isDirectCall = callee?.type === dist$3.AST_NODE_TYPES.Identifier &&
248127
+ ANGULAR_DI_FIRST_ARG_FUNCTIONS.has(callee.name);
248128
+ const isRequiredCall = callee?.type === dist$3.AST_NODE_TYPES.MemberExpression &&
248129
+ !callee.computed &&
248130
+ callee.object.type === dist$3.AST_NODE_TYPES.Identifier &&
248131
+ ANGULAR_DI_FIRST_ARG_FUNCTIONS.has(callee.object.name) &&
248132
+ callee.property.type === dist$3.AST_NODE_TYPES.Identifier &&
248133
+ callee.property.name === 'required';
248134
+ if (parent.type !== dist$3.AST_NODE_TYPES.CallExpression ||
248135
+ (!isDirectCall && !isRequiredCall) ||
248136
+ parent.arguments[0] !== identifier) {
248137
+ return false;
248138
+ }
248139
+ }
248140
+ }
248141
+ return true;
248142
+ }
248097
248143
  const rule$K = createRule({
248098
248144
  create(context) {
248099
248145
  const { checker, esTreeNodeToTSNodeMap, sourceCode, tsProgram } = getTypeAwareRuleContext(context);
@@ -248119,7 +248165,9 @@ const rule$K = createRule({
248119
248165
  }
248120
248166
  const graph = getImportGraph(tsProgram);
248121
248167
  const targetFileName = findCyclicTargetFileName(graph, currentFileName, moduleSpecifier);
248122
- if (!targetFileName) {
248168
+ if (!targetFileName ||
248169
+ (node.type === dist$3.AST_NODE_TYPES.ImportDeclaration &&
248170
+ isImportUsedOnlyAsAngularDiFirstArg(node, sourceCode))) {
248123
248171
  return;
248124
248172
  }
248125
248173
  context.report({
@@ -248222,6 +248270,42 @@ const rule$K = createRule({
248222
248270
  variable,
248223
248271
  });
248224
248272
  }
248273
+ function buildNamespaceToNamedFix(fixer, usage) {
248274
+ const importDecl = usage.node.parent;
248275
+ if (importDecl.specifiers.length !== 1) {
248276
+ return null;
248277
+ }
248278
+ const memberNames = new Set();
248279
+ const memberNodes = [];
248280
+ for (const ref of usage.variable.references) {
248281
+ const parent = ref.identifier.parent;
248282
+ if (parent.type !== dist$3.AST_NODE_TYPES.MemberExpression ||
248283
+ parent.object !== ref.identifier ||
248284
+ parent.computed) {
248285
+ return null;
248286
+ }
248287
+ const memberName = getMemberExpressionPropertyName(parent);
248288
+ if (!memberName || memberName === 'default') {
248289
+ return null;
248290
+ }
248291
+ memberNames.add(memberName);
248292
+ memberNodes.push(parent);
248293
+ }
248294
+ if (memberNames.size === 0) {
248295
+ return null;
248296
+ }
248297
+ const sourceText = sourceCode.getText(importDecl.source);
248298
+ const hasSemi = sourceCode.getText(importDecl).endsWith(';');
248299
+ const sortedMembers = [...memberNames].sort();
248300
+ const newImportText = `import {${sortedMembers.join(', ')}} from ${sourceText}${hasSemi ? ';' : ''}`;
248301
+ return [
248302
+ fixer.replaceText(importDecl, newImportText),
248303
+ ...memberNodes.flatMap((memberNode) => {
248304
+ const name = getMemberExpressionPropertyName(memberNode);
248305
+ return name ? [fixer.replaceText(memberNode, name)] : [];
248306
+ }),
248307
+ ];
248308
+ }
248225
248309
  function checkNamespaceMember(node) {
248226
248310
  if (!checkNamespaceMembers ||
248227
248311
  node.object.type !== dist$3.AST_NODE_TYPES.Identifier) {
@@ -248241,6 +248325,7 @@ const rule$K = createRule({
248241
248325
  moduleSpecifier: usage.moduleSpecifier,
248242
248326
  namespaceName: usage.node.local.name,
248243
248327
  },
248328
+ fix: (fixer) => buildNamespaceToNamedFix(fixer, usage),
248244
248329
  messageId: 'unknownNamespaceMember',
248245
248330
  node: node.property,
248246
248331
  });
@@ -248263,6 +248348,7 @@ const rule$K = createRule({
248263
248348
  docs: {
248264
248349
  description: 'Fast replacement for import/default, import/namespace, import/no-cycle, and import/no-named-as-default checks',
248265
248350
  },
248351
+ fixable: 'code',
248266
248352
  messages: {
248267
248353
  importCycle: 'Import cycle detected: {{cyclePath}}.',
248268
248354
  missingDefaultExport: 'No default export found in "{{moduleSpecifier}}".',
@@ -250629,6 +250715,9 @@ function getCalleeName(node) {
250629
250715
  }
250630
250716
  function findParentStatement(node) {
250631
250717
  for (let current = node; current.parent; current = current.parent) {
250718
+ if (isFunctionLike(current)) {
250719
+ return null;
250720
+ }
250632
250721
  if (current.parent.type === dist$3.AST_NODE_TYPES.BlockStatement ||
250633
250722
  current.parent.type === dist$3.AST_NODE_TYPES.Program) {
250634
250723
  return current;
@@ -250636,6 +250725,26 @@ function findParentStatement(node) {
250636
250725
  }
250637
250726
  return null;
250638
250727
  }
250728
+ function findConciseArrowAncestor(node) {
250729
+ for (let current = node; current.parent; current = current.parent) {
250730
+ const { parent } = current;
250731
+ if (parent.type === dist$3.AST_NODE_TYPES.ArrowFunctionExpression &&
250732
+ parent.body.type !== dist$3.AST_NODE_TYPES.BlockStatement) {
250733
+ return parent;
250734
+ }
250735
+ if (isFunctionLike(parent)) {
250736
+ return null;
250737
+ }
250738
+ }
250739
+ return null;
250740
+ }
250741
+ function getArrowBodyIndent(arrowFn, sourceText) {
250742
+ const arrowStart = arrowFn.range[0];
250743
+ const lineStart = sourceText.lastIndexOf('\n', arrowStart - 1) + 1;
250744
+ const textBeforeArrow = sourceText.slice(lineStart, arrowStart);
250745
+ const outerIndent = /^(\s*)/.exec(textBeforeArrow)?.[1] ?? '';
250746
+ return { innerIndent: `${outerIndent} `, outerIndent };
250747
+ }
250639
250748
  function getStatementIndent(statement, sourceText) {
250640
250749
  const start = statement.range[0];
250641
250750
  const lineStart = sourceText.lastIndexOf('\n', start - 1) + 1;
@@ -250678,23 +250787,49 @@ const rule$r = createRule({
250678
250787
  context.report({
250679
250788
  data: { call: callText },
250680
250789
  fix(fixer) {
250790
+ const varName = getCalleeName(firstCall);
250681
250791
  const parentStatement = findParentStatement(node);
250682
- if (!parentStatement) {
250792
+ if (parentStatement) {
250793
+ const indent = getStatementIndent(parentStatement, sourceCode.text);
250794
+ const fixes = [
250795
+ fixer.insertTextBefore(parentStatement, `const ${varName} = ${callText};\n\n${indent}`),
250796
+ ];
250797
+ for (const call of calls) {
250798
+ const { parent } = call;
250799
+ const target = parent.type === dist$3.AST_NODE_TYPES.TSAsExpression
250800
+ ? parent
250801
+ : call;
250802
+ fixes.push(fixer.replaceText(target, varName));
250803
+ }
250804
+ return fixes;
250805
+ }
250806
+ const arrowFn = findConciseArrowAncestor(node);
250807
+ if (!arrowFn) {
250683
250808
  return null;
250684
250809
  }
250685
- const varName = getCalleeName(firstCall);
250686
- const indent = getStatementIndent(parentStatement, sourceCode.text);
250687
- const fixes = [
250688
- fixer.insertTextBefore(parentStatement, `const ${varName} = ${callText};\n\n${indent}`),
250689
- ];
250690
- for (const call of calls) {
250810
+ const arrowBody = arrowFn.body;
250811
+ const { innerIndent, outerIndent } = getArrowBodyIndent(arrowFn, sourceCode.text);
250812
+ const bodyText = sourceCode.getText(arrowBody);
250813
+ const bodyStart = arrowBody.range[0];
250814
+ const targets = calls
250815
+ .map((call) => {
250691
250816
  const { parent } = call;
250692
- const target = parent.type === dist$3.AST_NODE_TYPES.TSAsExpression
250817
+ return parent.type === dist$3.AST_NODE_TYPES.TSAsExpression
250693
250818
  ? parent
250694
250819
  : call;
250695
- fixes.push(fixer.replaceText(target, varName));
250820
+ })
250821
+ .sort((a, b) => a.range[0] - b.range[0]);
250822
+ let replacedBody = '';
250823
+ let lastIndex = 0;
250824
+ for (const target of targets) {
250825
+ const start = target.range[0] - bodyStart;
250826
+ const end = target.range[1] - bodyStart;
250827
+ replacedBody += `${bodyText.slice(lastIndex, start)}${varName}`;
250828
+ lastIndex = end;
250696
250829
  }
250697
- return fixes;
250830
+ replacedBody += bodyText.slice(lastIndex);
250831
+ const newBody = `{\n${innerIndent}const ${varName} = ${callText};\n\n${innerIndent}return ${replacedBody};\n${outerIndent}}`;
250832
+ return [fixer.replaceText(arrowBody, newBody)];
250698
250833
  },
250699
250834
  messageId: 'noRepeatedSignalInConditional',
250700
250835
  node: firstCall,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ui/eslint-plugin-experience-next",
3
- "version": "0.499.0",
3
+ "version": "0.501.0",
4
4
  "description": "An ESLint plugin to enforce a consistent code styles across taiga-ui projects",
5
5
  "repository": {
6
6
  "type": "git",
@@ -46,7 +46,7 @@
46
46
  "eslint-plugin-jest": "^29.15.2",
47
47
  "eslint-plugin-package-json": "^0.91.1",
48
48
  "eslint-plugin-perfectionist": "^5.9.0",
49
- "eslint-plugin-playwright": "^2.10.1",
49
+ "eslint-plugin-playwright": "^2.10.2",
50
50
  "eslint-plugin-prettier": "^5.5.5",
51
51
  "eslint-plugin-promise": "^7.2.1",
52
52
  "eslint-plugin-regexp": "^3.1.0",