@taiga-ui/eslint-plugin-experience-next 0.527.0 → 0.528.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
@@ -75,6 +75,7 @@ from third-party plugins. The exact severities and file globs live in
75
75
  | [array-as-const](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/array-as-const.md) | Exported array of class references should be marked with `as const` | | 🔧 | |
76
76
  | [at-compat](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/at-compat.md) | Keep built-in `.at()` and indexed access aligned with the TypeScript target | ✅ | 🔧 | |
77
77
  | [attrs-newline](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/attrs-newline.md) | Enforce one attribute per line when a start tag spans multiple lines | ✅ | 🔧 | |
78
+ | [class-accessibility-spacing](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/class-accessibility-spacing.md) | Separate adjacent class members with a blank line when their accessibility group changes | ✅ | 🔧 | |
78
79
  | [class-property-naming](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/class-property-naming.md) | Enforce custom naming for class properties based on their type | | 🔧 | |
79
80
  | [decorator-key-sort](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/decorator-key-sort.md) | Sorts the keys of the object passed to the `@Component/@Injectable/@NgModule/@Pipe` decorator | ✅ | 🔧 | |
80
81
  | [element-newline](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/element-newline.md) | Require line breaks around block-level child nodes in HTML templates | ✅ | 🔧 | |
package/index.d.ts CHANGED
@@ -18,6 +18,9 @@ declare const plugin: {
18
18
  'attrs-newline': import("eslint").Rule.RuleModule & {
19
19
  name: string;
20
20
  };
21
+ 'class-accessibility-spacing': import("@typescript-eslint/utils/ts-eslint").RuleModule<"classAccessibilitySpacing", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
22
+ name: string;
23
+ };
21
24
  '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> & {
22
25
  name: string;
23
26
  };
package/index.esm.js CHANGED
@@ -493,6 +493,11 @@ function hasBlankLine(text) {
493
493
  }
494
494
  return false;
495
495
  }
