eslint-plugin-no-indexed-access-prop 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 +267 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +15 -0
- package/dist/rules/no-indexed-access-prop.d.ts +18 -0
- package/dist/rules/no-indexed-access-prop.js +220 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Valeri Vatchev
|
|
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,267 @@
|
|
|
1
|
+
# eslint-plugin-no-indexed-access-prop
|
|
2
|
+
|
|
3
|
+
ESLint- and Oxlint-compatible rule for forbidding TypeScript indexed access types such as `User['id']` or `T[K]`.
|
|
4
|
+
|
|
5
|
+
The package exports one rule:
|
|
6
|
+
|
|
7
|
+
- `no-indexed-access-prop`
|
|
8
|
+
|
|
9
|
+
## What it flags
|
|
10
|
+
|
|
11
|
+
Examples reported by default:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
type UserId = User['id'];
|
|
15
|
+
type Value<T, K extends keyof T> = T[K];
|
|
16
|
+
type UserValue = User['id' | 'name'];
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The rule can also be configured more narrowly:
|
|
20
|
+
|
|
21
|
+
- block all indexed access types
|
|
22
|
+
- block only literal property access such as `T['id']`
|
|
23
|
+
- block only selected literal property names such as `['id', 'name']`
|
|
24
|
+
|
|
25
|
+
## Suggestions
|
|
26
|
+
|
|
27
|
+
The rule provides safe editor suggestions when the replacement can be derived from local syntax alone.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
type UserId = { id: string }['id'];
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Suggested replacement:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
type UserId = string;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Suggestions are intentionally not emitted for cases that would require cross-file or type-aware resolution, such as:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
type UserId = User['id'];
|
|
45
|
+
type Value<T, K extends keyof T> = T[K];
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
### ESLint
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm install --save-dev eslint eslint-plugin-no-indexed-access-prop
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Oxlint
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install --save-dev oxlint eslint-plugin-no-indexed-access-prop
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
### ESLint flat config
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
import noIndexedAccessProp from 'eslint-plugin-no-indexed-access-prop';
|
|
68
|
+
|
|
69
|
+
export default [
|
|
70
|
+
{
|
|
71
|
+
files: ['**/*.{ts,tsx}'],
|
|
72
|
+
plugins: {
|
|
73
|
+
noIndexedAccessProp,
|
|
74
|
+
},
|
|
75
|
+
rules: {
|
|
76
|
+
'noIndexedAccessProp/no-indexed-access-prop': 'error',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Oxlint
|
|
83
|
+
|
|
84
|
+
Using an explicit alias keeps the rule name stable and short:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"jsPlugins": [
|
|
89
|
+
{
|
|
90
|
+
"name": "no-indexed-access-prop",
|
|
91
|
+
"specifier": "eslint-plugin-no-indexed-access-prop"
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"rules": {
|
|
95
|
+
"no-indexed-access-prop/no-indexed-access-prop": "error"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Options
|
|
101
|
+
|
|
102
|
+
The rule accepts a single options object.
|
|
103
|
+
|
|
104
|
+
### `mode`
|
|
105
|
+
|
|
106
|
+
Controls which indexed access forms are reported.
|
|
107
|
+
|
|
108
|
+
- `"all"` (default): report every indexed access type
|
|
109
|
+
- `"literal-only"`: report only string-literal access such as `T['id']` or `T['id' | 'name']`
|
|
110
|
+
- `"configured-only"`: report only configured literal property names
|
|
111
|
+
|
|
112
|
+
### `allowGenericIndex`
|
|
113
|
+
|
|
114
|
+
Only meaningful in `mode: "all"`.
|
|
115
|
+
|
|
116
|
+
When `true`, generic or non-literal indexed access such as `T[K]` is allowed, while literal property access is still reported.
|
|
117
|
+
|
|
118
|
+
### `allowUnionLiteralIndex`
|
|
119
|
+
|
|
120
|
+
When `true`, union-literal access such as `T['id' | 'name']` is allowed.
|
|
121
|
+
|
|
122
|
+
This exemption applies in every mode.
|
|
123
|
+
|
|
124
|
+
### `properties`
|
|
125
|
+
|
|
126
|
+
Required when `mode: "configured-only"`.
|
|
127
|
+
|
|
128
|
+
Specifies the blocked literal property names.
|
|
129
|
+
|
|
130
|
+
## Configuration examples
|
|
131
|
+
|
|
132
|
+
### Block everything
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"rules": {
|
|
137
|
+
"no-indexed-access-prop/no-indexed-access-prop": "error"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Block only literal property access
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"rules": {
|
|
147
|
+
"no-indexed-access-prop/no-indexed-access-prop": [
|
|
148
|
+
"error",
|
|
149
|
+
{ "mode": "literal-only" }
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Block only selected properties
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"rules": {
|
|
160
|
+
"no-indexed-access-prop/no-indexed-access-prop": [
|
|
161
|
+
"error",
|
|
162
|
+
{ "mode": "configured-only", "properties": ["id", "name"] }
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Allow `T[K]` but still block literal property access
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"rules": {
|
|
173
|
+
"no-indexed-access-prop/no-indexed-access-prop": [
|
|
174
|
+
"error",
|
|
175
|
+
{ "mode": "all", "allowGenericIndex": true }
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Allow union-literal access
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"rules": {
|
|
186
|
+
"no-indexed-access-prop/no-indexed-access-prop": [
|
|
187
|
+
"error",
|
|
188
|
+
{ "mode": "literal-only", "allowUnionLiteralIndex": true }
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Option migration
|
|
195
|
+
|
|
196
|
+
Older examples may show this shape:
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
{ "properties": ["id"] }
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
That shape has been replaced. Use:
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{ "mode": "configured-only", "properties": ["id"] }
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Development
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
npm install
|
|
212
|
+
npm test
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Publishing notes
|
|
216
|
+
|
|
217
|
+
Before publishing to npm, verify at least the following:
|
|
218
|
+
|
|
219
|
+
1. `package.json` has the final package name and version
|
|
220
|
+
2. any desired registry metadata such as `repository`, `bugs`, `homepage`, and `license` is set
|
|
221
|
+
3. `npm test` passes
|
|
222
|
+
4. `npm pack --dry-run` contains only the intended artifacts
|
|
223
|
+
|
|
224
|
+
### First publish from a personal npm account
|
|
225
|
+
|
|
226
|
+
npm trusted publishing cannot be the first publish for a brand new package. The package must already exist on npm before a trusted publisher can be attached to it.
|
|
227
|
+
|
|
228
|
+
This repository includes scripts that isolate the bootstrap publish from your global npm login by using a repo-local user config file, `.npmrc.publish`, via npm's `--userconfig` support.
|
|
229
|
+
|
|
230
|
+
Bootstrap flow:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
npm run publish:login
|
|
234
|
+
npm run publish:whoami
|
|
235
|
+
npm test
|
|
236
|
+
npm run publish:bootstrap:dry-run
|
|
237
|
+
npm run publish:bootstrap
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
What this does:
|
|
241
|
+
|
|
242
|
+
- `publish:login` logs into the public npm registry using `./.npmrc.publish` instead of your global `~/.npmrc`
|
|
243
|
+
- `publish:whoami` confirms the active npm identity from that local config
|
|
244
|
+
- `publish:bootstrap` performs the one-time initial publish from your personal account
|
|
245
|
+
|
|
246
|
+
The `.npmrc.publish` file is gitignored and can be deleted after the first publish if you do not want to keep the local credentials around.
|
|
247
|
+
|
|
248
|
+
### Trusted publishing with GitHub Actions
|
|
249
|
+
|
|
250
|
+
After the first publish succeeds, configure npm trusted publishing for future releases:
|
|
251
|
+
|
|
252
|
+
1. Open the package settings on npmjs.com
|
|
253
|
+
2. Open the `Trusted Publisher` section
|
|
254
|
+
3. Choose `GitHub Actions`
|
|
255
|
+
4. Configure:
|
|
256
|
+
- Organization or user: `ValTM`
|
|
257
|
+
- Repository: `eslint-plugin-no-indexed-access-prop`
|
|
258
|
+
- Workflow filename: `publish.yml`
|
|
259
|
+
|
|
260
|
+
After that, future releases can be published from GitHub Actions without a long-lived npm token.
|
|
261
|
+
|
|
262
|
+
Trusted publishing references:
|
|
263
|
+
|
|
264
|
+
- https://docs.npmjs.com/trusted-publishers
|
|
265
|
+
- https://docs.npmjs.com/generating-provenance-statements
|
|
266
|
+
- https://docs.npmjs.com/cli/v11/commands/npm-trust
|
|
267
|
+
- https://docs.github.com/en/actions/tutorials/publish-packages/publish-nodejs-packages
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { RULE_NAME, noIndexedAccessPropRule } from "./rules/no-indexed-access-prop.js";
|
|
2
|
+
export declare const PACKAGE_NAME = "eslint-plugin-no-indexed-access-prop";
|
|
3
|
+
export declare const PACKAGE_VERSION = "0.1.0";
|
|
4
|
+
export declare const rules: {
|
|
5
|
+
readonly "no-indexed-access-prop": import("@typescript-eslint/utils/ts-eslint").RuleModule<"replaceWithInlinePropertyType" | "replaceWithInlinePropertyTypeUnion" | "unexpectedIndexedAccess" | "unexpectedPropertyIndexedAccess", [import("./rules/no-indexed-access-prop.js").NoIndexedAccessPropOptions], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
6
|
+
};
|
|
7
|
+
declare const plugin: {
|
|
8
|
+
meta: {
|
|
9
|
+
name: string;
|
|
10
|
+
version: string;
|
|
11
|
+
};
|
|
12
|
+
rules: {
|
|
13
|
+
readonly "no-indexed-access-prop": import("@typescript-eslint/utils/ts-eslint").RuleModule<"replaceWithInlinePropertyType" | "replaceWithInlinePropertyTypeUnion" | "unexpectedIndexedAccess" | "unexpectedPropertyIndexedAccess", [import("./rules/no-indexed-access-prop.js").NoIndexedAccessPropOptions], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export { RULE_NAME, noIndexedAccessPropRule };
|
|
17
|
+
export default plugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { RULE_NAME, noIndexedAccessPropRule } from "./rules/no-indexed-access-prop.js";
|
|
2
|
+
export const PACKAGE_NAME = "eslint-plugin-no-indexed-access-prop";
|
|
3
|
+
export const PACKAGE_VERSION = "0.1.0";
|
|
4
|
+
export const rules = {
|
|
5
|
+
[RULE_NAME]: noIndexedAccessPropRule,
|
|
6
|
+
};
|
|
7
|
+
const plugin = {
|
|
8
|
+
meta: {
|
|
9
|
+
name: PACKAGE_NAME,
|
|
10
|
+
version: PACKAGE_VERSION,
|
|
11
|
+
},
|
|
12
|
+
rules,
|
|
13
|
+
};
|
|
14
|
+
export { RULE_NAME, noIndexedAccessPropRule };
|
|
15
|
+
export default plugin;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
+
export declare const RULE_NAME = "no-indexed-access-prop";
|
|
3
|
+
export type NoIndexedAccessPropOptions = {
|
|
4
|
+
readonly mode?: "all";
|
|
5
|
+
readonly allowGenericIndex?: boolean;
|
|
6
|
+
readonly allowUnionLiteralIndex?: boolean;
|
|
7
|
+
} | {
|
|
8
|
+
readonly mode: "literal-only";
|
|
9
|
+
readonly allowUnionLiteralIndex?: boolean;
|
|
10
|
+
} | {
|
|
11
|
+
readonly mode: "configured-only";
|
|
12
|
+
readonly properties: readonly string[];
|
|
13
|
+
readonly allowUnionLiteralIndex?: boolean;
|
|
14
|
+
};
|
|
15
|
+
type Options = [NoIndexedAccessPropOptions];
|
|
16
|
+
type MessageIds = "replaceWithInlinePropertyType" | "replaceWithInlinePropertyTypeUnion" | "unexpectedIndexedAccess" | "unexpectedPropertyIndexedAccess";
|
|
17
|
+
export declare const noIndexedAccessPropRule: ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
+
export const RULE_NAME = "no-indexed-access-prop";
|
|
3
|
+
const createRule = ESLintUtils.RuleCreator.withoutDocs;
|
|
4
|
+
function getMode(options) {
|
|
5
|
+
return options.mode ?? "all";
|
|
6
|
+
}
|
|
7
|
+
function getConfiguredProperties(options) {
|
|
8
|
+
if (options.mode !== "configured-only") {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
return new Set(options.properties);
|
|
12
|
+
}
|
|
13
|
+
function getStringLiteralPropertyName(indexType) {
|
|
14
|
+
if (indexType.type !== "TSLiteralType") {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const literal = indexType.literal;
|
|
18
|
+
if (literal.type === "Literal" && typeof literal.value === "string") {
|
|
19
|
+
return literal.value;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
function getLiteralIndexAccess(indexType) {
|
|
24
|
+
const propertyName = getStringLiteralPropertyName(indexType);
|
|
25
|
+
if (propertyName != null) {
|
|
26
|
+
return {
|
|
27
|
+
kind: "single-literal",
|
|
28
|
+
propertyNames: [propertyName],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (indexType.type !== "TSUnionType") {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const propertyNames = [];
|
|
35
|
+
for (const member of indexType.types) {
|
|
36
|
+
const memberPropertyName = getStringLiteralPropertyName(member);
|
|
37
|
+
if (memberPropertyName == null) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
propertyNames.push(memberPropertyName);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
kind: "union-literal",
|
|
44
|
+
propertyNames: [...new Set(propertyNames)],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function getPropertySignatureName(member) {
|
|
48
|
+
if (member.computed || member.optional) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (member.key.type === "Identifier") {
|
|
52
|
+
return member.key.name;
|
|
53
|
+
}
|
|
54
|
+
if (member.key.type === "Literal" && typeof member.key.value === "string") {
|
|
55
|
+
return member.key.value;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
function getInlinePropertyTypeText(objectType, propertyName, sourceCode) {
|
|
60
|
+
for (const member of objectType.members) {
|
|
61
|
+
if (member.type !== "TSPropertySignature") {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (getPropertySignatureName(member) !== propertyName) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (member.typeAnnotation == null) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return sourceCode.getText(member.typeAnnotation.typeAnnotation);
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
function getSuggestion(node, sourceCode) {
|
|
75
|
+
const objectType = node.objectType;
|
|
76
|
+
const literalIndexAccess = getLiteralIndexAccess(node.indexType);
|
|
77
|
+
if (objectType.type !== "TSTypeLiteral" || literalIndexAccess == null) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const propertyTypeTexts = [];
|
|
81
|
+
for (const propertyName of literalIndexAccess.propertyNames) {
|
|
82
|
+
const propertyTypeText = getInlinePropertyTypeText(objectType, propertyName, sourceCode);
|
|
83
|
+
if (propertyTypeText == null) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
propertyTypeTexts.push(propertyTypeText);
|
|
87
|
+
}
|
|
88
|
+
if (propertyTypeTexts.length === 1) {
|
|
89
|
+
return {
|
|
90
|
+
messageId: "replaceWithInlinePropertyType",
|
|
91
|
+
replacementText: propertyTypeTexts[0],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
messageId: "replaceWithInlinePropertyTypeUnion",
|
|
96
|
+
replacementText: propertyTypeTexts.map((propertyTypeText) => `(${propertyTypeText})`).join(" | "),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function getReportDescriptor(options, configuredProperties, literalIndexAccess) {
|
|
100
|
+
if (literalIndexAccess?.kind === "union-literal" && options.allowUnionLiteralIndex) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
switch (getMode(options)) {
|
|
104
|
+
case "literal-only":
|
|
105
|
+
if (literalIndexAccess == null) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
messageId: "unexpectedIndexedAccess",
|
|
110
|
+
};
|
|
111
|
+
case "configured-only": {
|
|
112
|
+
if (literalIndexAccess == null || configuredProperties == null) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const matchedProperties = literalIndexAccess.propertyNames.filter((property) => configuredProperties.has(property));
|
|
116
|
+
if (matchedProperties.length === 0) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
messageId: "unexpectedPropertyIndexedAccess",
|
|
121
|
+
data: {
|
|
122
|
+
properties: matchedProperties.map((property) => `'${property}'`).join(", "),
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
case "all":
|
|
127
|
+
if (literalIndexAccess == null && "allowGenericIndex" in options && options.allowGenericIndex) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
messageId: "unexpectedIndexedAccess",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
export const noIndexedAccessPropRule = createRule({
|
|
137
|
+
name: RULE_NAME,
|
|
138
|
+
meta: {
|
|
139
|
+
type: "problem",
|
|
140
|
+
hasSuggestions: true,
|
|
141
|
+
docs: {
|
|
142
|
+
description: "Disallow TypeScript indexed access types using an explicit enforcement mode and optional exemptions.",
|
|
143
|
+
},
|
|
144
|
+
schema: [
|
|
145
|
+
{
|
|
146
|
+
oneOf: [
|
|
147
|
+
{
|
|
148
|
+
type: "object",
|
|
149
|
+
additionalProperties: false,
|
|
150
|
+
properties: {
|
|
151
|
+
mode: { type: "string", enum: ["all"] },
|
|
152
|
+
allowGenericIndex: { type: "boolean" },
|
|
153
|
+
allowUnionLiteralIndex: { type: "boolean" },
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
type: "object",
|
|
158
|
+
additionalProperties: false,
|
|
159
|
+
properties: {
|
|
160
|
+
mode: { type: "string", enum: ["literal-only"] },
|
|
161
|
+
allowUnionLiteralIndex: { type: "boolean" },
|
|
162
|
+
},
|
|
163
|
+
required: ["mode"],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
type: "object",
|
|
167
|
+
additionalProperties: false,
|
|
168
|
+
properties: {
|
|
169
|
+
mode: { type: "string", enum: ["configured-only"] },
|
|
170
|
+
properties: {
|
|
171
|
+
type: "array",
|
|
172
|
+
items: {
|
|
173
|
+
type: "string",
|
|
174
|
+
},
|
|
175
|
+
minItems: 1,
|
|
176
|
+
uniqueItems: true,
|
|
177
|
+
},
|
|
178
|
+
allowUnionLiteralIndex: { type: "boolean" },
|
|
179
|
+
},
|
|
180
|
+
required: ["mode", "properties"],
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
messages: {
|
|
186
|
+
replaceWithInlinePropertyType: "Replace this indexed access with the inline property type.",
|
|
187
|
+
replaceWithInlinePropertyTypeUnion: "Replace this indexed access with the corresponding inline property type union.",
|
|
188
|
+
unexpectedIndexedAccess: "Do not use TypeScript indexed access types. Prefer a named alias or a direct property declaration.",
|
|
189
|
+
unexpectedPropertyIndexedAccess: "Do not use TypeScript indexed access types for blocked properties: {{ properties }}.",
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
defaultOptions: [{}],
|
|
193
|
+
create(context, [options]) {
|
|
194
|
+
const configuredProperties = getConfiguredProperties(options);
|
|
195
|
+
return {
|
|
196
|
+
TSIndexedAccessType(node) {
|
|
197
|
+
const literalIndexAccess = getLiteralIndexAccess(node.indexType);
|
|
198
|
+
const suggestion = getSuggestion(node, context.sourceCode);
|
|
199
|
+
const reportDescriptor = getReportDescriptor(options, configuredProperties, literalIndexAccess);
|
|
200
|
+
if (reportDescriptor == null) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
context.report({
|
|
204
|
+
node,
|
|
205
|
+
...reportDescriptor,
|
|
206
|
+
suggest: suggestion == null
|
|
207
|
+
? undefined
|
|
208
|
+
: [
|
|
209
|
+
{
|
|
210
|
+
messageId: suggestion.messageId,
|
|
211
|
+
fix(fixer) {
|
|
212
|
+
return fixer.replaceText(node, suggestion.replacementText);
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
});
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
},
|
|
220
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-plugin-no-indexed-access-prop",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ESLint and Oxlint plugin that forbids TypeScript indexed access property types.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"eslint",
|
|
7
|
+
"eslint-plugin",
|
|
8
|
+
"oxlint",
|
|
9
|
+
"typescript",
|
|
10
|
+
"indexed-access",
|
|
11
|
+
"typescript-eslint"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/ValTM/eslint-plugin-no-indexed-access-prop.git"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/ValTM/eslint-plugin-no-indexed-access-prop/issues"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/ValTM/eslint-plugin-no-indexed-access-prop#readme",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./rules/no-indexed-access-prop": {
|
|
32
|
+
"types": "./dist/rules/no-indexed-access-prop.d.ts",
|
|
33
|
+
"import": "./dist/rules/no-indexed-access-prop.js"
|
|
34
|
+
},
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc -p tsconfig.json",
|
|
42
|
+
"prepack": "npm run build",
|
|
43
|
+
"publish:login": "npm login --registry https://registry.npmjs.org --userconfig ./.npmrc.publish",
|
|
44
|
+
"publish:whoami": "npm --registry https://registry.npmjs.org --userconfig ./.npmrc.publish whoami",
|
|
45
|
+
"publish:bootstrap:dry-run": "npm --registry https://registry.npmjs.org --userconfig ./.npmrc.publish publish --dry-run",
|
|
46
|
+
"publish:bootstrap": "npm --registry https://registry.npmjs.org --userconfig ./.npmrc.publish publish",
|
|
47
|
+
"test:unit": "node --test tests/no-indexed-access-prop.test.mjs",
|
|
48
|
+
"test:oxlint": "node --test tests/oxlint-smoke.test.mjs",
|
|
49
|
+
"test": "npm run build && npm run test:unit && npm run test:oxlint"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"eslint": ">=9"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@typescript-eslint/parser": "^8.58.2",
|
|
56
|
+
"@typescript-eslint/rule-tester": "^8.58.2",
|
|
57
|
+
"@typescript-eslint/utils": "^8.58.2",
|
|
58
|
+
"eslint": "^10.2.0",
|
|
59
|
+
"oxlint": "^1.60.0",
|
|
60
|
+
"typescript": "^6.0.2"
|
|
61
|
+
}
|
|
62
|
+
}
|