@wsxjs/eslint-plugin-wsx 0.0.11 → 0.0.13
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 +217 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +46 -145
- package/dist/index.mjs +46 -145
- package/package.json +2 -2
- package/src/configs/recommended.ts +2 -0
- package/src/index.ts +2 -0
- package/src/rules/state-requires-initial-value.ts +136 -0
- package/src/types.ts +1 -0
package/README.md
CHANGED
|
@@ -2,6 +2,222 @@
|
|
|
2
2
|
|
|
3
3
|
ESLint plugin for WSX Framework - enforces best practices and framework-specific rules for Web Components with JSX.
|
|
4
4
|
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install --save-dev @wsxjs/eslint-plugin-wsx
|
|
9
|
+
# or
|
|
10
|
+
pnpm add -D @wsxjs/eslint-plugin-wsx
|
|
11
|
+
# or
|
|
12
|
+
yarn add -D @wsxjs/eslint-plugin-wsx
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
### ESLint 9+ (Flat Config)
|
|
18
|
+
|
|
19
|
+
Create or update `eslint.config.js` (or `eslint.config.mjs`):
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
import js from "@eslint/js";
|
|
23
|
+
import typescript from "@typescript-eslint/eslint-plugin";
|
|
24
|
+
import typescriptParser from "@typescript-eslint/parser";
|
|
25
|
+
import wsxPlugin from "@wsxjs/eslint-plugin-wsx";
|
|
26
|
+
import globals from "globals";
|
|
27
|
+
|
|
28
|
+
export default [
|
|
29
|
+
{
|
|
30
|
+
ignores: ["**/dist/", "**/node_modules/"],
|
|
31
|
+
},
|
|
32
|
+
js.configs.recommended,
|
|
33
|
+
{
|
|
34
|
+
files: ["**/*.{ts,tsx,js,jsx,wsx}"],
|
|
35
|
+
languageOptions: {
|
|
36
|
+
parser: typescriptParser,
|
|
37
|
+
parserOptions: {
|
|
38
|
+
ecmaVersion: "latest",
|
|
39
|
+
sourceType: "module",
|
|
40
|
+
ecmaFeatures: {
|
|
41
|
+
jsx: true,
|
|
42
|
+
},
|
|
43
|
+
jsxPragma: "h",
|
|
44
|
+
jsxFragmentName: "Fragment",
|
|
45
|
+
experimentalDecorators: true, // Required for @state decorator
|
|
46
|
+
extraFileExtensions: [".wsx"],
|
|
47
|
+
},
|
|
48
|
+
globals: {
|
|
49
|
+
...globals.browser,
|
|
50
|
+
...globals.es2021,
|
|
51
|
+
h: "readonly",
|
|
52
|
+
Fragment: "readonly",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
plugins: {
|
|
56
|
+
"@typescript-eslint": typescript,
|
|
57
|
+
wsx: wsxPlugin,
|
|
58
|
+
},
|
|
59
|
+
rules: {
|
|
60
|
+
...typescript.configs.recommended.rules,
|
|
61
|
+
"@typescript-eslint/no-explicit-any": "warn",
|
|
62
|
+
"@typescript-eslint/no-unused-vars": [
|
|
63
|
+
"error",
|
|
64
|
+
{
|
|
65
|
+
argsIgnorePattern: "^_",
|
|
66
|
+
varsIgnorePattern: "^_",
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
// WSX plugin rules
|
|
70
|
+
"wsx/render-method-required": "error",
|
|
71
|
+
"wsx/no-react-imports": "error",
|
|
72
|
+
"wsx/web-component-naming": "warn",
|
|
73
|
+
"wsx/state-requires-initial-value": "error",
|
|
74
|
+
"no-undef": "off", // TypeScript handles this
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Required Dependencies
|
|
81
|
+
|
|
82
|
+
Make sure you have these peer dependencies installed:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm install --save-dev eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser globals
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Important**: The `experimentalDecorators: true` option in `parserOptions` is **required** for the `wsx/state-requires-initial-value` rule to work correctly. Without it, ESLint cannot parse `@state` decorators and the rule will not detect violations.
|
|
89
|
+
|
|
90
|
+
## Rules
|
|
91
|
+
|
|
92
|
+
### `wsx/render-method-required`
|
|
93
|
+
|
|
94
|
+
**Error level**: `error`
|
|
95
|
+
|
|
96
|
+
Ensures WSX components implement the required `render()` method.
|
|
97
|
+
|
|
98
|
+
**Invalid**:
|
|
99
|
+
```typescript
|
|
100
|
+
class MyComponent extends WebComponent {
|
|
101
|
+
// Missing render() method
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Valid**:
|
|
106
|
+
```typescript
|
|
107
|
+
class MyComponent extends WebComponent {
|
|
108
|
+
render() {
|
|
109
|
+
return <div>Hello</div>;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### `wsx/no-react-imports`
|
|
115
|
+
|
|
116
|
+
**Error level**: `error`
|
|
117
|
+
|
|
118
|
+
Prevents React imports in WSX files. WSX uses its own JSX runtime.
|
|
119
|
+
|
|
120
|
+
**Invalid**:
|
|
121
|
+
```typescript
|
|
122
|
+
import React from "react"; // ❌
|
|
123
|
+
import { useState } from "react"; // ❌
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Valid**:
|
|
127
|
+
```typescript
|
|
128
|
+
import { WebComponent, state } from "@wsxjs/wsx-core"; // ✅
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### `wsx/web-component-naming`
|
|
132
|
+
|
|
133
|
+
**Error level**: `warn`
|
|
134
|
+
|
|
135
|
+
Enforces proper Web Component tag naming conventions (kebab-case with at least one hyphen).
|
|
136
|
+
|
|
137
|
+
**Invalid**:
|
|
138
|
+
```typescript
|
|
139
|
+
@autoRegister({ tagName: "mycomponent" }) // ❌ Missing hyphen
|
|
140
|
+
@autoRegister({ tagName: "MyComponent" }) // ❌ Not kebab-case
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Valid**:
|
|
144
|
+
```typescript
|
|
145
|
+
@autoRegister({ tagName: "my-component" }) // ✅
|
|
146
|
+
@autoRegister({ tagName: "wsx-button" }) // ✅
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### `wsx/state-requires-initial-value`
|
|
150
|
+
|
|
151
|
+
**Error level**: `error`
|
|
152
|
+
|
|
153
|
+
Requires `@state` decorator properties to have initial values. This is mandatory because:
|
|
154
|
+
1. The Babel plugin needs the initial value to determine if it's a primitive (uses `useState`) or object/array (uses `reactive`)
|
|
155
|
+
2. Without an initial value, the decorator cannot be properly transformed at compile time
|
|
156
|
+
3. The runtime fallback also requires an initial value to set up reactive state correctly
|
|
157
|
+
|
|
158
|
+
**Invalid**:
|
|
159
|
+
```typescript
|
|
160
|
+
class MyComponent extends WebComponent {
|
|
161
|
+
@state private maskStrokeColor?: string; // ❌ Missing initial value
|
|
162
|
+
@state private count; // ❌ Missing initial value
|
|
163
|
+
@state private user; // ❌ Missing initial value
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Valid**:
|
|
168
|
+
```typescript
|
|
169
|
+
class MyComponent extends WebComponent {
|
|
170
|
+
@state private maskStrokeColor = ""; // ✅ String
|
|
171
|
+
@state private count = 0; // ✅ Number
|
|
172
|
+
@state private enabled = false; // ✅ Boolean
|
|
173
|
+
@state private user = { name: "John" }; // ✅ Object
|
|
174
|
+
@state private items = []; // ✅ Array
|
|
175
|
+
@state private optional: string | undefined = undefined; // ✅ Optional with explicit undefined
|
|
176
|
+
@state private size?: number = 32; // ✅ Optional with default value
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Error Message Example**:
|
|
181
|
+
```
|
|
182
|
+
@state decorator on property 'size' requires an initial value.
|
|
183
|
+
|
|
184
|
+
Examples:
|
|
185
|
+
@state private size = ''; // for string
|
|
186
|
+
@state private size = 0; // for number
|
|
187
|
+
@state private size = {}; // for object
|
|
188
|
+
@state private size = []; // for array
|
|
189
|
+
@state private size = undefined; // for optional
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Configuration Options
|
|
193
|
+
|
|
194
|
+
### Disable Specific Rules
|
|
195
|
+
|
|
196
|
+
If you need to disable a specific rule:
|
|
197
|
+
|
|
198
|
+
```javascript
|
|
199
|
+
{
|
|
200
|
+
rules: {
|
|
201
|
+
"wsx/web-component-naming": "off", // Disable naming rule
|
|
202
|
+
"wsx/state-requires-initial-value": "warn", // Change to warning
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### File-Specific Rules
|
|
208
|
+
|
|
209
|
+
Apply rules only to `.wsx` files:
|
|
210
|
+
|
|
211
|
+
```javascript
|
|
212
|
+
{
|
|
213
|
+
files: ["**/*.wsx"],
|
|
214
|
+
rules: {
|
|
215
|
+
"wsx/render-method-required": "error",
|
|
216
|
+
"wsx/no-react-imports": "error",
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
5
221
|
## Testing Results
|
|
6
222
|
|
|
7
223
|
✅ **38 tests passed** with **100% code coverage**
|
|
@@ -38,6 +254,7 @@ This plugin now uses industry-standard testing practices:
|
|
|
38
254
|
- 🔍 **render-method-required**: Ensures WSX components implement the required `render()` method
|
|
39
255
|
- 🚫 **no-react-imports**: Prevents React imports in WSX files
|
|
40
256
|
- 🏷️ **web-component-naming**: Enforces proper Web Component tag naming conventions
|
|
257
|
+
- ✅ **state-requires-initial-value**: Requires `@state` decorator properties to have initial values
|
|
41
258
|
|
|
42
259
|
## Framework Integration
|
|
43
260
|
|
package/dist/index.d.mts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -184,110 +184,74 @@ var webComponentNaming = {
|
|
|
184
184
|
}
|
|
185
185
|
};
|
|
186
186
|
|
|
187
|
-
// src/rules/
|
|
188
|
-
var
|
|
187
|
+
// src/rules/state-requires-initial-value.ts
|
|
188
|
+
var stateRequiresInitialValue = {
|
|
189
189
|
meta: {
|
|
190
190
|
type: "problem",
|
|
191
191
|
docs: {
|
|
192
|
-
description: "
|
|
192
|
+
description: "require @state decorator properties to have initial values",
|
|
193
193
|
category: "Possible Errors",
|
|
194
194
|
recommended: true
|
|
195
195
|
},
|
|
196
196
|
messages: {
|
|
197
|
-
|
|
197
|
+
missingInitialValue: "@state decorator on property '{{propertyName}}' requires an initial value.\n\nExamples:\n @state private {{propertyName}} = ''; // for string\n @state private {{propertyName}} = 0; // for number\n @state private {{propertyName}} = {}; // for object\n @state private {{propertyName}} = []; // for array\n @state private {{propertyName}} = undefined; // for optional"
|
|
198
198
|
},
|
|
199
199
|
schema: []
|
|
200
200
|
},
|
|
201
201
|
create(context) {
|
|
202
|
-
const classObservedAttributes = /* @__PURE__ */ new Map();
|
|
203
202
|
const stateImports = /* @__PURE__ */ new Set();
|
|
204
203
|
return {
|
|
205
|
-
// Track imports
|
|
206
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
204
|
+
// Track imports to identify @state decorator
|
|
207
205
|
ImportDeclaration(node) {
|
|
208
|
-
if (node.source.type === "
|
|
209
|
-
node.specifiers.forEach((
|
|
210
|
-
if (
|
|
211
|
-
|
|
206
|
+
if (node.source.type === "Literal" && typeof node.source.value === "string" && node.source.value === "@wsxjs/wsx-core") {
|
|
207
|
+
node.specifiers.forEach((specifier) => {
|
|
208
|
+
if (specifier.type === "ImportSpecifier") {
|
|
209
|
+
if (specifier.imported.type === "Identifier" && specifier.imported.name === "state") {
|
|
210
|
+
const localName = specifier.local.type === "Identifier" ? specifier.local.name : null;
|
|
211
|
+
if (localName) {
|
|
212
|
+
stateImports.add(localName);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} else if (specifier.type === "ImportDefaultSpecifier") {
|
|
216
|
+
const localName = specifier.local.type === "Identifier" ? specifier.local.name : null;
|
|
212
217
|
if (localName) {
|
|
213
218
|
stateImports.add(localName);
|
|
214
219
|
}
|
|
220
|
+
} else if (specifier.type === "ImportNamespaceSpecifier") {
|
|
221
|
+
const localName = specifier.local.type === "Identifier" ? specifier.local.name : null;
|
|
222
|
+
if (localName) {
|
|
223
|
+
stateImports.add("state");
|
|
224
|
+
}
|
|
215
225
|
}
|
|
216
226
|
});
|
|
217
227
|
}
|
|
218
228
|
},
|
|
229
|
+
// Check class properties for @state decorator
|
|
230
|
+
// Support both ClassProperty (older) and PropertyDefinition (newer TypeScript ESLint)
|
|
219
231
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
232
|
+
"ClassProperty, ClassPrivateProperty, PropertyDefinition"(node) {
|
|
233
|
+
if (!node.decorators || node.decorators.length === 0) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const hasStateDecorator = node.decorators.some((decorator) => {
|
|
237
|
+
if (decorator.expression.type === "Identifier") {
|
|
238
|
+
return decorator.expression.name === "state" || stateImports.has(decorator.expression.name);
|
|
239
|
+
} else if (decorator.expression.type === "CallExpression") {
|
|
240
|
+
if (decorator.expression.callee.type === "Identifier") {
|
|
241
|
+
return decorator.expression.callee.name === "state" || stateImports.has(decorator.expression.callee.name);
|
|
228
242
|
}
|
|
229
|
-
} else if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
);
|
|
233
|
-
if (returnStmt?.argument?.type === "ArrayExpression") {
|
|
234
|
-
observedAttributes.push(
|
|
235
|
-
...returnStmt.argument.elements.filter((el) => el && el.type === "StringLiteral").map((el) => el.value)
|
|
236
|
-
);
|
|
243
|
+
} else if (decorator.expression.type === "MemberExpression") {
|
|
244
|
+
if (decorator.expression.property.type === "Identifier" && decorator.expression.property.name === "state") {
|
|
245
|
+
return true;
|
|
237
246
|
}
|
|
238
247
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
},
|
|
244
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
245
|
-
"ClassDeclaration:exit"(node) {
|
|
246
|
-
classObservedAttributes.delete(node);
|
|
247
|
-
},
|
|
248
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
249
|
-
Decorator(node) {
|
|
250
|
-
let isStateDecorator = false;
|
|
251
|
-
if (node.expression.type === "Identifier") {
|
|
252
|
-
const name = node.expression.name;
|
|
253
|
-
isStateDecorator = name === "state" || stateImports.has(name);
|
|
254
|
-
} else if (node.expression.type === "CallExpression") {
|
|
255
|
-
const callee = node.expression.callee;
|
|
256
|
-
if (callee.type === "Identifier") {
|
|
257
|
-
const name = callee.name;
|
|
258
|
-
isStateDecorator = name === "state" || stateImports.has(name);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
if (!isStateDecorator) {
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
let classNode = node.parent;
|
|
265
|
-
while (classNode && classNode.type !== "ClassDeclaration" && classNode.type !== "ClassExpression") {
|
|
266
|
-
classNode = classNode.parent;
|
|
267
|
-
}
|
|
268
|
-
if (!classNode) {
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
const propertyNode = node.parent;
|
|
272
|
-
if (!propertyNode || propertyNode.type !== "ClassProperty" && propertyNode.type !== "ClassPrivateProperty") {
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
const propertyName = propertyNode.key.type === "Identifier" ? propertyNode.key.name : null;
|
|
276
|
-
if (!propertyName) {
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
const observedAttributes = classObservedAttributes.get(classNode);
|
|
280
|
-
if (!observedAttributes || observedAttributes.length === 0) {
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
const propertyKebabCase = propertyName.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
284
|
-
const propertyLower = propertyName.toLowerCase();
|
|
285
|
-
if (observedAttributes.some(
|
|
286
|
-
(attr) => attr.toLowerCase() === propertyLower || attr.toLowerCase() === propertyKebabCase
|
|
287
|
-
)) {
|
|
248
|
+
return false;
|
|
249
|
+
});
|
|
250
|
+
if (hasStateDecorator && !node.value) {
|
|
251
|
+
const propertyName = node.key.type === "Identifier" ? node.key.name : node.key.type === "PrivateIdentifier" ? node.key.name : "unknown";
|
|
288
252
|
context.report({
|
|
289
253
|
node,
|
|
290
|
-
messageId: "
|
|
254
|
+
messageId: "missingInitialValue",
|
|
291
255
|
data: { propertyName }
|
|
292
256
|
});
|
|
293
257
|
}
|
|
@@ -296,69 +260,6 @@ var noStateOnHtmlAttributes = {
|
|
|
296
260
|
}
|
|
297
261
|
};
|
|
298
262
|
|
|
299
|
-
// src/rules/no-state-on-methods.ts
|
|
300
|
-
var noStateOnMethods = {
|
|
301
|
-
meta: {
|
|
302
|
-
type: "problem",
|
|
303
|
-
docs: {
|
|
304
|
-
description: "disallow @state decorator on methods",
|
|
305
|
-
category: "Possible Errors",
|
|
306
|
-
recommended: true
|
|
307
|
-
},
|
|
308
|
-
messages: {
|
|
309
|
-
stateOnMethod: "@state decorator cannot be used on methods. '{{methodName}}' is a method, not a property. @state can only be used on properties with primitive, object, or array values."
|
|
310
|
-
},
|
|
311
|
-
schema: []
|
|
312
|
-
},
|
|
313
|
-
create(context) {
|
|
314
|
-
const stateImports = /* @__PURE__ */ new Set();
|
|
315
|
-
return {
|
|
316
|
-
// Track imports
|
|
317
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
318
|
-
ImportDeclaration(node) {
|
|
319
|
-
if (node.source.type === "StringLiteral" && (node.source.value === "@wsxjs/wsx-core" || node.source.value.endsWith("/wsx-core"))) {
|
|
320
|
-
node.specifiers.forEach((spec) => {
|
|
321
|
-
if (spec.type === "ImportSpecifier" && spec.imported.type === "Identifier" && spec.imported.name === "state") {
|
|
322
|
-
const localName = spec.local.type === "Identifier" ? spec.local.name : null;
|
|
323
|
-
if (localName) {
|
|
324
|
-
stateImports.add(localName);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
},
|
|
330
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
331
|
-
Decorator(node) {
|
|
332
|
-
let isStateDecorator = false;
|
|
333
|
-
if (node.expression.type === "Identifier") {
|
|
334
|
-
const name = node.expression.name;
|
|
335
|
-
isStateDecorator = name === "state" || stateImports.has(name);
|
|
336
|
-
} else if (node.expression.type === "CallExpression") {
|
|
337
|
-
const callee = node.expression.callee;
|
|
338
|
-
if (callee.type === "Identifier") {
|
|
339
|
-
const name = callee.name;
|
|
340
|
-
isStateDecorator = name === "state" || stateImports.has(name);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
if (!isStateDecorator) {
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
const parent = node.parent;
|
|
347
|
-
if (parent && (parent.type === "ClassMethod" || parent.type === "ClassPrivateMethod")) {
|
|
348
|
-
const methodName = parent.key.type === "Identifier" ? parent.key.name : parent.key.type === "StringLiteral" ? parent.key.value : null;
|
|
349
|
-
if (methodName) {
|
|
350
|
-
context.report({
|
|
351
|
-
node,
|
|
352
|
-
messageId: "stateOnMethod",
|
|
353
|
-
data: { methodName }
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
};
|
|
361
|
-
|
|
362
263
|
// src/configs/recommended.ts
|
|
363
264
|
var recommendedConfig = {
|
|
364
265
|
parser: "@typescript-eslint/parser",
|
|
@@ -369,7 +270,9 @@ var recommendedConfig = {
|
|
|
369
270
|
jsx: true
|
|
370
271
|
},
|
|
371
272
|
jsxPragma: "h",
|
|
372
|
-
jsxFragmentName: "Fragment"
|
|
273
|
+
jsxFragmentName: "Fragment",
|
|
274
|
+
experimentalDecorators: true
|
|
275
|
+
// Required to parse @state decorators
|
|
373
276
|
},
|
|
374
277
|
plugins: ["wsx"],
|
|
375
278
|
rules: {
|
|
@@ -377,8 +280,7 @@ var recommendedConfig = {
|
|
|
377
280
|
"wsx/render-method-required": "error",
|
|
378
281
|
"wsx/no-react-imports": "error",
|
|
379
282
|
"wsx/web-component-naming": "warn",
|
|
380
|
-
"wsx/
|
|
381
|
-
"wsx/no-state-on-methods": "error",
|
|
283
|
+
"wsx/state-requires-initial-value": "error",
|
|
382
284
|
// TypeScript 规则(推荐)
|
|
383
285
|
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
|
384
286
|
"@typescript-eslint/no-explicit-any": "warn",
|
|
@@ -544,8 +446,7 @@ var plugin = {
|
|
|
544
446
|
"render-method-required": renderMethodRequired,
|
|
545
447
|
"no-react-imports": noReactImports,
|
|
546
448
|
"web-component-naming": webComponentNaming,
|
|
547
|
-
"
|
|
548
|
-
"no-state-on-methods": noStateOnMethods
|
|
449
|
+
"state-requires-initial-value": stateRequiresInitialValue
|
|
549
450
|
},
|
|
550
451
|
// 配置预设
|
|
551
452
|
configs: {
|
package/dist/index.mjs
CHANGED
|
@@ -155,110 +155,74 @@ var webComponentNaming = {
|
|
|
155
155
|
}
|
|
156
156
|
};
|
|
157
157
|
|
|
158
|
-
// src/rules/
|
|
159
|
-
var
|
|
158
|
+
// src/rules/state-requires-initial-value.ts
|
|
159
|
+
var stateRequiresInitialValue = {
|
|
160
160
|
meta: {
|
|
161
161
|
type: "problem",
|
|
162
162
|
docs: {
|
|
163
|
-
description: "
|
|
163
|
+
description: "require @state decorator properties to have initial values",
|
|
164
164
|
category: "Possible Errors",
|
|
165
165
|
recommended: true
|
|
166
166
|
},
|
|
167
167
|
messages: {
|
|
168
|
-
|
|
168
|
+
missingInitialValue: "@state decorator on property '{{propertyName}}' requires an initial value.\n\nExamples:\n @state private {{propertyName}} = ''; // for string\n @state private {{propertyName}} = 0; // for number\n @state private {{propertyName}} = {}; // for object\n @state private {{propertyName}} = []; // for array\n @state private {{propertyName}} = undefined; // for optional"
|
|
169
169
|
},
|
|
170
170
|
schema: []
|
|
171
171
|
},
|
|
172
172
|
create(context) {
|
|
173
|
-
const classObservedAttributes = /* @__PURE__ */ new Map();
|
|
174
173
|
const stateImports = /* @__PURE__ */ new Set();
|
|
175
174
|
return {
|
|
176
|
-
// Track imports
|
|
177
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
175
|
+
// Track imports to identify @state decorator
|
|
178
176
|
ImportDeclaration(node) {
|
|
179
|
-
if (node.source.type === "
|
|
180
|
-
node.specifiers.forEach((
|
|
181
|
-
if (
|
|
182
|
-
|
|
177
|
+
if (node.source.type === "Literal" && typeof node.source.value === "string" && node.source.value === "@wsxjs/wsx-core") {
|
|
178
|
+
node.specifiers.forEach((specifier) => {
|
|
179
|
+
if (specifier.type === "ImportSpecifier") {
|
|
180
|
+
if (specifier.imported.type === "Identifier" && specifier.imported.name === "state") {
|
|
181
|
+
const localName = specifier.local.type === "Identifier" ? specifier.local.name : null;
|
|
182
|
+
if (localName) {
|
|
183
|
+
stateImports.add(localName);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} else if (specifier.type === "ImportDefaultSpecifier") {
|
|
187
|
+
const localName = specifier.local.type === "Identifier" ? specifier.local.name : null;
|
|
183
188
|
if (localName) {
|
|
184
189
|
stateImports.add(localName);
|
|
185
190
|
}
|
|
191
|
+
} else if (specifier.type === "ImportNamespaceSpecifier") {
|
|
192
|
+
const localName = specifier.local.type === "Identifier" ? specifier.local.name : null;
|
|
193
|
+
if (localName) {
|
|
194
|
+
stateImports.add("state");
|
|
195
|
+
}
|
|
186
196
|
}
|
|
187
197
|
});
|
|
188
198
|
}
|
|
189
199
|
},
|
|
200
|
+
// Check class properties for @state decorator
|
|
201
|
+
// Support both ClassProperty (older) and PropertyDefinition (newer TypeScript ESLint)
|
|
190
202
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
203
|
+
"ClassProperty, ClassPrivateProperty, PropertyDefinition"(node) {
|
|
204
|
+
if (!node.decorators || node.decorators.length === 0) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const hasStateDecorator = node.decorators.some((decorator) => {
|
|
208
|
+
if (decorator.expression.type === "Identifier") {
|
|
209
|
+
return decorator.expression.name === "state" || stateImports.has(decorator.expression.name);
|
|
210
|
+
} else if (decorator.expression.type === "CallExpression") {
|
|
211
|
+
if (decorator.expression.callee.type === "Identifier") {
|
|
212
|
+
return decorator.expression.callee.name === "state" || stateImports.has(decorator.expression.callee.name);
|
|
199
213
|
}
|
|
200
|
-
} else if (
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
);
|
|
204
|
-
if (returnStmt?.argument?.type === "ArrayExpression") {
|
|
205
|
-
observedAttributes.push(
|
|
206
|
-
...returnStmt.argument.elements.filter((el) => el && el.type === "StringLiteral").map((el) => el.value)
|
|
207
|
-
);
|
|
214
|
+
} else if (decorator.expression.type === "MemberExpression") {
|
|
215
|
+
if (decorator.expression.property.type === "Identifier" && decorator.expression.property.name === "state") {
|
|
216
|
+
return true;
|
|
208
217
|
}
|
|
209
218
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
},
|
|
215
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
216
|
-
"ClassDeclaration:exit"(node) {
|
|
217
|
-
classObservedAttributes.delete(node);
|
|
218
|
-
},
|
|
219
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
220
|
-
Decorator(node) {
|
|
221
|
-
let isStateDecorator = false;
|
|
222
|
-
if (node.expression.type === "Identifier") {
|
|
223
|
-
const name = node.expression.name;
|
|
224
|
-
isStateDecorator = name === "state" || stateImports.has(name);
|
|
225
|
-
} else if (node.expression.type === "CallExpression") {
|
|
226
|
-
const callee = node.expression.callee;
|
|
227
|
-
if (callee.type === "Identifier") {
|
|
228
|
-
const name = callee.name;
|
|
229
|
-
isStateDecorator = name === "state" || stateImports.has(name);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
if (!isStateDecorator) {
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
let classNode = node.parent;
|
|
236
|
-
while (classNode && classNode.type !== "ClassDeclaration" && classNode.type !== "ClassExpression") {
|
|
237
|
-
classNode = classNode.parent;
|
|
238
|
-
}
|
|
239
|
-
if (!classNode) {
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
const propertyNode = node.parent;
|
|
243
|
-
if (!propertyNode || propertyNode.type !== "ClassProperty" && propertyNode.type !== "ClassPrivateProperty") {
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
const propertyName = propertyNode.key.type === "Identifier" ? propertyNode.key.name : null;
|
|
247
|
-
if (!propertyName) {
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
const observedAttributes = classObservedAttributes.get(classNode);
|
|
251
|
-
if (!observedAttributes || observedAttributes.length === 0) {
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
const propertyKebabCase = propertyName.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
255
|
-
const propertyLower = propertyName.toLowerCase();
|
|
256
|
-
if (observedAttributes.some(
|
|
257
|
-
(attr) => attr.toLowerCase() === propertyLower || attr.toLowerCase() === propertyKebabCase
|
|
258
|
-
)) {
|
|
219
|
+
return false;
|
|
220
|
+
});
|
|
221
|
+
if (hasStateDecorator && !node.value) {
|
|
222
|
+
const propertyName = node.key.type === "Identifier" ? node.key.name : node.key.type === "PrivateIdentifier" ? node.key.name : "unknown";
|
|
259
223
|
context.report({
|
|
260
224
|
node,
|
|
261
|
-
messageId: "
|
|
225
|
+
messageId: "missingInitialValue",
|
|
262
226
|
data: { propertyName }
|
|
263
227
|
});
|
|
264
228
|
}
|
|
@@ -267,69 +231,6 @@ var noStateOnHtmlAttributes = {
|
|
|
267
231
|
}
|
|
268
232
|
};
|
|
269
233
|
|
|
270
|
-
// src/rules/no-state-on-methods.ts
|
|
271
|
-
var noStateOnMethods = {
|
|
272
|
-
meta: {
|
|
273
|
-
type: "problem",
|
|
274
|
-
docs: {
|
|
275
|
-
description: "disallow @state decorator on methods",
|
|
276
|
-
category: "Possible Errors",
|
|
277
|
-
recommended: true
|
|
278
|
-
},
|
|
279
|
-
messages: {
|
|
280
|
-
stateOnMethod: "@state decorator cannot be used on methods. '{{methodName}}' is a method, not a property. @state can only be used on properties with primitive, object, or array values."
|
|
281
|
-
},
|
|
282
|
-
schema: []
|
|
283
|
-
},
|
|
284
|
-
create(context) {
|
|
285
|
-
const stateImports = /* @__PURE__ */ new Set();
|
|
286
|
-
return {
|
|
287
|
-
// Track imports
|
|
288
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
289
|
-
ImportDeclaration(node) {
|
|
290
|
-
if (node.source.type === "StringLiteral" && (node.source.value === "@wsxjs/wsx-core" || node.source.value.endsWith("/wsx-core"))) {
|
|
291
|
-
node.specifiers.forEach((spec) => {
|
|
292
|
-
if (spec.type === "ImportSpecifier" && spec.imported.type === "Identifier" && spec.imported.name === "state") {
|
|
293
|
-
const localName = spec.local.type === "Identifier" ? spec.local.name : null;
|
|
294
|
-
if (localName) {
|
|
295
|
-
stateImports.add(localName);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
},
|
|
301
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
302
|
-
Decorator(node) {
|
|
303
|
-
let isStateDecorator = false;
|
|
304
|
-
if (node.expression.type === "Identifier") {
|
|
305
|
-
const name = node.expression.name;
|
|
306
|
-
isStateDecorator = name === "state" || stateImports.has(name);
|
|
307
|
-
} else if (node.expression.type === "CallExpression") {
|
|
308
|
-
const callee = node.expression.callee;
|
|
309
|
-
if (callee.type === "Identifier") {
|
|
310
|
-
const name = callee.name;
|
|
311
|
-
isStateDecorator = name === "state" || stateImports.has(name);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
if (!isStateDecorator) {
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
const parent = node.parent;
|
|
318
|
-
if (parent && (parent.type === "ClassMethod" || parent.type === "ClassPrivateMethod")) {
|
|
319
|
-
const methodName = parent.key.type === "Identifier" ? parent.key.name : parent.key.type === "StringLiteral" ? parent.key.value : null;
|
|
320
|
-
if (methodName) {
|
|
321
|
-
context.report({
|
|
322
|
-
node,
|
|
323
|
-
messageId: "stateOnMethod",
|
|
324
|
-
data: { methodName }
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
};
|
|
332
|
-
|
|
333
234
|
// src/configs/recommended.ts
|
|
334
235
|
var recommendedConfig = {
|
|
335
236
|
parser: "@typescript-eslint/parser",
|
|
@@ -340,7 +241,9 @@ var recommendedConfig = {
|
|
|
340
241
|
jsx: true
|
|
341
242
|
},
|
|
342
243
|
jsxPragma: "h",
|
|
343
|
-
jsxFragmentName: "Fragment"
|
|
244
|
+
jsxFragmentName: "Fragment",
|
|
245
|
+
experimentalDecorators: true
|
|
246
|
+
// Required to parse @state decorators
|
|
344
247
|
},
|
|
345
248
|
plugins: ["wsx"],
|
|
346
249
|
rules: {
|
|
@@ -348,8 +251,7 @@ var recommendedConfig = {
|
|
|
348
251
|
"wsx/render-method-required": "error",
|
|
349
252
|
"wsx/no-react-imports": "error",
|
|
350
253
|
"wsx/web-component-naming": "warn",
|
|
351
|
-
"wsx/
|
|
352
|
-
"wsx/no-state-on-methods": "error",
|
|
254
|
+
"wsx/state-requires-initial-value": "error",
|
|
353
255
|
// TypeScript 规则(推荐)
|
|
354
256
|
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
|
355
257
|
"@typescript-eslint/no-explicit-any": "warn",
|
|
@@ -515,8 +417,7 @@ var plugin = {
|
|
|
515
417
|
"render-method-required": renderMethodRequired,
|
|
516
418
|
"no-react-imports": noReactImports,
|
|
517
419
|
"web-component-naming": webComponentNaming,
|
|
518
|
-
"
|
|
519
|
-
"no-state-on-methods": noStateOnMethods
|
|
420
|
+
"state-requires-initial-value": stateRequiresInitialValue
|
|
520
421
|
},
|
|
521
422
|
// 配置预设
|
|
522
423
|
configs: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wsxjs/eslint-plugin-wsx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"description": "ESLint plugin for WSX Framework",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"web-components"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@wsxjs/wsx-core": "0.0.
|
|
28
|
+
"@wsxjs/wsx-core": "0.0.13"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"tsup": "^8.0.0",
|
|
@@ -16,6 +16,7 @@ export const recommendedConfig: WSXConfig = {
|
|
|
16
16
|
},
|
|
17
17
|
jsxPragma: "h",
|
|
18
18
|
jsxFragmentName: "Fragment",
|
|
19
|
+
experimentalDecorators: true, // Required to parse @state decorators
|
|
19
20
|
},
|
|
20
21
|
plugins: ["wsx"],
|
|
21
22
|
rules: {
|
|
@@ -23,6 +24,7 @@ export const recommendedConfig: WSXConfig = {
|
|
|
23
24
|
"wsx/render-method-required": "error",
|
|
24
25
|
"wsx/no-react-imports": "error",
|
|
25
26
|
"wsx/web-component-naming": "warn",
|
|
27
|
+
"wsx/state-requires-initial-value": "error",
|
|
26
28
|
|
|
27
29
|
// TypeScript 规则(推荐)
|
|
28
30
|
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { renderMethodRequired } from "./rules/render-method-required";
|
|
9
9
|
import { noReactImports } from "./rules/no-react-imports";
|
|
10
10
|
import { webComponentNaming } from "./rules/web-component-naming";
|
|
11
|
+
import { stateRequiresInitialValue } from "./rules/state-requires-initial-value";
|
|
11
12
|
import { recommendedConfig } from "./configs/recommended";
|
|
12
13
|
import { createFlatConfig } from "./configs/flat";
|
|
13
14
|
import { WSXPlugin } from "./types";
|
|
@@ -24,6 +25,7 @@ const plugin: WSXPlugin = {
|
|
|
24
25
|
"render-method-required": renderMethodRequired,
|
|
25
26
|
"no-react-imports": noReactImports,
|
|
26
27
|
"web-component-naming": webComponentNaming,
|
|
28
|
+
"state-requires-initial-value": stateRequiresInitialValue,
|
|
27
29
|
},
|
|
28
30
|
|
|
29
31
|
// 配置预设
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint 规则:state-requires-initial-value
|
|
3
|
+
*
|
|
4
|
+
* 确保 @state 装饰器的属性必须有初始值
|
|
5
|
+
* 这是强制性的,因为我们需要初始值来判断是 primitive 还是 object/array
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Rule } from "eslint";
|
|
9
|
+
import { WSXRuleModule } from "../types";
|
|
10
|
+
|
|
11
|
+
export const stateRequiresInitialValue: WSXRuleModule = {
|
|
12
|
+
meta: {
|
|
13
|
+
type: "problem",
|
|
14
|
+
docs: {
|
|
15
|
+
description: "require @state decorator properties to have initial values",
|
|
16
|
+
category: "Possible Errors",
|
|
17
|
+
recommended: true,
|
|
18
|
+
},
|
|
19
|
+
messages: {
|
|
20
|
+
missingInitialValue:
|
|
21
|
+
"@state decorator on property '{{propertyName}}' requires an initial value.\n" +
|
|
22
|
+
"\n" +
|
|
23
|
+
"Examples:\n" +
|
|
24
|
+
" @state private {{propertyName}} = ''; // for string\n" +
|
|
25
|
+
" @state private {{propertyName}} = 0; // for number\n" +
|
|
26
|
+
" @state private {{propertyName}} = {}; // for object\n" +
|
|
27
|
+
" @state private {{propertyName}} = []; // for array\n" +
|
|
28
|
+
" @state private {{propertyName}} = undefined; // for optional",
|
|
29
|
+
},
|
|
30
|
+
schema: [],
|
|
31
|
+
},
|
|
32
|
+
create(context: Rule.RuleContext) {
|
|
33
|
+
// Track imported 'state' identifiers from @wsxjs/wsx-core
|
|
34
|
+
const stateImports = new Set<string>();
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
// Track imports to identify @state decorator
|
|
38
|
+
ImportDeclaration(node) {
|
|
39
|
+
if (
|
|
40
|
+
node.source.type === "Literal" &&
|
|
41
|
+
typeof node.source.value === "string" &&
|
|
42
|
+
node.source.value === "@wsxjs/wsx-core"
|
|
43
|
+
) {
|
|
44
|
+
node.specifiers.forEach((specifier) => {
|
|
45
|
+
if (specifier.type === "ImportSpecifier") {
|
|
46
|
+
if (
|
|
47
|
+
specifier.imported.type === "Identifier" &&
|
|
48
|
+
specifier.imported.name === "state"
|
|
49
|
+
) {
|
|
50
|
+
// Track both the imported name and any alias
|
|
51
|
+
const localName =
|
|
52
|
+
specifier.local.type === "Identifier"
|
|
53
|
+
? specifier.local.name
|
|
54
|
+
: null;
|
|
55
|
+
if (localName) {
|
|
56
|
+
stateImports.add(localName);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} else if (specifier.type === "ImportDefaultSpecifier") {
|
|
60
|
+
// Handle default import (less common but possible)
|
|
61
|
+
const localName =
|
|
62
|
+
specifier.local.type === "Identifier" ? specifier.local.name : null;
|
|
63
|
+
if (localName) {
|
|
64
|
+
stateImports.add(localName);
|
|
65
|
+
}
|
|
66
|
+
} else if (specifier.type === "ImportNamespaceSpecifier") {
|
|
67
|
+
// Handle namespace import: import * as wsx from '@wsxjs/wsx-core'
|
|
68
|
+
// In this case, @state would be wsx.state, which is harder to detect
|
|
69
|
+
// We'll check for both 'state' and namespace.state patterns
|
|
70
|
+
const localName =
|
|
71
|
+
specifier.local.type === "Identifier" ? specifier.local.name : null;
|
|
72
|
+
if (localName) {
|
|
73
|
+
// For namespace imports, we'd need to check member expressions
|
|
74
|
+
// This is more complex, so we'll also check for plain 'state'
|
|
75
|
+
stateImports.add("state");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// Check class properties for @state decorator
|
|
83
|
+
// Support both ClassProperty (older) and PropertyDefinition (newer TypeScript ESLint)
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
85
|
+
"ClassProperty, ClassPrivateProperty, PropertyDefinition"(node: any) {
|
|
86
|
+
if (!node.decorators || node.decorators.length === 0) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check if any decorator is @state
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
92
|
+
const hasStateDecorator = node.decorators.some((decorator: any) => {
|
|
93
|
+
if (decorator.expression.type === "Identifier") {
|
|
94
|
+
// Direct identifier: @state
|
|
95
|
+
return (
|
|
96
|
+
decorator.expression.name === "state" ||
|
|
97
|
+
stateImports.has(decorator.expression.name)
|
|
98
|
+
);
|
|
99
|
+
} else if (decorator.expression.type === "CallExpression") {
|
|
100
|
+
// Call expression: @state()
|
|
101
|
+
if (decorator.expression.callee.type === "Identifier") {
|
|
102
|
+
return (
|
|
103
|
+
decorator.expression.callee.name === "state" ||
|
|
104
|
+
stateImports.has(decorator.expression.callee.name)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
} else if (decorator.expression.type === "MemberExpression") {
|
|
108
|
+
// Member expression: @namespace.state
|
|
109
|
+
if (
|
|
110
|
+
decorator.expression.property.type === "Identifier" &&
|
|
111
|
+
decorator.expression.property.name === "state"
|
|
112
|
+
) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (hasStateDecorator && !node.value) {
|
|
120
|
+
const propertyName =
|
|
121
|
+
node.key.type === "Identifier"
|
|
122
|
+
? node.key.name
|
|
123
|
+
: node.key.type === "PrivateIdentifier"
|
|
124
|
+
? node.key.name
|
|
125
|
+
: "unknown";
|
|
126
|
+
|
|
127
|
+
context.report({
|
|
128
|
+
node,
|
|
129
|
+
messageId: "missingInitialValue",
|
|
130
|
+
data: { propertyName },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
};
|