@tianjos/eslint-plugin-elegant 0.3.2 → 0.5.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.
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("@typescript-eslint/utils");
4
+ const createRule_1 = require("../utils/createRule");
5
+ const DEFAULT_MAX = 4;
6
+ /**
7
+ * Ambient types every codebase reaches for. Instantiating a `Date` or a `Map` is
8
+ * not a design decision worth budgeting, so they never count as collaborators.
9
+ */
10
+ const BUILT_INS = [
11
+ 'Array',
12
+ 'Date',
13
+ 'Error',
14
+ 'Map',
15
+ 'Promise',
16
+ 'RegExp',
17
+ 'Set',
18
+ 'URL',
19
+ 'WeakMap',
20
+ 'WeakSet',
21
+ ];
22
+ const injected = (body, sourceCode) => {
23
+ const constructor = body.body.find((member) => member.type === utils_1.AST_NODE_TYPES.MethodDefinition &&
24
+ member.kind === 'constructor');
25
+ return (constructor?.value.params ?? []).flatMap((param) => {
26
+ const target = param.type === utils_1.AST_NODE_TYPES.TSParameterProperty
27
+ ? param.parameter
28
+ : param;
29
+ const annotation = target.typeAnnotation?.typeAnnotation;
30
+ return annotation?.type === utils_1.AST_NODE_TYPES.TSTypeReference
31
+ ? [
32
+ {
33
+ key: sourceCode.getText(annotation),
34
+ root: sourceCode.getText(annotation.typeName),
35
+ },
36
+ ]
37
+ : [];
38
+ });
39
+ };
40
+ const instantiated = (node, sourceCode) => {
41
+ const root = sourceCode.getText(node.callee);
42
+ return {
43
+ key: `${root}${node.typeArguments === undefined
44
+ ? ''
45
+ : sourceCode.getText(node.typeArguments)}`,
46
+ root,
47
+ };
48
+ };
49
+ exports.default = (0, createRule_1.createRule)({
50
+ name: 'max-class-dependencies',
51
+ meta: {
52
+ type: 'suggestion',
53
+ docs: {
54
+ description: 'Enforce a maximum number of distinct collaborators a class depends on.',
55
+ },
56
+ messages: {
57
+ tooManyDependencies: "Class '{{name}}' depends on {{count}} types (max {{max}}): {{names}}. Consider extracting a collaborator.",
58
+ },
59
+ schema: [
60
+ {
61
+ type: 'object',
62
+ properties: {
63
+ max: { type: 'integer', minimum: 1 },
64
+ ignore: { type: 'array', items: { type: 'string' } },
65
+ },
66
+ additionalProperties: false,
67
+ },
68
+ ],
69
+ },
70
+ defaultOptions: [{ max: DEFAULT_MAX, ignore: [] }],
71
+ create(context, [{ max, ignore }]) {
72
+ const sourceCode = context.sourceCode;
73
+ const ignored = new Set([...BUILT_INS, ...ignore]);
74
+ const scopes = [];
75
+ const record = (scope, dependency) => {
76
+ if (scope !== undefined && !ignored.has(dependency.root)) {
77
+ scope.add(dependency.key);
78
+ }
79
+ };
80
+ return {
81
+ ClassBody(node) {
82
+ const scope = new Set();
83
+ scopes.push(scope);
84
+ for (const dependency of injected(node, sourceCode)) {
85
+ record(scope, dependency);
86
+ }
87
+ },
88
+ NewExpression(node) {
89
+ if (node.parent.type === utils_1.AST_NODE_TYPES.ThrowStatement) {
90
+ return;
91
+ }
92
+ record(scopes[scopes.length - 1], instantiated(node, sourceCode));
93
+ },
94
+ 'ClassBody:exit'(node) {
95
+ const dependencies = scopes.pop() ?? new Set();
96
+ if (dependencies.size <= max) {
97
+ return;
98
+ }
99
+ context.report({
100
+ node: node.parent.id ?? node,
101
+ messageId: 'tooManyDependencies',
102
+ data: {
103
+ name: node.parent.id?.name ?? '(anonymous)',
104
+ count: dependencies.size,
105
+ max,
106
+ names: [...dependencies].join(', '),
107
+ },
108
+ });
109
+ },
110
+ };
111
+ },
112
+ });
@@ -0,0 +1,9 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ type Options = [{
3
+ max: number;
4
+ ignoreDecorated: boolean;
5
+ }];
6
+ declare const _default: TSESLint.RuleModule<"tooManyFields", Options, unknown, TSESLint.RuleListener> & {
7
+ name: string;
8
+ };
9
+ export default _default;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("@typescript-eslint/utils");
4
+ const createRule_1 = require("../utils/createRule");
5
+ const DEFAULT_MAX = 5;
6
+ const isField = (member) => member.type === utils_1.AST_NODE_TYPES.PropertyDefinition ||
7
+ member.type === utils_1.AST_NODE_TYPES.TSAbstractPropertyDefinition ||
8
+ member.type === utils_1.AST_NODE_TYPES.AccessorProperty;
9
+ const named = (key, sourceCode) => key.type === utils_1.AST_NODE_TYPES.Identifier ? key.name : sourceCode.getText(key);
10
+ /** Every property declared in the class body. */
11
+ const declared = (member, sourceCode) => isField(member) && !member.static ? [named(member.key, sourceCode)] : [];
12
+ /**
13
+ * The same, minus the decorated ones: `@Column`, `@IsString` and friends map a
14
+ * field to a table or a payload, which is framework shape rather than state the
15
+ * class chose to carry.
16
+ */
17
+ const declaredUndecorated = (member, sourceCode) => isField(member) && member.decorators.length > 0
18
+ ? []
19
+ : declared(member, sourceCode);
20
+ /**
21
+ * Constructor parameter properties. `ignoreDecorated` deliberately does not
22
+ * reach them: a decorator on a parameter is injection (`@Inject(TOKEN)`), so the
23
+ * field is a genuine collaborator and has to stay inside the budget.
24
+ */
25
+ const promoted = (member, sourceCode) => {
26
+ if (member.type !== utils_1.AST_NODE_TYPES.MethodDefinition ||
27
+ member.kind !== 'constructor') {
28
+ return [];
29
+ }
30
+ return member.value.params.flatMap((param) => param.type === utils_1.AST_NODE_TYPES.TSParameterProperty
31
+ ? [named(param.parameter, sourceCode)]
32
+ : []);
33
+ };
34
+ exports.default = (0, createRule_1.createRule)({
35
+ name: 'max-class-fields',
36
+ meta: {
37
+ type: 'suggestion',
38
+ docs: {
39
+ description: 'Enforce a maximum number of instance fields per class to keep objects from becoming data bags.',
40
+ },
41
+ messages: {
42
+ tooManyFields: "Class '{{name}}' holds {{count}} fields (max {{max}}): {{names}}. Consider grouping related fields into a value object.",
43
+ },
44
+ schema: [
45
+ {
46
+ type: 'object',
47
+ properties: {
48
+ max: { type: 'integer', minimum: 1 },
49
+ ignoreDecorated: { type: 'boolean' },
50
+ },
51
+ additionalProperties: false,
52
+ },
53
+ ],
54
+ },
55
+ defaultOptions: [{ max: DEFAULT_MAX, ignoreDecorated: true }],
56
+ create(context, [{ max, ignoreDecorated }]) {
57
+ const sourceCode = context.sourceCode;
58
+ const fieldsOf = ignoreDecorated ? declaredUndecorated : declared;
59
+ return {
60
+ ClassBody(node) {
61
+ const fields = node.body.flatMap((member) => [
62
+ ...fieldsOf(member, sourceCode),
63
+ ...promoted(member, sourceCode),
64
+ ]);
65
+ if (fields.length <= max) {
66
+ return;
67
+ }
68
+ context.report({
69
+ node: node.parent.id ?? node,
70
+ messageId: 'tooManyFields',
71
+ data: {
72
+ name: node.parent.id?.name ?? '(anonymous)',
73
+ count: fields.length,
74
+ max,
75
+ names: fields.join(', '),
76
+ },
77
+ });
78
+ },
79
+ };
80
+ },
81
+ });
@@ -32,10 +32,9 @@ exports.default = (0, createRule_1.createRule)({
32
32
  if (methods.length <= max) {
33
33
  return;
34
34
  }
35
- const classNode = node.parent;
36
- const name = classNode.id?.name ?? '(anonymous)';
35
+ const name = node.parent.id?.name ?? '(anonymous)';
37
36
  context.report({
38
- node: classNode.id ?? node,
37
+ node: node.parent.id ?? node,
39
38
  messageId: 'tooManyMethods',
40
39
  data: { name, count: methods.length, max },
41
40
  });
@@ -0,0 +1,7 @@
1
+ type Options = [{
2
+ max: number;
3
+ }];
4
+ declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"tooManyReturns", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
5
+ name: string;
6
+ };
7
+ export default _default;
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("@typescript-eslint/utils");
4
+ const createRule_1 = require("../utils/createRule");
5
+ const DEFAULT_MAX = 3;
6
+ /**
7
+ * The name to report a function by. Declarations carry their own; the rest
8
+ * borrow it from whatever binds them, so a method and an arrow assigned to a
9
+ * const are named rather than reported as anonymous.
10
+ */
11
+ const nameOf = (node) => {
12
+ if (node.id !== null) {
13
+ return node.id.name;
14
+ }
15
+ if ((node.parent.type === utils_1.AST_NODE_TYPES.MethodDefinition ||
16
+ node.parent.type === utils_1.AST_NODE_TYPES.PropertyDefinition ||
17
+ node.parent.type === utils_1.AST_NODE_TYPES.Property) &&
18
+ !node.parent.computed &&
19
+ node.parent.key.type === utils_1.AST_NODE_TYPES.Identifier) {
20
+ return node.parent.key.name;
21
+ }
22
+ if (node.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
23
+ node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
24
+ return node.parent.id.name;
25
+ }
26
+ return '(anonymous)';
27
+ };
28
+ exports.default = (0, createRule_1.createRule)({
29
+ name: 'max-returns',
30
+ meta: {
31
+ type: 'suggestion',
32
+ docs: {
33
+ description: 'Enforce a maximum number of return statements per function.',
34
+ },
35
+ messages: {
36
+ tooManyReturns: "Function '{{name}}' returns from {{count}} places (max {{max}}). Consider collapsing the branches or extracting them into named functions.",
37
+ },
38
+ schema: [
39
+ {
40
+ type: 'object',
41
+ properties: {
42
+ max: { type: 'integer', minimum: 1 },
43
+ },
44
+ additionalProperties: false,
45
+ },
46
+ ],
47
+ },
48
+ defaultOptions: [{ max: DEFAULT_MAX }],
49
+ create(context, [{ max }]) {
50
+ const sourceCode = context.sourceCode;
51
+ const scopes = [];
52
+ const enter = (node) => {
53
+ scopes.push({ node, count: 0 });
54
+ };
55
+ const leave = () => {
56
+ const scope = scopes.pop();
57
+ if (scope === undefined || scope.count <= max) {
58
+ return;
59
+ }
60
+ const signature = scope.node.id ?? sourceCode.getFirstToken(scope.node);
61
+ context.report({
62
+ loc: (signature ?? scope.node).loc,
63
+ messageId: 'tooManyReturns',
64
+ data: {
65
+ name: nameOf(scope.node),
66
+ count: scope.count,
67
+ max,
68
+ },
69
+ });
70
+ };
71
+ return {
72
+ ArrowFunctionExpression: enter,
73
+ 'ArrowFunctionExpression:exit': leave,
74
+ FunctionDeclaration: enter,
75
+ 'FunctionDeclaration:exit': leave,
76
+ FunctionExpression: enter,
77
+ 'FunctionExpression:exit': leave,
78
+ ReturnStatement() {
79
+ const scope = scopes[scopes.length - 1];
80
+ if (scope !== undefined) {
81
+ scope.count += 1;
82
+ }
83
+ },
84
+ };
85
+ },
86
+ });
@@ -0,0 +1,7 @@
1
+ type Options = [{
2
+ minMembers: number;
3
+ }];
4
+ declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"anonymousParamType", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
5
+ name: string;
6
+ };
7
+ export default _default;
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("@typescript-eslint/utils");
4
+ const createRule_1 = require("../utils/createRule");
5
+ const DEFAULT_MIN_MEMBERS = 2;
6
+ const bindingOf = (param) => {
7
+ if (param.type === utils_1.AST_NODE_TYPES.TSParameterProperty) {
8
+ return bindingOf(param.parameter);
9
+ }
10
+ const target = param.type === utils_1.AST_NODE_TYPES.AssignmentPattern ? param.left : param;
11
+ switch (target.type) {
12
+ case utils_1.AST_NODE_TYPES.ArrayPattern:
13
+ case utils_1.AST_NODE_TYPES.Identifier:
14
+ case utils_1.AST_NODE_TYPES.ObjectPattern:
15
+ case utils_1.AST_NODE_TYPES.RestElement:
16
+ return target;
17
+ default:
18
+ return undefined;
19
+ }
20
+ };
21
+ const nameOf = (binding) => binding.type === utils_1.AST_NODE_TYPES.Identifier ? binding.name : '(destructured)';
22
+ /** The type nodes a shape can hide inside, one level down. */
23
+ const reachableFrom = (node) => {
24
+ switch (node.type) {
25
+ case utils_1.AST_NODE_TYPES.TSTypeReference:
26
+ return node.typeArguments?.params ?? [];
27
+ case utils_1.AST_NODE_TYPES.TSUnionType:
28
+ case utils_1.AST_NODE_TYPES.TSIntersectionType:
29
+ return node.types;
30
+ case utils_1.AST_NODE_TYPES.TSArrayType:
31
+ return [node.elementType];
32
+ default:
33
+ return [];
34
+ }
35
+ };
36
+ /**
37
+ * The first anonymous shape of at least `minMembers` properties reachable from
38
+ * a type annotation. Shapes hide behind generic arguments as readily as they
39
+ * sit in the open — `Array<{ day; count }>` is as unnamed as `{ day; count }`.
40
+ */
41
+ const shapeIn = (node, minMembers) => {
42
+ if (node.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
43
+ return node.members.length >= minMembers ? node : undefined;
44
+ }
45
+ for (const inner of reachableFrom(node)) {
46
+ const shape = shapeIn(inner, minMembers);
47
+ if (shape !== undefined) {
48
+ return shape;
49
+ }
50
+ }
51
+ return undefined;
52
+ };
53
+ /**
54
+ * Whether the signature is a function handed straight to a call. Such a
55
+ * parameter annotates whatever the callee yields — often an untyped response
56
+ * body — so an inline shape is the only way to type it at all.
57
+ */
58
+ const isInlineCallback = (node) => (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
59
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression) &&
60
+ node.parent.type === utils_1.AST_NODE_TYPES.CallExpression;
61
+ exports.default = (0, createRule_1.createRule)({
62
+ name: 'no-anonymous-param-type',
63
+ meta: {
64
+ type: 'suggestion',
65
+ docs: {
66
+ description: 'Disallow parameters typed as an anonymous shape. Name the shape so it can carry behaviour instead of being a bag.',
67
+ },
68
+ messages: {
69
+ anonymousParamType: "Parameter '{{name}}' is typed as an anonymous shape of {{count}} properties. Name the shape so it can carry behaviour instead of being a bag.",
70
+ },
71
+ schema: [
72
+ {
73
+ type: 'object',
74
+ properties: {
75
+ minMembers: { type: 'integer', minimum: 1 },
76
+ },
77
+ additionalProperties: false,
78
+ },
79
+ ],
80
+ },
81
+ defaultOptions: [{ minMembers: DEFAULT_MIN_MEMBERS }],
82
+ create(context, [{ minMembers }]) {
83
+ const check = (node) => {
84
+ if (isInlineCallback(node)) {
85
+ return;
86
+ }
87
+ for (const param of node.params) {
88
+ const binding = bindingOf(param);
89
+ if (binding === undefined) {
90
+ continue;
91
+ }
92
+ const annotation = binding.typeAnnotation?.typeAnnotation;
93
+ if (annotation === undefined) {
94
+ continue;
95
+ }
96
+ const shape = shapeIn(annotation, minMembers);
97
+ if (shape === undefined) {
98
+ continue;
99
+ }
100
+ context.report({
101
+ node: param,
102
+ messageId: 'anonymousParamType',
103
+ data: {
104
+ name: nameOf(binding),
105
+ count: shape.members.length,
106
+ },
107
+ });
108
+ }
109
+ };
110
+ return {
111
+ ArrowFunctionExpression: check,
112
+ FunctionDeclaration: check,
113
+ FunctionExpression: check,
114
+ TSDeclareFunction: check,
115
+ TSEmptyBodyFunctionExpression: check,
116
+ TSFunctionType: check,
117
+ TSMethodSignature: check,
118
+ };
119
+ },
120
+ });
@@ -0,0 +1,7 @@
1
+ type Options = [{
2
+ allow: string[];
3
+ }];
4
+ declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"commentInBody", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
5
+ name: string;
6
+ };
7
+ export default _default;
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("@typescript-eslint/utils");
4
+ const createRule_1 = require("../utils/createRule");
5
+ /**
6
+ * Comments the toolchain reads rather than humans. Suppressing another rule or
7
+ * a type error inside a body is routine and says nothing about the code's shape,
8
+ * so these never count — the counterpart of qulice's `@checkstyle` exemption.
9
+ */
10
+ const DIRECTIVES = [
11
+ '@ts-',
12
+ '@vite-ignore',
13
+ 'c8 ignore',
14
+ 'eslint',
15
+ 'istanbul ignore',
16
+ 'prettier-ignore',
17
+ 'v8 ignore',
18
+ 'webpackChunkName',
19
+ ];
20
+ const directs = (comment, allow) => {
21
+ const text = comment.value.trim();
22
+ return [...DIRECTIVES, ...allow].some((prefix) => text.startsWith(prefix));
23
+ };
24
+ const encloses = (block, comment) => block.range[0] < comment.range[0] && comment.range[1] < block.range[1];
25
+ const span = (block) => block.range[1] - block.range[0];
26
+ /**
27
+ * The tightest block wrapping the comment, which is what decides whether it is
28
+ * merely annotating an empty one. An empty `catch` inside a busy function still
29
+ * earns its explanation, so the lookup cannot stop at the function body.
30
+ */
31
+ const innermost = (blocks, comment) => blocks
32
+ .filter((block) => encloses(block, comment))
33
+ .reduce((tightest, block) => tightest === undefined || span(block) < span(tightest)
34
+ ? block
35
+ : tightest, undefined);
36
+ exports.default = (0, createRule_1.createRule)({
37
+ name: 'no-comments-in-function-body',
38
+ meta: {
39
+ type: 'suggestion',
40
+ docs: {
41
+ description: 'Disallow comments inside function bodies, where they stand in for a name the code should carry itself.',
42
+ },
43
+ messages: {
44
+ commentInBody: 'A comment inside a function body signals code that needs a better name. Move it to a docblock above the function, or extract what it explains into a named function.',
45
+ },
46
+ schema: [
47
+ {
48
+ type: 'object',
49
+ properties: {
50
+ allow: { type: 'array', items: { type: 'string' } },
51
+ },
52
+ additionalProperties: false,
53
+ },
54
+ ],
55
+ },
56
+ defaultOptions: [{ allow: [] }],
57
+ create(context, [{ allow }]) {
58
+ const sourceCode = context.sourceCode;
59
+ const bodies = [];
60
+ const blocks = [];
61
+ const collect = (node) => {
62
+ if (node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
63
+ bodies.push(node.body);
64
+ }
65
+ };
66
+ return {
67
+ BlockStatement(node) {
68
+ blocks.push(node);
69
+ },
70
+ ArrowFunctionExpression: collect,
71
+ FunctionDeclaration: collect,
72
+ FunctionExpression: collect,
73
+ 'Program:exit'() {
74
+ for (const comment of sourceCode.getAllComments()) {
75
+ const inside = bodies.some((body) => encloses(body, comment));
76
+ const block = innermost(blocks, comment);
77
+ const explains = block === undefined || block.body.length === 0;
78
+ if (inside && !explains && !directs(comment, allow)) {
79
+ context.report({ loc: comment.loc, messageId: 'commentInBody' });
80
+ }
81
+ }
82
+ },
83
+ };
84
+ },
85
+ });
@@ -0,0 +1,4 @@
1
+ declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"elseAfterThrow", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
2
+ name: string;
3
+ };
4
+ export default _default;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("@typescript-eslint/utils");
4
+ const createRule_1 = require("../utils/createRule");
5
+ /**
6
+ * Whether the branch leaves through a `throw` no matter what. A bare `throw`
7
+ * qualifies, as does a block whose last statement is one. The check does not
8
+ * recurse: a block ending in a nested `if` may or may not throw, and `else`
9
+ * still carries information there.
10
+ */
11
+ const alwaysThrows = (branch) => {
12
+ if (branch.type === utils_1.AST_NODE_TYPES.ThrowStatement) {
13
+ return true;
14
+ }
15
+ if (branch.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
16
+ return false;
17
+ }
18
+ return branch.body.at(-1)?.type === utils_1.AST_NODE_TYPES.ThrowStatement;
19
+ };
20
+ exports.default = (0, createRule_1.createRule)({
21
+ name: 'no-else-after-throw',
22
+ meta: {
23
+ type: 'suggestion',
24
+ docs: {
25
+ description: 'Disallow an else branch when the then branch always throws.',
26
+ },
27
+ messages: {
28
+ elseAfterThrow: "The 'then' branch always throws, so 'else' adds nothing but nesting. Drop it and let the alternative sit at the outer level.",
29
+ },
30
+ schema: [],
31
+ },
32
+ defaultOptions: [],
33
+ create(context) {
34
+ const sourceCode = context.sourceCode;
35
+ return {
36
+ IfStatement(node) {
37
+ if (node.alternate === null || !alwaysThrows(node.consequent)) {
38
+ return;
39
+ }
40
+ const keyword = sourceCode.getTokenBefore(node.alternate);
41
+ context.report({
42
+ node: keyword ?? node.alternate,
43
+ messageId: 'elseAfterThrow',
44
+ });
45
+ },
46
+ };
47
+ },
48
+ });
@@ -0,0 +1,8 @@
1
+ type Options = [{
2
+ objects: string[];
3
+ methods: string[];
4
+ }];
5
+ declare const _default: import("@typescript-eslint/utils/ts-eslint").RuleModule<"interpolatedMessage", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
6
+ name: string;
7
+ };
8
+ export default _default;