@lvce-editor/eslint-plugin-virtual-dom 13.2.0 → 13.4.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.
Files changed (2) hide show
  1. package/dist/index.js +533 -13
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,6 +1,12 @@
1
1
  const isPropertyNode = node => {
2
2
  return typeof node === 'object' && node !== null && 'type' in node && node.type === 'Property' && 'key' in node && 'value' in node && 'computed' in node;
3
3
  };
4
+ const isObjectExpressionNode = node => {
5
+ return typeof node === 'object' && node !== null && 'type' in node && node.type === 'ObjectExpression' && 'properties' in node;
6
+ };
7
+ const isArrayExpressionNode = node => {
8
+ return typeof node === 'object' && node !== null && 'type' in node && node.type === 'ArrayExpression' && 'elements' in node;
9
+ };
4
10
  const isIdentifierNode = node => {
5
11
  return typeof node === 'object' && node !== null && 'type' in node && node.type === 'Identifier' && 'name' in node;
6
12
  };
@@ -13,25 +19,412 @@ const isTemplateLiteralNode = node => {
13
19
  const isBinaryExpressionNode = node => {
14
20
  return typeof node === 'object' && node !== null && 'type' in node && node.type === 'BinaryExpression' && 'operator' in node && 'left' in node && 'right' in node;
15
21
  };
16
- const isClassNameKey = node => {
17
- if (node.computed) {
18
- return false;
22
+ const isCallExpressionNode = node => {
23
+ return typeof node === 'object' && node !== null && 'type' in node && node.type === 'CallExpression' && 'callee' in node && 'arguments' in node;
24
+ };
25
+ const isUnaryExpressionNode = node => {
26
+ return typeof node === 'object' && node !== null && 'type' in node && node.type === 'UnaryExpression' && 'operator' in node && 'argument' in node;
27
+ };
28
+ const isMemberExpressionNode = node => {
29
+ return typeof node === 'object' && node !== null && 'type' in node && node.type === 'MemberExpression' && 'object' in node && 'property' in node && 'computed' in node;
30
+ };
31
+ const isArrowFunctionExpressionNode = node => {
32
+ return typeof node === 'object' && node !== null && 'type' in node && node.type === 'ArrowFunctionExpression';
33
+ };
34
+ const isFunctionExpressionNode = node => {
35
+ return typeof node === 'object' && node !== null && 'type' in node && node.type === 'FunctionExpression';
36
+ };
37
+ const getStaticPropertyName = property => {
38
+ if (property.computed) {
39
+ return undefined;
19
40
  }
20
- if (isIdentifierNode(node.key)) {
21
- return node.key.name === 'className';
41
+ if (isIdentifierNode(property.key)) {
42
+ return property.key.name;
22
43
  }
23
- if (isLiteralNode(node.key)) {
24
- return node.key.value === 'className';
44
+ if (isLiteralNode(property.key) && typeof property.key.value === 'string') {
45
+ return property.key.value;
25
46
  }
26
- return false;
47
+ return undefined;
48
+ };
49
+ const getProperty = (node, name) => {
50
+ return node.properties.find(property => isPropertyNode(property) && getStaticPropertyName(property) === name);
51
+ };
52
+ const hasProperty = (node, name) => {
53
+ return Boolean(getProperty(node, name));
54
+ };
55
+ const isVirtualDomNode = node => {
56
+ return isObjectExpressionNode(node) && hasProperty(node, 'type');
57
+ };
58
+ const isTextCall = node => {
59
+ return isCallExpressionNode(node) && isIdentifierNode(node.callee) && node.callee.name === 'text';
60
+ };
61
+ const isMemberExpressionWithProperty = (node, propertyName) => {
62
+ return isMemberExpressionNode(node) && !node.computed && isIdentifierNode(node.property) && node.property.name === propertyName;
63
+ };
64
+ const isNumericLiteral = node => {
65
+ return isLiteralNode(node) && typeof node.value === 'number';
66
+ };
67
+ const getStaticNumberValue = node => {
68
+ if (isNumericLiteral(node)) {
69
+ return node.value;
70
+ }
71
+ if (isUnaryExpressionNode(node) && node.operator === '-' && isNumericLiteral(node.argument)) {
72
+ return -node.argument.value;
73
+ }
74
+ return undefined;
75
+ };
76
+ const isStringLiteral = node => {
77
+ return isLiteralNode(node) && typeof node.value === 'string';
78
+ };
79
+
80
+ const meta$9 = {
81
+ docs: {
82
+ description: 'Require an explicit role on clickable virtual-dom div nodes'
83
+ },
84
+ messages: {
85
+ clickableDivNeedsRole: 'Add an explicit `role` to clickable virtual-dom div nodes.'
86
+ },
87
+ type: 'problem'
88
+ };
89
+ const create$9 = context => {
90
+ return {
91
+ ObjectExpression(node) {
92
+ if (!isVirtualDomNode(node)) {
93
+ return;
94
+ }
95
+ const typeProperty = getProperty(node, 'type');
96
+ const clickProperty = getProperty(node, 'onClick');
97
+ const roleProperty = getProperty(node, 'role');
98
+ if (!typeProperty || !clickProperty || roleProperty || !isMemberExpressionWithProperty(typeProperty.value, 'Div')) {
99
+ return;
100
+ }
101
+ context.report({
102
+ messageId: 'clickableDivNeedsRole',
103
+ node: clickProperty
104
+ });
105
+ }
106
+ };
107
+ };
108
+
109
+ const clickableDivNeedsRole = {
110
+ __proto__: null,
111
+ create: create$9,
112
+ meta: meta$9
113
+ };
114
+
115
+ const meta$8 = {
116
+ docs: {
117
+ description: 'Disallow empty ARIA values in virtual-dom nodes'
118
+ },
119
+ messages: {
120
+ noEmptyAria: 'Omit empty ARIA attributes instead of using an empty string.'
121
+ },
122
+ type: 'problem'
123
+ };
124
+ const isAriaProperty = name => {
125
+ return /^aria[A-Z]/.test(name);
126
+ };
127
+ const create$8 = context => {
128
+ return {
129
+ ObjectExpression(node) {
130
+ if (!isVirtualDomNode(node)) {
131
+ return;
132
+ }
133
+ for (const property of node.properties) {
134
+ if (!isPropertyNode(property)) {
135
+ continue;
136
+ }
137
+ const name = getStaticPropertyName(property);
138
+ if (!name || !isAriaProperty(name) || !isStringLiteral(property.value) || property.value.value !== '') {
139
+ continue;
140
+ }
141
+ context.report({
142
+ messageId: 'noEmptyAria',
143
+ node: property.value
144
+ });
145
+ }
146
+ }
147
+ };
148
+ };
149
+
150
+ const noEmptyAria = {
151
+ __proto__: null,
152
+ create: create$8,
153
+ meta: meta$8
154
+ };
155
+
156
+ const meta$7 = {
157
+ docs: {
158
+ description: 'Disallow empty className values in virtual-dom nodes'
159
+ },
160
+ messages: {
161
+ noEmptyClassName: 'Omit `className` instead of using an empty string.'
162
+ },
163
+ type: 'problem'
164
+ };
165
+ const create$7 = context => {
166
+ return {
167
+ ObjectExpression(node) {
168
+ if (!isVirtualDomNode(node)) {
169
+ return;
170
+ }
171
+ const classNameProperty = getProperty(node, 'className');
172
+ if (!classNameProperty || !isStringLiteral(classNameProperty.value) || classNameProperty.value.value !== '') {
173
+ return;
174
+ }
175
+ context.report({
176
+ messageId: 'noEmptyClassName',
177
+ node: classNameProperty.value
178
+ });
179
+ }
180
+ };
181
+ };
182
+
183
+ const noEmptyClassName = {
184
+ __proto__: null,
185
+ create: create$7,
186
+ meta: meta$7
187
+ };
188
+
189
+ const meta$6 = {
190
+ docs: {
191
+ description: 'Disallow inline event handlers in virtual-dom nodes'
192
+ },
193
+ messages: {
194
+ noInlineEventHandlers: 'Use a registered event listener id instead of an inline event handler.'
195
+ },
196
+ type: 'problem'
197
+ };
198
+ const isEventPropertyName = name => {
199
+ return /^on[A-Z]/.test(name);
200
+ };
201
+ const isInlineEventHandler = node => {
202
+ return isStringLiteral(node) || isTemplateLiteralNode(node) || isArrowFunctionExpressionNode(node) || isFunctionExpressionNode(node);
203
+ };
204
+ const create$6 = context => {
205
+ return {
206
+ ObjectExpression(node) {
207
+ if (!isVirtualDomNode(node)) {
208
+ return;
209
+ }
210
+ for (const property of node.properties) {
211
+ if (!isPropertyNode(property)) {
212
+ continue;
213
+ }
214
+ const name = getStaticPropertyName(property);
215
+ if (!name || !isEventPropertyName(name) || !isInlineEventHandler(property.value)) {
216
+ continue;
217
+ }
218
+ context.report({
219
+ messageId: 'noInlineEventHandlers',
220
+ node: property.value
221
+ });
222
+ }
223
+ }
224
+ };
225
+ };
226
+
227
+ const noInlineEventHandlers = {
228
+ __proto__: null,
229
+ create: create$6,
230
+ meta: meta$6
231
+ };
232
+
233
+ const meta$5 = {
234
+ docs: {
235
+ description: 'Disallow inline style attributes in virtual-dom nodes'
236
+ },
237
+ messages: {
238
+ noInlineStyle: 'Use class names or generated CSS instead of inline `style`.'
239
+ },
240
+ type: 'problem'
241
+ };
242
+ const create$5 = context => {
243
+ return {
244
+ ObjectExpression(node) {
245
+ if (!isVirtualDomNode(node)) {
246
+ return;
247
+ }
248
+ const styleProperty = getProperty(node, 'style');
249
+ if (!styleProperty) {
250
+ return;
251
+ }
252
+ context.report({
253
+ messageId: 'noInlineStyle',
254
+ node: styleProperty
255
+ });
256
+ }
257
+ };
258
+ };
259
+
260
+ const noInlineStyle = {
261
+ __proto__: null,
262
+ create: create$5,
263
+ meta: meta$5
264
+ };
265
+
266
+ const meta$4 = {
267
+ docs: {
268
+ description: 'Disallow object-like attribute values in virtual-dom nodes'
269
+ },
270
+ messages: {
271
+ noObjectAttributeValues: 'Avoid object, array, or function attribute values because virtual-dom diffing compares attributes by reference.'
272
+ },
273
+ type: 'problem'
274
+ };
275
+ const ignoredProperties = new Set(['childCount', 'type']);
276
+ const isObjectLikeAttributeValue = node => {
277
+ return isObjectExpressionNode(node) || isArrayExpressionNode(node) || isArrowFunctionExpressionNode(node) || isFunctionExpressionNode(node);
278
+ };
279
+ const create$4 = context => {
280
+ return {
281
+ ObjectExpression(node) {
282
+ if (!isVirtualDomNode(node)) {
283
+ return;
284
+ }
285
+ for (const property of node.properties) {
286
+ if (!isPropertyNode(property)) {
287
+ continue;
288
+ }
289
+ const name = getStaticPropertyName(property);
290
+ if (!name || ignoredProperties.has(name) || !isObjectLikeAttributeValue(property.value)) {
291
+ continue;
292
+ }
293
+ context.report({
294
+ messageId: 'noObjectAttributeValues',
295
+ node: property.value
296
+ });
297
+ }
298
+ }
299
+ };
300
+ };
301
+
302
+ const noObjectAttributeValues = {
303
+ __proto__: null,
304
+ create: create$4,
305
+ meta: meta$4
306
+ };
307
+
308
+ const meta$3 = {
309
+ docs: {
310
+ description: 'Disallow raw string children in virtual-dom arrays'
311
+ },
312
+ messages: {
313
+ noRawTextChildren: 'Use `text(...)` instead of a raw string in virtual-dom arrays.'
314
+ },
315
+ type: 'problem'
316
+ };
317
+ const isVirtualDomArray = node => {
318
+ return isArrayExpressionNode(node) && node.elements.some(element => isVirtualDomNode(element) || isTextCall(element));
319
+ };
320
+ const create$3 = context => {
321
+ return {
322
+ ArrayExpression(node) {
323
+ if (!isVirtualDomArray(node)) {
324
+ return;
325
+ }
326
+ for (const element of node.elements) {
327
+ if (!isStringLiteral(element)) {
328
+ continue;
329
+ }
330
+ context.report({
331
+ messageId: 'noRawTextChildren',
332
+ node: element
333
+ });
334
+ }
335
+ }
336
+ };
337
+ };
338
+
339
+ const noRawTextChildren = {
340
+ __proto__: null,
341
+ create: create$3,
342
+ meta: meta$3
343
+ };
344
+
345
+ const meta$2 = {
346
+ docs: {
347
+ description: 'Prefer virtual-dom constants over raw type, role, aria boolean, and tabIndex values'
348
+ },
349
+ messages: {
350
+ preferAriaBooleanConstant: 'Use an ARIA boolean constant instead of a raw string.',
351
+ preferRoleConstant: 'Use `AriaRoles.*` instead of a raw role string.',
352
+ preferTabIndexConstant: 'Use `TabIndex.*` instead of a raw tabIndex number.',
353
+ preferTypeConstant: 'Use `VirtualDomElements.*` instead of a raw element type number.'
354
+ },
355
+ type: 'suggestion'
356
+ };
357
+ const isAriaBooleanProperty = name => {
358
+ return /^aria[A-Z]/.test(name);
359
+ };
360
+ const create$2 = context => {
361
+ return {
362
+ ObjectExpression(node) {
363
+ if (!isVirtualDomNode(node)) {
364
+ return;
365
+ }
366
+ for (const property of node.properties) {
367
+ if (!isPropertyNode(property)) {
368
+ continue;
369
+ }
370
+ const name = getStaticPropertyName(property);
371
+ if (!name) {
372
+ continue;
373
+ }
374
+ if (name === 'type' && isNumericLiteral(property.value)) {
375
+ context.report({
376
+ messageId: 'preferTypeConstant',
377
+ node: property.value
378
+ });
379
+ continue;
380
+ }
381
+ if (name === 'role' && isStringLiteral(property.value)) {
382
+ context.report({
383
+ messageId: 'preferRoleConstant',
384
+ node: property.value
385
+ });
386
+ continue;
387
+ }
388
+ if (name === 'tabIndex' && isNumericLiteral(property.value)) {
389
+ context.report({
390
+ messageId: 'preferTabIndexConstant',
391
+ node: property.value
392
+ });
393
+ continue;
394
+ }
395
+ if (isAriaBooleanProperty(name) && isStringLiteral(property.value) && (property.value.value === 'true' || property.value.value === 'false')) {
396
+ context.report({
397
+ messageId: 'preferAriaBooleanConstant',
398
+ node: property.value
399
+ });
400
+ }
401
+ }
402
+ }
403
+ };
404
+ };
405
+
406
+ const preferConstants = {
407
+ __proto__: null,
408
+ create: create$2,
409
+ meta: meta$2
410
+ };
411
+
412
+ const isClassNameKey = node => {
413
+ return getStaticPropertyName(node) === 'className';
27
414
  };
28
415
  const isStringLiteralWithSpace = node => {
29
- return isLiteralNode(node) && typeof node.value === 'string' && /\s/.test(node.value);
416
+ return isStringLiteral(node) && /\s/.test(node.value);
417
+ };
418
+ const isStaticMultiClassName = node => {
419
+ return isStringLiteral(node) && /\s/.test(node.value) && node.value.trim().split(/\s+/).length > 1;
30
420
  };
31
421
  const hasTemplateClassSeparator = node => {
32
422
  return node.quasis.some(quasi => /\s/.test(quasi.value.raw));
33
423
  };
34
424
  const isManualClassNameConcatenation = node => {
425
+ if (isStaticMultiClassName(node)) {
426
+ return true;
427
+ }
35
428
  if (isTemplateLiteralNode(node)) {
36
429
  return node.expressions.length > 0 && hasTemplateClassSeparator(node);
37
430
  }
@@ -40,7 +433,7 @@ const isManualClassNameConcatenation = node => {
40
433
  }
41
434
  return isStringLiteralWithSpace(node.left) || isStringLiteralWithSpace(node.right) || isManualClassNameConcatenation(node.left) || isManualClassNameConcatenation(node.right);
42
435
  };
43
- const meta = {
436
+ const meta$1 = {
44
437
  docs: {
45
438
  description: 'Prefer mergeClassNames for composing virtual-dom className values'
46
439
  },
@@ -49,7 +442,7 @@ const meta = {
49
442
  },
50
443
  type: 'suggestion'
51
444
  };
52
- const create = context => {
445
+ const create$1 = context => {
53
446
  return {
54
447
  Property(node) {
55
448
  if (!isPropertyNode(node) || !isClassNameKey(node) || !isManualClassNameConcatenation(node.value)) {
@@ -64,6 +457,115 @@ const create = context => {
64
457
  };
65
458
 
66
459
  const preferMergeClassNames = {
460
+ __proto__: null,
461
+ create: create$1,
462
+ meta: meta$1
463
+ };
464
+
465
+ const meta = {
466
+ docs: {
467
+ description: 'Validate statically analyzable virtual-dom childCount values'
468
+ },
469
+ messages: {
470
+ invalidChildCount: '`childCount` must be a non-negative integer.',
471
+ validChildCount: '`childCount` declares more children than this static virtual-dom array contains.'
472
+ },
473
+ type: 'problem'
474
+ };
475
+ const getChildCountNode = node => {
476
+ if (!isVirtualDomNode(node)) {
477
+ return undefined;
478
+ }
479
+ return getProperty(node, 'childCount')?.value;
480
+ };
481
+ const getStaticChildCount = node => {
482
+ if (isTextCall(node)) {
483
+ return 0;
484
+ }
485
+ if (!isVirtualDomNode(node)) {
486
+ return undefined;
487
+ }
488
+ const childCountProperty = getProperty(node, 'childCount');
489
+ if (!childCountProperty) {
490
+ return 0;
491
+ }
492
+ const childCount = getStaticNumberValue(childCountProperty.value);
493
+ if (childCount === undefined) {
494
+ return undefined;
495
+ }
496
+ return childCount;
497
+ };
498
+ const isValidChildCount = childCount => {
499
+ return Number.isSafeInteger(childCount) && childCount >= 0;
500
+ };
501
+ const isAnalyzableVirtualDomElement = node => {
502
+ return isTextCall(node) || isVirtualDomNode(node);
503
+ };
504
+ const computeSubtreeEnd = (elements, index) => {
505
+ const childCount = getStaticChildCount(elements[index]);
506
+ if (childCount === undefined || !isValidChildCount(childCount)) {
507
+ return undefined;
508
+ }
509
+ let next = index + 1;
510
+ for (let childIndex = 0; childIndex < childCount; childIndex++) {
511
+ if (next >= elements.length) {
512
+ return undefined;
513
+ }
514
+ const childEnd = computeSubtreeEnd(elements, next);
515
+ if (childEnd === undefined) {
516
+ return undefined;
517
+ }
518
+ next = childEnd;
519
+ }
520
+ return next;
521
+ };
522
+ const hasImpossibleChildCount = (elements, index) => {
523
+ const childCount = getStaticChildCount(elements[index]);
524
+ if (childCount === undefined || !isValidChildCount(childCount)) {
525
+ return false;
526
+ }
527
+ let next = index + 1;
528
+ for (let childIndex = 0; childIndex < childCount; childIndex++) {
529
+ if (next >= elements.length) {
530
+ return true;
531
+ }
532
+ const childEnd = computeSubtreeEnd(elements, next);
533
+ if (childEnd === undefined) {
534
+ return true;
535
+ }
536
+ next = childEnd;
537
+ }
538
+ return false;
539
+ };
540
+ const create = context => {
541
+ return {
542
+ ArrayExpression(node) {
543
+ if (!isArrayExpressionNode(node) || !node.elements.some(isVirtualDomNode) || !node.elements.every(isAnalyzableVirtualDomElement)) {
544
+ return;
545
+ }
546
+ for (let index = 0; index < node.elements.length; index++) {
547
+ const element = node.elements[index];
548
+ const childCount = getStaticChildCount(element);
549
+ const childCountNode = getChildCountNode(element);
550
+ if (childCount !== undefined && !isValidChildCount(childCount) && childCountNode) {
551
+ context.report({
552
+ messageId: 'invalidChildCount',
553
+ node: childCountNode
554
+ });
555
+ continue;
556
+ }
557
+ if (childCountNode && hasImpossibleChildCount(node.elements, index)) {
558
+ context.report({
559
+ messageId: 'validChildCount',
560
+ node: childCountNode
561
+ });
562
+ }
563
+ }
564
+ }
565
+ };
566
+ };
567
+
568
+ const validChildCount = {
67
569
  __proto__: null,
68
570
  create,
69
571
  meta
@@ -76,7 +578,16 @@ const plugin = {
76
578
  version: '0.0.1'
77
579
  },
78
580
  rules: {
79
- 'prefer-merge-class-names': preferMergeClassNames
581
+ 'clickable-div-needs-role': clickableDivNeedsRole,
582
+ 'no-empty-aria': noEmptyAria,
583
+ 'no-empty-class-name': noEmptyClassName,
584
+ 'no-inline-event-handlers': noInlineEventHandlers,
585
+ 'no-inline-style': noInlineStyle,
586
+ 'no-object-attribute-values': noObjectAttributeValues,
587
+ 'no-raw-text-children': noRawTextChildren,
588
+ 'prefer-constants': preferConstants,
589
+ 'prefer-merge-class-names': preferMergeClassNames,
590
+ 'valid-child-count': validChildCount
80
591
  }
81
592
  };
82
593
  const recommended = [{
@@ -85,7 +596,16 @@ const recommended = [{
85
596
  'virtual-dom': plugin
86
597
  },
87
598
  rules: {
88
- 'virtual-dom/prefer-merge-class-names': 'error'
599
+ 'virtual-dom/clickable-div-needs-role': 'error',
600
+ 'virtual-dom/no-empty-aria': 'error',
601
+ 'virtual-dom/no-empty-class-name': 'error',
602
+ 'virtual-dom/no-inline-event-handlers': 'error',
603
+ 'virtual-dom/no-inline-style': 'error',
604
+ 'virtual-dom/no-object-attribute-values': 'error',
605
+ 'virtual-dom/no-raw-text-children': 'error',
606
+ 'virtual-dom/prefer-constants': 'error',
607
+ 'virtual-dom/prefer-merge-class-names': 'error',
608
+ 'virtual-dom/valid-child-count': 'error'
89
609
  }
90
610
  }];
91
611
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/eslint-plugin-virtual-dom",
3
- "version": "13.2.0",
3
+ "version": "13.4.0",
4
4
  "main": "dist/index.js",
5
5
  "repository": {
6
6
  "type": "git",