@tianjos/eslint-plugin-elegant 0.1.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/LICENSE +21 -0
- package/README.md +139 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +36 -0
- package/dist/rules/max-class-methods.d.ts +7 -0
- package/dist/rules/max-class-methods.js +45 -0
- package/dist/rules/no-boolean-param.d.ts +4 -0
- package/dist/rules/no-boolean-param.js +58 -0
- package/dist/rules/no-null-return.d.ts +4 -0
- package/dist/rules/no-null-return.js +30 -0
- package/dist/rules/no-public-mutable-props.d.ts +4 -0
- package/dist/rules/no-public-mutable-props.js +60 -0
- package/dist/rules/no-type-assertion.d.ts +4 -0
- package/dist/rules/no-type-assertion.js +34 -0
- package/dist/utils/createRule.d.ts +8 -0
- package/dist/utils/createRule.js +9 -0
- package/package.json +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thiago
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# @tianjos/eslint-plugin-elegant
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@tianjos/eslint-plugin-elegant)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](https://github.com/tianjos/eslint-plugin-elegant/actions/workflows/ci.yml)
|
|
6
|
+
|
|
7
|
+
Opinionated ESLint rules for **elegant, behavior-rich TypeScript**. The plugin
|
|
8
|
+
pushes code toward intention-revealing functions, honest types, and
|
|
9
|
+
encapsulated domain models — the kind of constraints that pay off in NestJS
|
|
10
|
+
services and DDD-style codebases.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install --save-dev @tianjos/eslint-plugin-elegant
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add -D @tianjos/eslint-plugin-elegant
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
yarn add -D @tianjos/eslint-plugin-elegant
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Peer dependencies
|
|
27
|
+
|
|
28
|
+
This plugin does not bundle ESLint or the TypeScript toolchain. Install them
|
|
29
|
+
alongside it:
|
|
30
|
+
|
|
31
|
+
| Peer | Required version |
|
|
32
|
+
| ----------------------------- | ---------------- |
|
|
33
|
+
| `eslint` | `>=9` |
|
|
34
|
+
| `typescript` | `>=5` |
|
|
35
|
+
| `@typescript-eslint/parser` | `>=8` |
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install --save-dev eslint typescript @typescript-eslint/parser
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
This plugin targets **flat config** (`eslint.config.mjs`). The fastest way to
|
|
44
|
+
adopt it is to spread the `recommended` ruleset:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
// eslint.config.mjs
|
|
48
|
+
import parser from '@typescript-eslint/parser';
|
|
49
|
+
import elegant from '@tianjos/eslint-plugin-elegant';
|
|
50
|
+
|
|
51
|
+
export default [
|
|
52
|
+
{
|
|
53
|
+
files: ['src/**/*.ts'],
|
|
54
|
+
languageOptions: {
|
|
55
|
+
parser,
|
|
56
|
+
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
|
57
|
+
},
|
|
58
|
+
plugins: { elegant },
|
|
59
|
+
rules: {
|
|
60
|
+
...elegant.configs.recommended.rules,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
A complete, copy-pasteable example (including test-file overrides) lives in
|
|
67
|
+
[`eslint.config.example.mjs`](./eslint.config.example.mjs).
|
|
68
|
+
|
|
69
|
+
## Rules
|
|
70
|
+
|
|
71
|
+
The `recommended` config enables every custom rule plus the native
|
|
72
|
+
[`max-params`](https://eslint.org/docs/latest/rules/max-params) rule.
|
|
73
|
+
|
|
74
|
+
| Rule | Source | What it catches | `recommended` |
|
|
75
|
+
| -------------------------------------- | ------ | ------------------------------------------------------------------------------- | ------------- |
|
|
76
|
+
| `elegant/no-boolean-param` | custom | Boolean parameters (flag arguments) on functions, methods, and constructors | `error` |
|
|
77
|
+
| `elegant/max-class-methods` | custom | Classes with more methods than the configured `max` (constructors excluded) | `warn` (max 10) |
|
|
78
|
+
| `elegant/no-type-assertion` | custom | `value as T` and `<T>value` assertions (`as const` is allowed) | `error` |
|
|
79
|
+
| `elegant/no-null-return` | custom | `return null` statements | `error` |
|
|
80
|
+
| `elegant/no-public-mutable-props` | custom | Public, non-`readonly` class properties and public constructor parameter props | `error` |
|
|
81
|
+
| `max-params` | native | Functions declaring more than `max` parameters | `warn` (max 3) |
|
|
82
|
+
|
|
83
|
+
### Rule details
|
|
84
|
+
|
|
85
|
+
- **`no-boolean-param`** — a boolean argument almost always means the callee
|
|
86
|
+
does two things. Prefer two intention-revealing functions or an options
|
|
87
|
+
object. Flags both annotated (`flag: boolean`) and boolean-defaulted
|
|
88
|
+
(`flag = false`) parameters.
|
|
89
|
+
- **`max-class-methods`** — a proxy for the Single Responsibility Principle.
|
|
90
|
+
Constructors are not counted; getters and setters are.
|
|
91
|
+
- **`no-type-assertion`** — assertions silence the type checker. Reach for a
|
|
92
|
+
type guard, a generic, or a correctly typed value instead. `as const` is
|
|
93
|
+
permitted because it narrows rather than widens.
|
|
94
|
+
- **`no-null-return`** — keeps absence out of return values; model it with an
|
|
95
|
+
explicit domain type or throw.
|
|
96
|
+
- **`no-public-mutable-props`** — public state should be `readonly` so callers
|
|
97
|
+
cannot break an aggregate's invariants. `private`/`protected` members and
|
|
98
|
+
`readonly` members are allowed.
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
### Overriding thresholds
|
|
103
|
+
|
|
104
|
+
`max-class-methods` and the native `max-params` both take a `max` option:
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
rules: {
|
|
108
|
+
...elegant.configs.recommended.rules,
|
|
109
|
+
'elegant/max-class-methods': ['warn', { max: 15 }],
|
|
110
|
+
'max-params': ['warn', { max: 4 }],
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Relaxing rules in test files
|
|
115
|
+
|
|
116
|
+
Tests routinely use flag arguments and larger fixtures. Add a second config
|
|
117
|
+
block scoped to your spec globs:
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
{
|
|
121
|
+
files: ['**/*.spec.ts', '**/*.test.ts', '**/*.e2e-spec.ts'],
|
|
122
|
+
rules: {
|
|
123
|
+
'elegant/no-boolean-param': 'off',
|
|
124
|
+
'elegant/max-class-methods': 'off',
|
|
125
|
+
'max-params': 'off',
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Compatibility
|
|
131
|
+
|
|
132
|
+
The package ships a single CommonJS build that is consumable as both
|
|
133
|
+
`require('@tianjos/eslint-plugin-elegant')` and an ESM
|
|
134
|
+
`import elegant from '@tianjos/eslint-plugin-elegant'`. The exported object
|
|
135
|
+
exposes `{ meta, rules, configs }`.
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
[MIT](./LICENSE) © Thiago
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
declare const rules: {
|
|
3
|
+
'no-boolean-param': TSESLint.RuleModule<"booleanParam", [], unknown, TSESLint.RuleListener> & {
|
|
4
|
+
name: string;
|
|
5
|
+
};
|
|
6
|
+
'max-class-methods': TSESLint.RuleModule<"tooManyMethods", [{
|
|
7
|
+
max: number;
|
|
8
|
+
}], unknown, TSESLint.RuleListener> & {
|
|
9
|
+
name: string;
|
|
10
|
+
};
|
|
11
|
+
'no-type-assertion': TSESLint.RuleModule<"noAssertion", [], unknown, TSESLint.RuleListener> & {
|
|
12
|
+
name: string;
|
|
13
|
+
};
|
|
14
|
+
'no-null-return': TSESLint.RuleModule<"noNullReturn", [], unknown, TSESLint.RuleListener> & {
|
|
15
|
+
name: string;
|
|
16
|
+
};
|
|
17
|
+
'no-public-mutable-props': TSESLint.RuleModule<"mutableProp", [], unknown, TSESLint.RuleListener> & {
|
|
18
|
+
name: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
type Plugin = {
|
|
22
|
+
meta: {
|
|
23
|
+
name: string;
|
|
24
|
+
version: string;
|
|
25
|
+
};
|
|
26
|
+
rules: typeof rules;
|
|
27
|
+
configs: Record<string, TSESLint.FlatConfig.Config>;
|
|
28
|
+
/**
|
|
29
|
+
* Self-reference so the package resolves identically whether consumers reach
|
|
30
|
+
* it via `require('...')`, `require('...').default`, or an ESM
|
|
31
|
+
* `import elegant from '...'`. This sidesteps the CJS/ESM interop hazard.
|
|
32
|
+
*/
|
|
33
|
+
default?: Plugin;
|
|
34
|
+
};
|
|
35
|
+
declare const plugin: Plugin;
|
|
36
|
+
export = plugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
const max_class_methods_1 = __importDefault(require("./rules/max-class-methods"));
|
|
6
|
+
const no_boolean_param_1 = __importDefault(require("./rules/no-boolean-param"));
|
|
7
|
+
const no_null_return_1 = __importDefault(require("./rules/no-null-return"));
|
|
8
|
+
const no_public_mutable_props_1 = __importDefault(require("./rules/no-public-mutable-props"));
|
|
9
|
+
const no_type_assertion_1 = __importDefault(require("./rules/no-type-assertion"));
|
|
10
|
+
const { name, version } = require('../package.json');
|
|
11
|
+
const rules = {
|
|
12
|
+
'no-boolean-param': no_boolean_param_1.default,
|
|
13
|
+
'max-class-methods': max_class_methods_1.default,
|
|
14
|
+
'no-type-assertion': no_type_assertion_1.default,
|
|
15
|
+
'no-null-return': no_null_return_1.default,
|
|
16
|
+
'no-public-mutable-props': no_public_mutable_props_1.default,
|
|
17
|
+
};
|
|
18
|
+
const plugin = {
|
|
19
|
+
meta: { name, version },
|
|
20
|
+
rules,
|
|
21
|
+
configs: {},
|
|
22
|
+
};
|
|
23
|
+
plugin.configs.recommended = {
|
|
24
|
+
name: 'elegant/recommended',
|
|
25
|
+
plugins: { elegant: plugin },
|
|
26
|
+
rules: {
|
|
27
|
+
'elegant/no-boolean-param': 'error',
|
|
28
|
+
'elegant/max-class-methods': ['warn', { max: 10 }],
|
|
29
|
+
'elegant/no-type-assertion': 'error',
|
|
30
|
+
'elegant/no-null-return': 'error',
|
|
31
|
+
'elegant/no-public-mutable-props': 'error',
|
|
32
|
+
'max-params': ['warn', { max: 3 }],
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
plugin.default = plugin;
|
|
36
|
+
module.exports = plugin;
|
|
@@ -0,0 +1,45 @@
|
|
|
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 = 10;
|
|
6
|
+
exports.default = (0, createRule_1.createRule)({
|
|
7
|
+
name: 'max-class-methods',
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'suggestion',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Enforce a maximum number of methods per class to keep classes cohesive and single-purpose.',
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
tooManyMethods: "Class '{{name}}' has {{count}} methods (max {{max}}). Consider splitting its responsibilities.",
|
|
15
|
+
},
|
|
16
|
+
schema: [
|
|
17
|
+
{
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
max: { type: 'integer', minimum: 1 },
|
|
21
|
+
},
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
defaultOptions: [{ max: DEFAULT_MAX }],
|
|
27
|
+
create(context, [{ max }]) {
|
|
28
|
+
return {
|
|
29
|
+
ClassBody(node) {
|
|
30
|
+
const methods = node.body.filter((member) => member.type === utils_1.AST_NODE_TYPES.MethodDefinition &&
|
|
31
|
+
member.kind !== 'constructor');
|
|
32
|
+
if (methods.length <= max) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const classNode = node.parent;
|
|
36
|
+
const name = classNode.id?.name ?? '(anonymous)';
|
|
37
|
+
context.report({
|
|
38
|
+
node: classNode.id ?? node,
|
|
39
|
+
messageId: 'tooManyMethods',
|
|
40
|
+
data: { name, count: methods.length, max },
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
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 isBooleanAnnotation = (annotation) => annotation?.type === utils_1.AST_NODE_TYPES.TSBooleanKeyword;
|
|
6
|
+
const isBooleanLiteral = (node) => node.type === utils_1.AST_NODE_TYPES.Literal && typeof node.value === 'boolean';
|
|
7
|
+
exports.default = (0, createRule_1.createRule)({
|
|
8
|
+
name: 'no-boolean-param',
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'suggestion',
|
|
11
|
+
docs: {
|
|
12
|
+
description: 'Disallow boolean parameters, which are flag arguments signalling a function that does more than one thing.',
|
|
13
|
+
},
|
|
14
|
+
messages: {
|
|
15
|
+
booleanParam: "Boolean parameter '{{name}}' is a flag argument. Prefer two intention-revealing functions or an options object.",
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
},
|
|
19
|
+
defaultOptions: [],
|
|
20
|
+
create(context) {
|
|
21
|
+
const checkParam = (param) => {
|
|
22
|
+
const target = param.type === utils_1.AST_NODE_TYPES.TSParameterProperty
|
|
23
|
+
? param.parameter
|
|
24
|
+
: param;
|
|
25
|
+
let name;
|
|
26
|
+
let annotation;
|
|
27
|
+
let defaultsToBoolean = false;
|
|
28
|
+
if (target.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
29
|
+
name = target;
|
|
30
|
+
annotation = target.typeAnnotation?.typeAnnotation;
|
|
31
|
+
}
|
|
32
|
+
else if (target.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
|
|
33
|
+
target.left.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
34
|
+
name = target.left;
|
|
35
|
+
annotation = target.left.typeAnnotation?.typeAnnotation;
|
|
36
|
+
defaultsToBoolean = isBooleanLiteral(target.right);
|
|
37
|
+
}
|
|
38
|
+
if (!name) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (isBooleanAnnotation(annotation) || defaultsToBoolean) {
|
|
42
|
+
context.report({
|
|
43
|
+
node: name,
|
|
44
|
+
messageId: 'booleanParam',
|
|
45
|
+
data: { name: name.name },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const checkFunction = (node) => {
|
|
50
|
+
node.params.forEach(checkParam);
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
FunctionDeclaration: checkFunction,
|
|
54
|
+
FunctionExpression: checkFunction,
|
|
55
|
+
ArrowFunctionExpression: checkFunction,
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
exports.default = (0, createRule_1.createRule)({
|
|
6
|
+
name: 'no-null-return',
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'suggestion',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Disallow returning null. Model absence with an explicit type, an Optional/Maybe, or by throwing.',
|
|
11
|
+
},
|
|
12
|
+
messages: {
|
|
13
|
+
noNullReturn: 'Returning null leaks absence into callers. Return an explicit empty value, a domain type, or throw.',
|
|
14
|
+
},
|
|
15
|
+
schema: [],
|
|
16
|
+
},
|
|
17
|
+
defaultOptions: [],
|
|
18
|
+
create(context) {
|
|
19
|
+
return {
|
|
20
|
+
ReturnStatement(node) {
|
|
21
|
+
const { argument } = node;
|
|
22
|
+
if (argument?.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
23
|
+
argument.value === null &&
|
|
24
|
+
argument.raw === 'null') {
|
|
25
|
+
context.report({ node, messageId: 'noNullReturn' });
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
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 isHidden = (accessibility) => accessibility === 'private' || accessibility === 'protected';
|
|
6
|
+
const keyName = (key) => {
|
|
7
|
+
if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
8
|
+
return key.name;
|
|
9
|
+
}
|
|
10
|
+
if (key.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
11
|
+
return String(key.value);
|
|
12
|
+
}
|
|
13
|
+
return 'property';
|
|
14
|
+
};
|
|
15
|
+
exports.default = (0, createRule_1.createRule)({
|
|
16
|
+
name: 'no-public-mutable-props',
|
|
17
|
+
meta: {
|
|
18
|
+
type: 'suggestion',
|
|
19
|
+
docs: {
|
|
20
|
+
description: 'Disallow public mutable class properties. Public state should be readonly to protect invariants and preserve encapsulation.',
|
|
21
|
+
},
|
|
22
|
+
messages: {
|
|
23
|
+
mutableProp: "Public property '{{name}}' is mutable. Make it readonly or expose it through a method that protects the invariant.",
|
|
24
|
+
},
|
|
25
|
+
schema: [],
|
|
26
|
+
},
|
|
27
|
+
defaultOptions: [],
|
|
28
|
+
create(context) {
|
|
29
|
+
return {
|
|
30
|
+
PropertyDefinition(node) {
|
|
31
|
+
if (node.readonly || isHidden(node.accessibility)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
context.report({
|
|
35
|
+
node: node.key,
|
|
36
|
+
messageId: 'mutableProp',
|
|
37
|
+
data: { name: keyName(node.key) },
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
TSParameterProperty(node) {
|
|
41
|
+
// Only constructor parameter properties carry an accessibility modifier.
|
|
42
|
+
if (node.accessibility !== 'public' ||
|
|
43
|
+
node.readonly) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const target = node.parameter.type === utils_1.AST_NODE_TYPES.AssignmentPattern
|
|
47
|
+
? node.parameter.left
|
|
48
|
+
: node.parameter;
|
|
49
|
+
if (target.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
context.report({
|
|
53
|
+
node: target,
|
|
54
|
+
messageId: 'mutableProp',
|
|
55
|
+
data: { name: target.name },
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
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 isAsConst = (node) => node.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
6
|
+
node.typeAnnotation.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
7
|
+
node.typeAnnotation.typeName.name === 'const';
|
|
8
|
+
exports.default = (0, createRule_1.createRule)({
|
|
9
|
+
name: 'no-type-assertion',
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'suggestion',
|
|
12
|
+
docs: {
|
|
13
|
+
description: 'Disallow type assertions, which bypass the type checker. Prefer type guards, generics, or honest types. `as const` is allowed.',
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
noAssertion: 'Type assertions silence the type checker. Use a type guard, a generic, or a correctly typed value instead.',
|
|
17
|
+
},
|
|
18
|
+
schema: [],
|
|
19
|
+
},
|
|
20
|
+
defaultOptions: [],
|
|
21
|
+
create(context) {
|
|
22
|
+
return {
|
|
23
|
+
TSAsExpression(node) {
|
|
24
|
+
if (isAsConst(node)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
context.report({ node, messageId: 'noAssertion' });
|
|
28
|
+
},
|
|
29
|
+
TSTypeAssertion(node) {
|
|
30
|
+
context.report({ node, messageId: 'noAssertion' });
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
/**
|
|
3
|
+
* Factory for all rules in this plugin. Centralises the docs URL convention so
|
|
4
|
+
* every rule links back to its documentation by name.
|
|
5
|
+
*/
|
|
6
|
+
export declare const createRule: <Options extends readonly unknown[], MessageIds extends string>({ meta, name, ...rule }: Readonly<ESLintUtils.RuleWithMetaAndName<Options, MessageIds, unknown>>) => ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
|
|
7
|
+
name: string;
|
|
8
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRule = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
/**
|
|
6
|
+
* Factory for all rules in this plugin. Centralises the docs URL convention so
|
|
7
|
+
* every rule links back to its documentation by name.
|
|
8
|
+
*/
|
|
9
|
+
exports.createRule = utils_1.ESLintUtils.RuleCreator((name) => `https://github.com/tianjos/eslint-plugin-elegant/blob/main/docs/rules/${name}.md`);
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tianjos/eslint-plugin-elegant",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Opinionated ESLint rules for elegant, behavior-rich TypeScript: no flag arguments, no type assertions, no null returns, no public mutable state, and small focused classes.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"eslint",
|
|
7
|
+
"eslint-plugin",
|
|
8
|
+
"typescript",
|
|
9
|
+
"nestjs",
|
|
10
|
+
"code-quality",
|
|
11
|
+
"linting"
|
|
12
|
+
],
|
|
13
|
+
"author": "Thiago <thiago@bullcredtech.com>",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"homepage": "https://github.com/tianjos/eslint-plugin-elegant#readme",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/tianjos/eslint-plugin-elegant.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/tianjos/eslint-plugin-elegant/issues"
|
|
22
|
+
},
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"require": "./dist/index.js",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -p tsconfig.json",
|
|
44
|
+
"typecheck": "tsc -p tsconfig.test.json",
|
|
45
|
+
"test": "jest",
|
|
46
|
+
"release": "standard-version",
|
|
47
|
+
"prepublishOnly": "npm run build && npm test"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@typescript-eslint/utils": "^8.0.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@typescript-eslint/parser": ">=8",
|
|
54
|
+
"eslint": ">=9",
|
|
55
|
+
"typescript": ">=5"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/jest": "^29.5.12",
|
|
59
|
+
"@types/node": "^20.14.0",
|
|
60
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
61
|
+
"@typescript-eslint/rule-tester": "^8.0.0",
|
|
62
|
+
"@typescript-eslint/utils": "^8.0.0",
|
|
63
|
+
"eslint": "^9.0.0",
|
|
64
|
+
"jest": "^29.7.0",
|
|
65
|
+
"standard-version": "^9.5.0",
|
|
66
|
+
"ts-jest": "^29.2.0",
|
|
67
|
+
"typescript": "^5.6.0"
|
|
68
|
+
},
|
|
69
|
+
"publishConfig": {
|
|
70
|
+
"access": "public"
|
|
71
|
+
}
|
|
72
|
+
}
|