496
+ function hasBlankLineBetweenNodes(text) {
497
+ const lines = splitLines(text);
498
+ const linesBetweenNodes = lines.slice(1, -1);
499
+ return linesBetweenNodes.some((line) => line.trim() === '');
500
+ }
496
501
  function getLineBreak(text) {
497
502
  if (text.includes('\r\n')) {
498
503
  return '\r\n';
@@ -1247,6 +1252,7 @@ var recommended = defineConfig([
1247
1252
  '@angular-eslint/use-lifecycle-interface': 'error',
1248
1253
  '@angular-eslint/use-pipe-transform-interface': 'error',
1249
1254
  '@taiga-ui/experience-next/at-compat': 'error',
1255
+ '@taiga-ui/experience-next/class-accessibility-spacing': 'error',
1250
1256
  '@taiga-ui/experience-next/decorator-key-sort': [
1251
1257
  'error',
1252
1258
  {
@@ -14974,6 +14980,40 @@ function getParenthesizedExpression(expression) {
14974
14980
  : null;
14975
14981
  }
14976
14982
 
14983
+ function isFieldLikeMember(member) {
14984
+ return (member.type === dist$4.AST_NODE_TYPES.PropertyDefinition ||
14985
+ member.type === dist$4.AST_NODE_TYPES.TSAbstractPropertyDefinition);
14986
+ }
14987
+ function isAccessorMember(member) {
14988
+ return (member.type === dist$4.AST_NODE_TYPES.MethodDefinition &&
14989
+ (member.kind === 'get' || member.kind === 'set'));
14990
+ }
14991
+ function isRelevantSpacingClassMember(member) {
14992
+ return isFieldLikeMember(member) || isAccessorMember(member);
14993
+ }
14994
+ function isAccessibilityClassMember(member) {
14995
+ switch (member.type) {
14996
+ case dist$4.AST_NODE_TYPES.AccessorProperty:
14997
+ case dist$4.AST_NODE_TYPES.MethodDefinition:
14998
+ case dist$4.AST_NODE_TYPES.PropertyDefinition:
14999
+ case dist$4.AST_NODE_TYPES.TSAbstractAccessorProperty:
15000
+ case dist$4.AST_NODE_TYPES.TSAbstractMethodDefinition:
15001
+ case dist$4.AST_NODE_TYPES.TSAbstractPropertyDefinition:
15002
+ case dist$4.AST_NODE_TYPES.TSIndexSignature:
15003
+ return true;
15004
+ default:
15005
+ return false;
15006
+ }
15007
+ }
15008
+ function isEcmascriptPrivateClassMember(member) {
15009
+ return 'key' in member && member.key.type === dist$4.AST_NODE_TYPES.PrivateIdentifier;
15010
+ }
15011
+ function getAccessibilityGroup(member) {
15012
+ return isEcmascriptPrivateClassMember(member)
15013
+ ? 'private'
15014
+ : (member.accessibility ?? 'public');
15015
+ }
15016
+
14977
15017
  const EQUALITY_OPERATORS = new Set(['!=', '!==', '==', '===']);
14978
15018
  function getChainExpressionRoot(node) {
14979
15019
  const parent = getParentNode(node);
@@ -15189,7 +15229,7 @@ function getMemberExpressionPropertyName(node) {
15189
15229
  return node.computed ? getStaticStringValue(node.property) : null;
15190
15230
  }
15191
15231
  function getClassMemberName(member) {
15192
- return member.key.type === dist$4.AST_NODE_TYPES.PrivateIdentifier
15232
+ return isEcmascriptPrivateClassMember(member)
15193
15233
  ? null
15194
15234
  : getStaticPropertyName(member.key);
15195
15235
  }
@@ -216112,12 +216152,10 @@ function getPreferredTemporaryNames(node) {
216112
216152
  ];
216113
216153
  }
216114
216154
  function getClassElementName(member) {
216115
- if (!('key' in member)) {
216116
- return null;
216155
+ if (isEcmascriptPrivateClassMember(member)) {
216156
+ return member.key.name;
216117
216157
  }
216118
- return member.key.type === dist$4.AST_NODE_TYPES.PrivateIdentifier
216119
- ? member.key.name
216120
- : getStaticPropertyName(member.key);
216158
+ return 'key' in member ? getStaticPropertyName(member.key) : null;
216121
216159
  }
216122
216160
  function hasClassElementName(classBody, name) {
216123
216161
  return classBody.body.some((member) => getClassElementName(member) === name);
@@ -216267,7 +216305,7 @@ function getLastIndexFix(fixer, sourceCode, node, call) {
216267
216305
  }
216268
216306
  return null;
216269
216307
  }
216270
- const rule$X = createRule({
216308
+ const rule$Y = createRule({
216271
216309
  create(context) {
216272
216310
  const typeAwareContext = getTypeAwareRuleContext(context);
216273
216311
  const compilerOptions = typeAwareContext.tsProgram.getCompilerOptions();
@@ -248073,7 +248111,7 @@ function buildMultilineStartTag(node, sourceText) {
248073
248111
  closing,
248074
248112
  ].join(lineBreak);
248075
248113
  }
248076
- const rule$W = createRule({
248114
+ const rule$X = createRule({
248077
248115
  name: 'attrs-newline',
248078
248116
  rule: {
248079
248117
  create(context) {
@@ -248159,6 +248197,59 @@ const rule$W = createRule({
248159
248197
  },
248160
248198
  });
248161
248199
 
248200
+ const rule$W = createRule({
248201
+ create(context) {
248202
+ const sourceCode = context.sourceCode;
248203
+ return {
248204
+ ClassBody(node) {
248205
+ for (let index = 0; index < node.body.length - 1; index++) {
248206
+ const current = node.body[index];
248207
+ const next = node.body[index + 1];
248208
+ if (!current ||
248209
+ !next ||
248210
+ !isAccessibilityClassMember(current) ||
248211
+ !isAccessibilityClassMember(next)) {
248212
+ continue;
248213
+ }
248214
+ const accessibilityGroupChanged = getAccessibilityGroup(current) !== getAccessibilityGroup(next);
248215
+ if (!accessibilityGroupChanged) {
248216
+ continue;
248217
+ }
248218
+ const betweenText = sourceCode.text.slice(current.range[1], next.range[0]);
248219
+ const missingBlankLine = !hasBlankLineBetweenNodes(betweenText);
248220
+ if (!missingBlankLine) {
248221
+ continue;
248222
+ }
248223
+ const reportTarget = {
248224
+ messageId: 'classAccessibilitySpacing',
248225
+ node: next,
248226
+ };
248227
+ if (hasCommentLikeText(betweenText)) {
248228
+ context.report(reportTarget);
248229
+ continue;
248230
+ }
248231
+ context.report({
248232
+ ...reportTarget,
248233
+ fix: (fixer) => fixer.replaceTextRange([current.range[1], next.range[0]], getSpacingReplacement(sourceCode, betweenText, next.loc.start.line, 1)),
248234
+ });
248235
+ }
248236
+ },
248237
+ };
248238
+ },
248239
+ meta: {
248240
+ docs: {
248241
+ description: 'Require a blank line between adjacent class members when their accessibility group changes',
248242
+ },
248243
+ fixable: 'code',
248244
+ messages: {
248245
+ classAccessibilitySpacing: 'Class members with different accessibility groups should be separated by a blank line',
248246
+ },
248247
+ schema: [],
248248
+ type: 'layout',
248249
+ },
248250
+ name: 'class-accessibility-spacing',
248251
+ });
248252
+
248162
248253
  function isObject(node) {
248163
248254
  return node?.type === dist$3.AST_NODE_TYPES.ObjectExpression;
248164
248255
  }
@@ -251223,40 +251314,78 @@ const rule$H = createRule({
251223
251314
  rule: config$3,
251224
251315
  });
251225
251316
 
251317
+ function isClassMemberCandidate(node) {
251318
+ return (node.type === dist$4.AST_NODE_TYPES.MethodDefinition ||
251319
+ node.type === dist$4.AST_NODE_TYPES.PropertyDefinition);
251320
+ }
251321
+ function isConstructorMember(node) {
251322
+ return node.type === dist$4.AST_NODE_TYPES.MethodDefinition && node.kind === 'constructor';
251323
+ }
251324
+ function getParameterPropertyName(node) {
251325
+ const { parameter } = node;
251326
+ return parameter.type === dist$4.AST_NODE_TYPES.Identifier
251327
+ ? parameter.name
251328
+ : parameter.left.name;
251329
+ }
251330
+ function getCandidateName(node) {
251331
+ return node.type === dist$4.AST_NODE_TYPES.TSParameterProperty
251332
+ ? (getParameterPropertyName(node) ?? 'member')
251333
+ : (getStaticPropertyName(node.key) ?? 'member');
251334
+ }
251335
+ function getCandidateKind(node) {
251336
+ return node.type === dist$4.AST_NODE_TYPES.MethodDefinition ? node.kind : 'property';
251337
+ }
251338
+ function getFirstNonWhitespaceOffset(text, offset) {
251339
+ let index = offset;
251340
+ while (index < text.length && text[index]?.trim() === '') {
251341
+ index++;
251342
+ }
251343
+ return index;
251344
+ }
251345
+ function getPublicModifierInsertion(options) {
251346
+ const { node, sourceCode } = options;
251347
+ const lastDecoratorIndex = node.decorators.length - 1;
251348
+ const lastDecorator = node.decorators[lastDecoratorIndex];
251349
+ if (lastDecorator) {
251350
+ const [, end] = lastDecorator.range;
251351
+ const memberStart = getFirstNonWhitespaceOffset(sourceCode.text, end);
251352
+ const hasWhitespaceAfterDecorator = memberStart > end;
251353
+ return {
251354
+ range: [memberStart, memberStart],
251355
+ text: hasWhitespaceAfterDecorator ? 'public ' : ' public ',
251356
+ };
251357
+ }
251358
+ return {
251359
+ range: [node.range[0], node.range[0]],
251360
+ text: 'public ',
251361
+ };
251362
+ }
251226
251363
  const rule$G = createRule({
251227
251364
  create(context) {
251365
+ const sourceCode = context.sourceCode;
251228
251366
  const checkImplicitPublic = (node) => {
251229
251367
  const classRef = getEnclosingClass(node);
251230
- if (!classRef ||
251231
- node.kind === 'constructor' ||
251232
- !!node?.accessibility ||
251233
- node.key?.type === dist$4.AST_NODE_TYPES.PrivateIdentifier) {
251368
+ if (!classRef) {
251234
251369
  return;
251235
251370
  }
251236
- const name = node?.key?.name ||
251237
- node?.parameter?.name ||
251238
- (node?.key?.type === 'Identifier' ? node.key.name : 'member');
251239
- let range = node?.parameter?.range ??
251240
- node?.key?.range ??
251241
- node?.range ?? [0, 0];
251242
- if (node.kind === 'set' || node.kind === 'get') {
251243
- const [start, end] = node.key.range;
251244
- range = [start - node.kind.length - 1, end - node.kind.length - 1];
251245
- }
251246
- else if (node.kind === 'method' && node.key?.object?.name === 'Symbol') {
251247
- const [start, end] = range;
251248
- range = [start - 1, end - 1];
251249
- }
251250
- if (node.type === 'PropertyDefinition' && node.decorators?.length > 0) {
251251
- const [, end] = node.decorators[node.decorators.length - 1].range ?? [];
251252
- range = [end + 1, end + 2];
251371
+ const privateNameCannotBePublic = isClassMemberCandidate(node) && isEcmascriptPrivateClassMember(node);
251372
+ if (isConstructorMember(node) ||
251373
+ Boolean(node.accessibility) ||
251374
+ privateNameCannotBePublic) {
251375
+ return;
251253
251376
  }
251254
251377
  context.report({
251255
251378
  data: {
251256
- kind: node.kind || 'property',
251257
- name,
251379
+ kind: getCandidateKind(node),
251380
+ name: getCandidateName(node),
251381
+ },
251382
+ fix: (fixer) => {
251383
+ const { range, text } = getPublicModifierInsertion({
251384
+ node,
251385
+ sourceCode,
251386
+ });
251387
+ return fixer.insertTextBeforeRange(range, text);
251258
251388
  },
251259
- fix: (fixer) => fixer.insertTextBeforeRange(range, ' public '),
251260
251389
  messageId: 'implicitPublic',
251261
251390
  node,
251262
251391
  });
@@ -255580,18 +255709,6 @@ const rule$a = createRule({
255580
255709
  name: 'short-tui-imports',
255581
255710
  });
255582
255711
 
255583
- function isFieldLikeMember(member) {
255584
- return (member.type === dist$4.AST_NODE_TYPES.PropertyDefinition ||
255585
- member.type === dist$4.AST_NODE_TYPES.TSAbstractPropertyDefinition);
255586
- }
255587
- function isAccessorMember(member) {
255588
- return (member.type === dist$4.AST_NODE_TYPES.MethodDefinition &&
255589
- (member.kind === 'get' || member.kind === 'set'));
255590
- }
255591
- function isRelevantSpacingClassMember(member) {
255592
- return isFieldLikeMember(member) || isAccessorMember(member);
255593
- }
255594
-
255595
255712
  const rule$9 = createRule({
255596
255713
  create(context) {
255597
255714
  const sourceCode = context.sourceCode;
@@ -256958,8 +257075,9 @@ const plugin = {
256958
257075
  },
256959
257076
  rules: {
256960
257077
  'array-as-const': rule$5,
256961
- 'at-compat': rule$X,
256962
- 'attrs-newline': rule$W,
257078
+ 'at-compat': rule$Y,
257079
+ 'attrs-newline': rule$X,
257080
+ 'class-accessibility-spacing': rule$W,
256963
257081
  'class-property-naming': rule$4,
256964
257082
  'decorator-key-sort': rule$V,
256965
257083
  'element-newline': rule$U,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ui/eslint-plugin-experience-next",
3
- "version": "0.527.0",
3
+ "version": "0.528.0",
4
4
  "description": "An ESLint plugin to enforce a consistent code styles across taiga-ui projects",
5
5
  "homepage": "https://github.com/taiga-family/toolkit#readme",
6
6
  "bugs": {
@@ -0,0 +1,4 @@
1
+ export declare const rule: import("@typescript-eslint/utils/ts-eslint").RuleModule<"classAccessibilitySpacing", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
2
+ name: string;
3
+ };
4
+ export default rule;
@@ -1,4 +1,5 @@
1
- export declare const rule: import("@typescript-eslint/utils/ts-eslint").RuleModule<"implicitPublic", readonly unknown[], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
1
+ import { type TSESLint } from '@typescript-eslint/utils';
2
+ export declare const rule: TSESLint.RuleModule<"implicitPublic", readonly unknown[], unknown, TSESLint.RuleListener> & {
2
3
  name: string;
3
4
  };
4
5
  export default rule;
@@ -1,6 +1,14 @@
1
1
  import { type TSESTree } from '@typescript-eslint/utils';
2
2
  export { getLeadingIndentation, getLineBreak, getLineStartOffset, hasBlankLine, hasCommentLikeText, isSingleLineNode, } from './spacing';
3
3
  export type FieldLikeMember = TSESTree.PropertyDefinition | TSESTree.TSAbstractPropertyDefinition;
4
+ export type AccessibilityGroup = 'private' | 'protected' | 'public';
5
+ export type AccessibilityClassMember = TSESTree.AccessorProperty | TSESTree.MethodDefinition | TSESTree.PropertyDefinition | TSESTree.TSAbstractAccessorProperty | TSESTree.TSAbstractMethodDefinition | TSESTree.TSAbstractPropertyDefinition | TSESTree.TSIndexSignature;
6
+ export type EcmascriptPrivateClassMember = Exclude<AccessibilityClassMember, TSESTree.TSIndexSignature> & {
7
+ readonly key: TSESTree.PrivateIdentifier;
8
+ };
4
9
  export declare function isFieldLikeMember(member: TSESTree.ClassElement): member is FieldLikeMember;
5
10
  export declare function isAccessorMember(member: TSESTree.ClassElement): member is TSESTree.MethodDefinition;
6
11
  export declare function isRelevantSpacingClassMember(member: TSESTree.ClassElement): member is FieldLikeMember | TSESTree.MethodDefinition;
12
+ export declare function isAccessibilityClassMember(member: TSESTree.ClassElement): member is AccessibilityClassMember;
13
+ export declare function isEcmascriptPrivateClassMember(member: TSESTree.ClassElement): member is EcmascriptPrivateClassMember;
14
+ export declare function getAccessibilityGroup(member: AccessibilityClassMember): AccessibilityGroup;
@@ -3,6 +3,7 @@ export declare function isSingleLineNode(node: TSESTree.Node): boolean;
3
3
  export declare function isLineBreakCharacter(char: string | undefined): boolean;
4
4
  export declare function hasCommentLikeText(text: string): boolean;
5
5
  export declare function hasBlankLine(text: string): boolean;
6
+ export declare function hasBlankLineBetweenNodes(text: string): boolean;
6
7
  export declare function getLineBreak(text: string): string;
7
8
  export declare function hasLineBreak(text: string): boolean;
8
9
  export declare function getLeadingIndentation(text: string): string;