@lvce-editor/eslint-plugin-virtual-dom 17.1.0 → 17.3.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 +666 -53
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -52,8 +52,19 @@ const getProperty = (node, name) => {
52
52
  const hasProperty = (node, name) => {
53
53
  return Boolean(getProperty(node, name));
54
54
  };
55
+ const virtualDomElementNames = new Set(['A', 'Abbr', 'Article', 'Aside', 'Audio', 'Br', 'Button', 'Cite', 'Code', 'Col', 'ColGroup', 'Data', 'Dd', 'Del', 'Div', 'Dl', 'Dt', 'Figcaption', 'Figure', 'Footer', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'Header', 'Hr', 'I', 'Img', 'Input', 'Ins', 'Kbd', 'Label', 'Li', 'Nav', 'Ol', 'Option', 'P', 'Pre', 'Reference', 'Root', 'Search', 'Section', 'Select', 'Span', 'Table', 'TBody', 'Td', 'Text', 'TextArea', 'Tfoot', 'Th', 'THead', 'Time', 'Tr', 'Ul', 'Video']);
56
+ const getVirtualDomElementName = node => {
57
+ if (!isMemberExpressionNode(node) || node.computed || !isIdentifierNode(node.object) || !isIdentifierNode(node.property) || !node.object.name.endsWith('Elements') || !virtualDomElementNames.has(node.property.name)) {
58
+ return undefined;
59
+ }
60
+ return node.property.name;
61
+ };
55
62
  const isVirtualDomNode = node => {
56
- return isObjectExpressionNode(node) && hasProperty(node, 'type');
63
+ if (!isObjectExpressionNode(node)) {
64
+ return false;
65
+ }
66
+ const typeProperty = getProperty(node, 'type');
67
+ return Boolean(typeProperty && (hasProperty(node, 'childCount') || getVirtualDomElementName(typeProperty.value)));
57
68
  };
58
69
  const isTextCall = node => {
59
70
  return isCallExpressionNode(node) && isIdentifierNode(node.callee) && node.callee.name === 'text';
@@ -77,7 +88,67 @@ const isStringLiteral = node => {
77
88
  return isLiteralNode(node) && typeof node.value === 'string';
78
89
  };
79
90
 
80
- const meta$c = {
91
+ const controlsRequiringDirectName = new Set(['Input', 'TextArea']);
92
+ const controlsAllowingChildren = new Set(['A', 'Button']);
93
+ const hasDirectName = node => {
94
+ return Boolean(getProperty(node, 'ariaLabel') || getProperty(node, 'ariaLabelledBy') || getProperty(node, 'title'));
95
+ };
96
+ const meta$p = {
97
+ docs: {
98
+ description: 'Require a statically determinable accessible name for virtual-dom controls'
99
+ },
100
+ messages: {
101
+ accessibleControlName: 'Add an accessible name to this virtual-dom `{{name}}` control.'
102
+ },
103
+ type: 'problem'
104
+ };
105
+ const create$p = context => {
106
+ return {
107
+ ObjectExpression(node) {
108
+ if (!isVirtualDomNode(node) || hasDirectName(node)) {
109
+ return;
110
+ }
111
+ const typeProperty = getProperty(node, 'type');
112
+ const elementName = typeProperty && getVirtualDomElementName(typeProperty.value);
113
+ if (!elementName) {
114
+ return;
115
+ }
116
+ if (controlsRequiringDirectName.has(elementName)) {
117
+ context.report({
118
+ data: {
119
+ name: elementName
120
+ },
121
+ messageId: 'accessibleControlName',
122
+ node
123
+ });
124
+ return;
125
+ }
126
+ if (!controlsAllowingChildren.has(elementName)) {
127
+ return;
128
+ }
129
+ const childCountProperty = getProperty(node, 'childCount');
130
+ const childCount = childCountProperty && getStaticNumberValue(childCountProperty.value);
131
+ if (childCount !== 0) {
132
+ return;
133
+ }
134
+ context.report({
135
+ data: {
136
+ name: elementName
137
+ },
138
+ messageId: 'accessibleControlName',
139
+ node
140
+ });
141
+ }
142
+ };
143
+ };
144
+
145
+ const accessibleControlName = {
146
+ __proto__: null,
147
+ create: create$p,
148
+ meta: meta$p
149
+ };
150
+
151
+ const meta$o = {
81
152
  docs: {
82
153
  description: 'Require an explicit role on clickable virtual-dom div nodes'
83
154
  },
@@ -86,7 +157,7 @@ const meta$c = {
86
157
  },
87
158
  type: 'problem'
88
159
  };
89
- const create$c = context => {
160
+ const create$o = context => {
90
161
  return {
91
162
  ObjectExpression(node) {
92
163
  if (!isVirtualDomNode(node)) {
@@ -108,11 +179,11 @@ const create$c = context => {
108
179
 
109
180
  const clickableDivNeedsRole = {
110
181
  __proto__: null,
111
- create: create$c,
112
- meta: meta$c
182
+ create: create$o,
183
+ meta: meta$o
113
184
  };
114
185
 
115
- const meta$b = {
186
+ const meta$n = {
116
187
  docs: {
117
188
  description: 'Require static virtual-dom nodes created inside functions to be hoisted to module scope'
118
189
  },
@@ -223,7 +294,7 @@ const isStaticExpression = (sourceCode, node) => {
223
294
  return false;
224
295
  }
225
296
  };
226
- const create$b = context => {
297
+ const create$n = context => {
227
298
  return {
228
299
  ObjectExpression(node) {
229
300
  if (!isVirtualDomElement(node)) {
@@ -248,11 +319,52 @@ const create$b = context => {
248
319
 
249
320
  const hoistStaticNodes = {
250
321
  __proto__: null,
251
- create: create$b,
252
- meta: meta$b
322
+ create: create$n,
323
+ meta: meta$n
253
324
  };
254
325
 
255
- const meta$a = {
326
+ const meta$m = {
327
+ docs: {
328
+ description: 'Disallow conditional spreads in virtual-dom arrays'
329
+ },
330
+ messages: {
331
+ noConditionalSpread: 'Move conditional virtual-dom children into a helper function and spread the helper result.'
332
+ },
333
+ type: 'suggestion'
334
+ };
335
+ const isConditionalSpread = node => {
336
+ return node?.type === 'SpreadElement' && node.argument.type === 'ConditionalExpression';
337
+ };
338
+ const isVirtualDomArray$1 = node => {
339
+ return isArrayExpressionNode(node) && node.elements.some(element => isVirtualDomNode(element) || isTextCall(element));
340
+ };
341
+ const addsVirtualDomChildren = spread => {
342
+ return isVirtualDomArray$1(spread.argument.consequent) || isVirtualDomArray$1(spread.argument.alternate);
343
+ };
344
+ const create$m = context => {
345
+ return {
346
+ ArrayExpression(node) {
347
+ const conditionalSpreads = node.elements.filter(isConditionalSpread);
348
+ if (!isVirtualDomArray$1(node) && !conditionalSpreads.some(addsVirtualDomChildren)) {
349
+ return;
350
+ }
351
+ for (const spread of conditionalSpreads) {
352
+ context.report({
353
+ messageId: 'noConditionalSpread',
354
+ node: spread
355
+ });
356
+ }
357
+ }
358
+ };
359
+ };
360
+
361
+ const noConditionalSpread = {
362
+ __proto__: null,
363
+ create: create$m,
364
+ meta: meta$m
365
+ };
366
+
367
+ const meta$l = {
256
368
  docs: {
257
369
  description: 'Disallow empty ARIA values in virtual-dom nodes'
258
370
  },
@@ -264,7 +376,7 @@ const meta$a = {
264
376
  const isAriaProperty = name => {
265
377
  return /^aria[A-Z]/.test(name);
266
378
  };
267
- const create$a = context => {
379
+ const create$l = context => {
268
380
  return {
269
381
  ObjectExpression(node) {
270
382
  if (!isVirtualDomNode(node)) {
@@ -289,11 +401,11 @@ const create$a = context => {
289
401
 
290
402
  const noEmptyAria = {
291
403
  __proto__: null,
292
- create: create$a,
293
- meta: meta$a
404
+ create: create$l,
405
+ meta: meta$l
294
406
  };
295
407
 
296
- const meta$9 = {
408
+ const meta$k = {
297
409
  docs: {
298
410
  description: 'Disallow empty className values in virtual-dom nodes'
299
411
  },
@@ -302,7 +414,7 @@ const meta$9 = {
302
414
  },
303
415
  type: 'problem'
304
416
  };
305
- const create$9 = context => {
417
+ const create$k = context => {
306
418
  return {
307
419
  ObjectExpression(node) {
308
420
  if (!isVirtualDomNode(node)) {
@@ -322,11 +434,11 @@ const create$9 = context => {
322
434
 
323
435
  const noEmptyClassName = {
324
436
  __proto__: null,
325
- create: create$9,
326
- meta: meta$9
437
+ create: create$k,
438
+ meta: meta$k
327
439
  };
328
440
 
329
- const meta$8 = {
441
+ const meta$j = {
330
442
  docs: {
331
443
  description: 'Disallow inline event handlers in virtual-dom nodes'
332
444
  },
@@ -341,7 +453,7 @@ const isEventPropertyName = name => {
341
453
  const isInlineEventHandler = node => {
342
454
  return isStringLiteral(node) || isTemplateLiteralNode(node) || isArrowFunctionExpressionNode(node) || isFunctionExpressionNode(node);
343
455
  };
344
- const create$8 = context => {
456
+ const create$j = context => {
345
457
  return {
346
458
  ObjectExpression(node) {
347
459
  if (!isVirtualDomNode(node)) {
@@ -366,11 +478,11 @@ const create$8 = context => {
366
478
 
367
479
  const noInlineEventHandlers = {
368
480
  __proto__: null,
369
- create: create$8,
370
- meta: meta$8
481
+ create: create$j,
482
+ meta: meta$j
371
483
  };
372
484
 
373
- const meta$7 = {
485
+ const meta$i = {
374
486
  docs: {
375
487
  description: 'Disallow inline style attributes in virtual-dom nodes'
376
488
  },
@@ -379,7 +491,7 @@ const meta$7 = {
379
491
  },
380
492
  type: 'problem'
381
493
  };
382
- const create$7 = context => {
494
+ const create$i = context => {
383
495
  return {
384
496
  ObjectExpression(node) {
385
497
  if (!isVirtualDomNode(node)) {
@@ -399,11 +511,59 @@ const create$7 = context => {
399
511
 
400
512
  const noInlineStyle = {
401
513
  __proto__: null,
402
- create: create$7,
403
- meta: meta$7
514
+ create: create$i,
515
+ meta: meta$i
404
516
  };
405
517
 
406
- const meta$6 = {
518
+ const ignoredProperties$1 = new Set(['childCount', 'type']);
519
+ const isOptionalExpression = node => {
520
+ return typeof node === 'object' && node !== null && ('type' in node && node.type === 'ChainExpression' || 'optional' in node && node.optional === true);
521
+ };
522
+ const isNullish = node => {
523
+ return isLiteralNode(node) && node.value === null || isIdentifierNode(node) && node.name === 'undefined' || isOptionalExpression(node);
524
+ };
525
+ const meta$h = {
526
+ docs: {
527
+ description: 'Disallow nullish virtual-dom attribute values'
528
+ },
529
+ messages: {
530
+ noNullishAttributeValue: 'Omit `{{name}}` instead of assigning a nullish value.'
531
+ },
532
+ type: 'problem'
533
+ };
534
+ const create$h = context => {
535
+ return {
536
+ ObjectExpression(node) {
537
+ if (!isVirtualDomNode(node)) {
538
+ return;
539
+ }
540
+ for (const property of node.properties) {
541
+ if (!isPropertyNode(property)) {
542
+ continue;
543
+ }
544
+ const name = getStaticPropertyName(property);
545
+ if (!name || ignoredProperties$1.has(name) || !isNullish(property.value)) {
546
+ continue;
547
+ }
548
+ context.report({
549
+ data: {
550
+ name
551
+ },
552
+ messageId: 'noNullishAttributeValue',
553
+ node: property.value
554
+ });
555
+ }
556
+ }
557
+ };
558
+ };
559
+
560
+ const noNullishAttributeValues = {
561
+ __proto__: null,
562
+ create: create$h,
563
+ meta: meta$h
564
+ };
565
+
566
+ const meta$g = {
407
567
  docs: {
408
568
  description: 'Disallow object-like attribute values in virtual-dom nodes'
409
569
  },
@@ -416,7 +576,7 @@ const ignoredProperties = new Set(['childCount', 'type']);
416
576
  const isObjectLikeAttributeValue = node => {
417
577
  return isObjectExpressionNode(node) || isArrayExpressionNode(node) || isArrowFunctionExpressionNode(node) || isFunctionExpressionNode(node);
418
578
  };
419
- const create$6 = context => {
579
+ const create$g = context => {
420
580
  return {
421
581
  ObjectExpression(node) {
422
582
  if (!isVirtualDomNode(node)) {
@@ -441,11 +601,45 @@ const create$6 = context => {
441
601
 
442
602
  const noObjectAttributeValues = {
443
603
  __proto__: null,
444
- create: create$6,
445
- meta: meta$6
604
+ create: create$g,
605
+ meta: meta$g
446
606
  };
447
607
 
448
- const meta$5 = {
608
+ const meta$f = {
609
+ docs: {
610
+ description: 'Disallow positive virtual-dom tabIndex values'
611
+ },
612
+ messages: {
613
+ noPositiveTabIndex: 'Use a `tabIndex` of 0 or -1 instead of a positive value.'
614
+ },
615
+ type: 'problem'
616
+ };
617
+ const create$f = context => {
618
+ return {
619
+ ObjectExpression(node) {
620
+ if (!isVirtualDomNode(node)) {
621
+ return;
622
+ }
623
+ const tabIndexProperty = getProperty(node, 'tabIndex');
624
+ const tabIndex = tabIndexProperty && getStaticNumberValue(tabIndexProperty.value);
625
+ if (!tabIndexProperty || tabIndex === undefined || tabIndex <= 0) {
626
+ return;
627
+ }
628
+ context.report({
629
+ messageId: 'noPositiveTabIndex',
630
+ node: tabIndexProperty.value
631
+ });
632
+ }
633
+ };
634
+ };
635
+
636
+ const noPositiveTabIndex = {
637
+ __proto__: null,
638
+ create: create$f,
639
+ meta: meta$f
640
+ };
641
+
642
+ const meta$e = {
449
643
  docs: {
450
644
  description: 'Disallow raw string children in virtual-dom arrays'
451
645
  },
@@ -457,7 +651,7 @@ const meta$5 = {
457
651
  const isVirtualDomArray = node => {
458
652
  return isArrayExpressionNode(node) && node.elements.some(element => isVirtualDomNode(element) || isTextCall(element));
459
653
  };
460
- const create$5 = context => {
654
+ const create$e = context => {
461
655
  return {
462
656
  ArrayExpression(node) {
463
657
  if (!isVirtualDomArray(node)) {
@@ -478,16 +672,93 @@ const create$5 = context => {
478
672
 
479
673
  const noRawTextChildren = {
480
674
  __proto__: null,
481
- create: create$5,
482
- meta: meta$5
675
+ create: create$e,
676
+ meta: meta$e
483
677
  };
484
678
 
485
- const meta$4 = {
679
+ const meta$d = {
680
+ docs: {
681
+ description: 'Require each virtual-dom event listener to call its dedicated handler'
682
+ },
683
+ messages: {
684
+ noSharedEventListenerHandler: 'Use the dedicated handler `{{name}}` as the first event listener parameter instead of `{{handler}}`.'
685
+ },
686
+ type: 'problem'
687
+ };
688
+ const create$d = context => {
689
+ return {
690
+ ObjectExpression(node) {
691
+ if (!isObjectExpressionNode(node)) {
692
+ return;
693
+ }
694
+ const nameProperty = getProperty(node, 'name');
695
+ const paramsProperty = getProperty(node, 'params');
696
+ if (!nameProperty || !paramsProperty || !isStringLiteral(nameProperty.value) || !isArrayExpressionNode(paramsProperty.value)) {
697
+ return;
698
+ }
699
+ const name = nameProperty.value.value;
700
+ const firstParameter = paramsProperty.value.elements[0];
701
+ if (!name.startsWith('handle') || !isStringLiteral(firstParameter) || firstParameter.value === name) {
702
+ return;
703
+ }
704
+ context.report({
705
+ data: {
706
+ handler: firstParameter.value,
707
+ name
708
+ },
709
+ messageId: 'noSharedEventListenerHandler',
710
+ node: firstParameter
711
+ });
712
+ }
713
+ };
714
+ };
715
+
716
+ const noSharedEventListenerHandlers = {
717
+ __proto__: null,
718
+ create: create$d,
719
+ meta: meta$d
720
+ };
721
+
722
+ const meta$c = {
723
+ docs: {
724
+ description: 'Prefer constants for single literal virtual-dom class names'
725
+ },
726
+ messages: {
727
+ preferClassNameConstant: 'Use a `ClassNames.*` constant instead of a literal `className`.'
728
+ },
729
+ type: 'suggestion'
730
+ };
731
+ const create$c = context => {
732
+ return {
733
+ ObjectExpression(node) {
734
+ if (!isVirtualDomNode(node)) {
735
+ return;
736
+ }
737
+ const classNameProperty = getProperty(node, 'className');
738
+ if (!classNameProperty || !isStringLiteral(classNameProperty.value) || classNameProperty.value.value === '' || /\s/.test(classNameProperty.value.value)) {
739
+ return;
740
+ }
741
+ context.report({
742
+ messageId: 'preferClassNameConstant',
743
+ node: classNameProperty.value
744
+ });
745
+ }
746
+ };
747
+ };
748
+
749
+ const preferClassNameConstants = {
750
+ __proto__: null,
751
+ create: create$c,
752
+ meta: meta$c
753
+ };
754
+
755
+ const meta$b = {
486
756
  docs: {
487
757
  description: 'Prefer virtual-dom constants over raw type, role, aria boolean, and tabIndex values'
488
758
  },
489
759
  messages: {
490
760
  preferAriaBooleanConstant: 'Use an ARIA boolean constant instead of a raw string.',
761
+ preferEventListenerConstant: 'Use a named event listener constant instead of a raw numeric id.',
491
762
  preferRoleConstant: 'Use `AriaRoles.*` instead of a raw role string.',
492
763
  preferTabIndexConstant: 'Use `TabIndex.*` instead of a raw tabIndex number.',
493
764
  preferTypeConstant: 'Use `VirtualDomElements.*` instead of a raw element type number.'
@@ -497,7 +768,7 @@ const meta$4 = {
497
768
  const isAriaBooleanProperty = name => {
498
769
  return /^aria[A-Z]/.test(name);
499
770
  };
500
- const create$4 = context => {
771
+ const create$b = context => {
501
772
  return {
502
773
  ObjectExpression(node) {
503
774
  if (!isVirtualDomNode(node)) {
@@ -532,6 +803,13 @@ const create$4 = context => {
532
803
  });
533
804
  continue;
534
805
  }
806
+ if (/^on[A-Z]/.test(name) && isNumericLiteral(property.value)) {
807
+ context.report({
808
+ messageId: 'preferEventListenerConstant',
809
+ node: property.value
810
+ });
811
+ continue;
812
+ }
535
813
  if (isAriaBooleanProperty(name) && isStringLiteral(property.value) && (property.value.value === 'true' || property.value.value === 'false')) {
536
814
  context.report({
537
815
  messageId: 'preferAriaBooleanConstant',
@@ -545,11 +823,11 @@ const create$4 = context => {
545
823
 
546
824
  const preferConstants = {
547
825
  __proto__: null,
548
- create: create$4,
549
- meta: meta$4
826
+ create: create$b,
827
+ meta: meta$b
550
828
  };
551
829
 
552
- const meta$3 = {
830
+ const meta$a = {
553
831
  docs: {
554
832
  description: 'Require Intl.DateTimeFormat instances to be created lazily'
555
833
  },
@@ -564,7 +842,7 @@ const isFunctionNode = node => {
564
842
  const isDateTimeFormat = node => {
565
843
  return node.type === 'MemberExpression' && !node.computed && node.object.type === 'Identifier' && node.object.name === 'Intl' && node.property.type === 'Identifier' && node.property.name === 'DateTimeFormat';
566
844
  };
567
- const create$3 = context => {
845
+ const create$a = context => {
568
846
  const isCreatedLazily = node => {
569
847
  return context.sourceCode.getAncestors(node).some(isFunctionNode);
570
848
  };
@@ -585,8 +863,8 @@ const create$3 = context => {
585
863
 
586
864
  const preferLazyDateTimeFormat = {
587
865
  __proto__: null,
588
- create: create$3,
589
- meta: meta$3
866
+ create: create$a,
867
+ meta: meta$a
590
868
  };
591
869
 
592
870
  const isClassNameKey = node => {
@@ -634,7 +912,7 @@ const getParent = node => {
634
912
  }
635
913
  return undefined;
636
914
  };
637
- const meta$2 = {
915
+ const meta$9 = {
638
916
  docs: {
639
917
  description: 'Prefer mergeClassNames for composing virtual-dom className values'
640
918
  },
@@ -643,7 +921,7 @@ const meta$2 = {
643
921
  },
644
922
  type: 'suggestion'
645
923
  };
646
- const create$2 = context => {
924
+ const create$9 = context => {
647
925
  const reportNamespacedClassNameComposition = node => {
648
926
  if (!isManualClassNameConcatenation(node) || !hasClassNamesReference(node)) {
649
927
  return;
@@ -674,11 +952,11 @@ const create$2 = context => {
674
952
 
675
953
  const preferMergeClassNames = {
676
954
  __proto__: null,
677
- create: create$2,
678
- meta: meta$2
955
+ create: create$9,
956
+ meta: meta$9
679
957
  };
680
958
 
681
- const meta$1 = {
959
+ const meta$8 = {
682
960
  docs: {
683
961
  description: 'Prefer destructuring state properties before use'
684
962
  },
@@ -705,7 +983,7 @@ const isWriteExpression = node => {
705
983
  }
706
984
  return parent.type === 'UnaryExpression' && parent.operator === 'delete' && parent.argument === node;
707
985
  };
708
- const create$1 = context => {
986
+ const create$8 = context => {
709
987
  return {
710
988
  MemberExpression(node) {
711
989
  if (node.computed) {
@@ -733,11 +1011,218 @@ const create$1 = context => {
733
1011
 
734
1012
  const preferStateDestructuring = {
735
1013
  __proto__: null,
736
- create: create$1,
737
- meta: meta$1
1014
+ create: create$8,
1015
+ meta: meta$8
738
1016
  };
739
1017
 
740
- const meta = {
1018
+ const meta$7 = {
1019
+ docs: {
1020
+ description: 'Require event listener options for handlers with specific browser event behavior'
1021
+ },
1022
+ messages: {
1023
+ requirePreventDefault: '`{{name}}` event listeners must set `preventDefault` to `true`.'
1024
+ },
1025
+ type: 'problem'
1026
+ };
1027
+ const listenersRequiringPreventDefault = new Set(['handleDragOver', 'handleDrop']);
1028
+ const create$7 = context => {
1029
+ return {
1030
+ ObjectExpression(node) {
1031
+ if (!isObjectExpressionNode(node) || !getProperty(node, 'params')) {
1032
+ return;
1033
+ }
1034
+ const nameProperty = getProperty(node, 'name');
1035
+ if (!nameProperty || !isStringLiteral(nameProperty.value) || !listenersRequiringPreventDefault.has(nameProperty.value.value)) {
1036
+ return;
1037
+ }
1038
+ const preventDefaultProperty = getProperty(node, 'preventDefault');
1039
+ if (preventDefaultProperty && isLiteralNode(preventDefaultProperty.value) && preventDefaultProperty.value.value === true) {
1040
+ return;
1041
+ }
1042
+ context.report({
1043
+ data: {
1044
+ name: nameProperty.value.value
1045
+ },
1046
+ messageId: 'requirePreventDefault',
1047
+ node: preventDefaultProperty ? preventDefaultProperty.value : nameProperty
1048
+ });
1049
+ }
1050
+ };
1051
+ };
1052
+
1053
+ const requireEventListenerOptions = {
1054
+ __proto__: null,
1055
+ create: create$7,
1056
+ meta: meta$7
1057
+ };
1058
+
1059
+ const meta$6 = {
1060
+ docs: {
1061
+ description: 'Require alt text on virtual-dom image nodes'
1062
+ },
1063
+ messages: {
1064
+ requireImageAlt: 'Add an `alt` attribute to this virtual-dom image.'
1065
+ },
1066
+ type: 'problem'
1067
+ };
1068
+ const create$6 = context => {
1069
+ return {
1070
+ ObjectExpression(node) {
1071
+ if (!isVirtualDomNode(node)) {
1072
+ return;
1073
+ }
1074
+ const typeProperty = getProperty(node, 'type');
1075
+ if (!typeProperty || getVirtualDomElementName(typeProperty.value) !== 'Img' || getProperty(node, 'alt')) {
1076
+ return;
1077
+ }
1078
+ context.report({
1079
+ messageId: 'requireImageAlt',
1080
+ node
1081
+ });
1082
+ }
1083
+ };
1084
+ };
1085
+
1086
+ const requireImageAlt = {
1087
+ __proto__: null,
1088
+ create: create$6,
1089
+ meta: meta$6
1090
+ };
1091
+
1092
+ const meta$5 = {
1093
+ docs: {
1094
+ description: 'Require secure attributes on virtual-dom links'
1095
+ },
1096
+ messages: {
1097
+ secureLinkRel: 'Set `rel` to `noopener noreferrer` on virtual-dom links.',
1098
+ secureLinkTarget: 'Set `target` to `_blank` on virtual-dom links.'
1099
+ },
1100
+ type: 'problem'
1101
+ };
1102
+ const hasStringPropertyValue = (node, name, value) => {
1103
+ const property = getProperty(node, name);
1104
+ return Boolean(property && isStringLiteral(property.value) && property.value.value === value);
1105
+ };
1106
+ const create$5 = context => {
1107
+ return {
1108
+ ObjectExpression(node) {
1109
+ if (!isVirtualDomNode(node)) {
1110
+ return;
1111
+ }
1112
+ const typeProperty = getProperty(node, 'type');
1113
+ const hrefProperty = getProperty(node, 'href');
1114
+ if (!typeProperty || !hrefProperty || !isMemberExpressionWithProperty(typeProperty.value, 'A')) {
1115
+ return;
1116
+ }
1117
+ if (!hasStringPropertyValue(node, 'target', '_blank')) {
1118
+ context.report({
1119
+ messageId: 'secureLinkTarget',
1120
+ node
1121
+ });
1122
+ }
1123
+ if (!hasStringPropertyValue(node, 'rel', 'noopener noreferrer')) {
1124
+ context.report({
1125
+ messageId: 'secureLinkRel',
1126
+ node
1127
+ });
1128
+ }
1129
+ }
1130
+ };
1131
+ };
1132
+
1133
+ const secureLinks = {
1134
+ __proto__: null,
1135
+ create: create$5,
1136
+ meta: meta$5
1137
+ };
1138
+
1139
+ const validAriaProperties = new Set(['ariaActivedescendant', 'ariaAtomic', 'ariaAutoComplete', 'ariaBrailleLabel', 'ariaBrailleRoleDescription', 'ariaBusy', 'ariaChecked', 'ariaColCount', 'ariaColIndex', 'ariaColIndexText', 'ariaColSpan', 'ariaControls', 'ariaCurrent', 'ariaDescribedBy', 'ariaDescription', 'ariaDetails', 'ariaDisabled', 'ariaErrorMessage', 'ariaExpanded', 'ariaFlowTo', 'ariaHasPopup', 'ariaHidden', 'ariaInvalid', 'ariaKeyShortcuts', 'ariaLabel', 'ariaLabelledBy', 'ariaLevel', 'ariaLive', 'ariaModal', 'ariaMultiLine', 'ariaMultiSelectable', 'ariaOrientation', 'ariaOwns', 'ariaPlaceholder', 'ariaPosInSet', 'ariaPressed', 'ariaReadOnly', 'ariaRelevant', 'ariaRequired', 'ariaRoleDescription', 'ariaRowCount', 'ariaRowIndex', 'ariaRowIndexText', 'ariaRowSpan', 'ariaSelected', 'ariaSetSize', 'ariaSort', 'ariaValueMax', 'ariaValueMin', 'ariaValueNow', 'ariaValueText']);
1140
+ const meta$4 = {
1141
+ docs: {
1142
+ description: 'Validate virtual-dom ARIA property names'
1143
+ },
1144
+ messages: {
1145
+ validAriaProperty: '`{{name}}` is not a valid virtual-dom ARIA property.'
1146
+ },
1147
+ type: 'problem'
1148
+ };
1149
+ const create$4 = context => {
1150
+ return {
1151
+ ObjectExpression(node) {
1152
+ if (!isVirtualDomNode(node)) {
1153
+ return;
1154
+ }
1155
+ for (const property of node.properties) {
1156
+ if (!isPropertyNode(property)) {
1157
+ continue;
1158
+ }
1159
+ const name = getStaticPropertyName(property);
1160
+ if (!name?.startsWith('aria') || validAriaProperties.has(name)) {
1161
+ continue;
1162
+ }
1163
+ context.report({
1164
+ data: {
1165
+ name
1166
+ },
1167
+ messageId: 'validAriaProperty',
1168
+ node: property
1169
+ });
1170
+ }
1171
+ }
1172
+ };
1173
+ };
1174
+
1175
+ const validAriaProperties$1 = {
1176
+ __proto__: null,
1177
+ create: create$4,
1178
+ meta: meta$4
1179
+ };
1180
+
1181
+ const allowedValues = new Map([['ariaAtomic', new Set(['false', 'true'])], ['ariaAutoComplete', new Set(['both', 'inline', 'list', 'none'])], ['ariaBusy', new Set(['false', 'true'])], ['ariaChecked', new Set(['false', 'mixed', 'true'])], ['ariaCurrent', new Set(['date', 'false', 'location', 'page', 'step', 'time', 'true'])], ['ariaDisabled', new Set(['false', 'true'])], ['ariaExpanded', new Set(['false', 'true'])], ['ariaHasPopup', new Set(['dialog', 'false', 'grid', 'listbox', 'menu', 'tree', 'true'])], ['ariaHidden', new Set(['false', 'true'])], ['ariaInvalid', new Set(['false', 'grammar', 'spelling', 'true'])], ['ariaLive', new Set(['assertive', 'off', 'polite'])], ['ariaModal', new Set(['false', 'true'])], ['ariaMultiLine', new Set(['false', 'true'])], ['ariaMultiSelectable', new Set(['false', 'true'])], ['ariaOrientation', new Set(['horizontal', 'undefined', 'vertical'])], ['ariaPressed', new Set(['false', 'mixed', 'true'])], ['ariaReadOnly', new Set(['false', 'true'])], ['ariaRelevant', new Set(['additions', 'additions removals', 'additions text', 'all', 'removals', 'removals additions', 'removals text', 'text', 'text additions', 'text removals'])], ['ariaRequired', new Set(['false', 'true'])], ['ariaSelected', new Set(['false', 'true'])], ['ariaSort', new Set(['ascending', 'descending', 'none', 'other'])]]);
1182
+ const meta$3 = {
1183
+ docs: {
1184
+ description: 'Validate literal virtual-dom ARIA values'
1185
+ },
1186
+ messages: {
1187
+ validAriaValue: '`{{value}}` is not a valid value for `{{name}}`.'
1188
+ },
1189
+ type: 'problem'
1190
+ };
1191
+ const create$3 = context => {
1192
+ return {
1193
+ ObjectExpression(node) {
1194
+ if (!isVirtualDomNode(node)) {
1195
+ return;
1196
+ }
1197
+ for (const property of node.properties) {
1198
+ if (!isPropertyNode(property)) {
1199
+ continue;
1200
+ }
1201
+ const name = getStaticPropertyName(property);
1202
+ const values = name && allowedValues.get(name);
1203
+ if (!name || !values || !isStringLiteral(property.value) || values.has(property.value.value)) {
1204
+ continue;
1205
+ }
1206
+ context.report({
1207
+ data: {
1208
+ name,
1209
+ value: property.value.value
1210
+ },
1211
+ messageId: 'validAriaValue',
1212
+ node: property.value
1213
+ });
1214
+ }
1215
+ }
1216
+ };
1217
+ };
1218
+
1219
+ const validAriaValues = {
1220
+ __proto__: null,
1221
+ create: create$3,
1222
+ meta: meta$3
1223
+ };
1224
+
1225
+ const meta$2 = {
741
1226
  docs: {
742
1227
  description: 'Validate statically analyzable virtual-dom childCount values'
743
1228
  },
@@ -812,7 +1297,7 @@ const hasImpossibleChildCount = (elements, index) => {
812
1297
  }
813
1298
  return false;
814
1299
  };
815
- const create = context => {
1300
+ const create$2 = context => {
816
1301
  return {
817
1302
  ArrayExpression(node) {
818
1303
  if (!isArrayExpressionNode(node) || !node.elements.some(isVirtualDomNode) || !node.elements.every(isAnalyzableVirtualDomElement)) {
@@ -841,6 +1326,103 @@ const create = context => {
841
1326
  };
842
1327
 
843
1328
  const validChildCount = {
1329
+ __proto__: null,
1330
+ create: create$2,
1331
+ meta: meta$2
1332
+ };
1333
+
1334
+ const supportedEventProperties = new Set(['onBeforeInput', 'onBlur', 'onChange', 'onClick', 'onContextMenu', 'onDblClick', 'onDragEnd', 'onDragEnter', 'onDragLeave', 'onDragOver', 'onDragStart', 'onDrop', 'onFocus', 'onFocusIn', 'onFocusOut', 'onInput', 'onKeyDown', 'onKeyUp', 'onMouseDown', 'onMouseMove', 'onMouseOut', 'onMouseOver', 'onMouseUp', 'onPointerDown', 'onPointerMove', 'onPointerOut', 'onPointerOver', 'onScroll', 'onSelectionChange', 'onSubmit', 'onWheel']);
1335
+ const meta$1 = {
1336
+ docs: {
1337
+ description: 'Require virtual-dom event properties supported by the renderer'
1338
+ },
1339
+ messages: {
1340
+ validEventProperty: '`{{name}}` is not a supported virtual-dom event property.'
1341
+ },
1342
+ type: 'problem'
1343
+ };
1344
+ const create$1 = context => {
1345
+ return {
1346
+ ObjectExpression(node) {
1347
+ if (!isVirtualDomNode(node)) {
1348
+ return;
1349
+ }
1350
+ for (const property of node.properties) {
1351
+ if (!isPropertyNode(property)) {
1352
+ continue;
1353
+ }
1354
+ const name = getStaticPropertyName(property);
1355
+ if (!name?.startsWith('on') || supportedEventProperties.has(name)) {
1356
+ continue;
1357
+ }
1358
+ context.report({
1359
+ data: {
1360
+ name
1361
+ },
1362
+ messageId: 'validEventProperty',
1363
+ node: property
1364
+ });
1365
+ }
1366
+ }
1367
+ };
1368
+ };
1369
+
1370
+ const validEventProperties = {
1371
+ __proto__: null,
1372
+ create: create$1,
1373
+ meta: meta$1
1374
+ };
1375
+
1376
+ const voidElements = new Set(['Br', 'Col', 'Hr', 'Img', 'Input']);
1377
+ const meta = {
1378
+ docs: {
1379
+ description: 'Require canonical virtual-dom node shapes'
1380
+ },
1381
+ messages: {
1382
+ elementTextProperty: 'Use a `text(...)` child instead of a `text` property on an element node.',
1383
+ missingChildCount: 'Add an explicit `childCount` to this virtual-dom node.',
1384
+ voidElementChildren: '`VirtualDomElements.{{name}}` must have a `childCount` of 0.'
1385
+ },
1386
+ type: 'problem'
1387
+ };
1388
+ const create = context => {
1389
+ return {
1390
+ ObjectExpression(node) {
1391
+ if (!isObjectExpressionNode(node)) {
1392
+ return;
1393
+ }
1394
+ const typeProperty = getProperty(node, 'type');
1395
+ const elementName = typeProperty && getVirtualDomElementName(typeProperty.value);
1396
+ if (!typeProperty || !elementName) {
1397
+ return;
1398
+ }
1399
+ const childCountProperty = getProperty(node, 'childCount');
1400
+ if (!childCountProperty) {
1401
+ context.report({
1402
+ messageId: 'missingChildCount',
1403
+ node
1404
+ });
1405
+ } else if (voidElements.has(elementName) && getStaticNumberValue(childCountProperty.value) !== 0) {
1406
+ context.report({
1407
+ data: {
1408
+ name: elementName
1409
+ },
1410
+ messageId: 'voidElementChildren',
1411
+ node: childCountProperty
1412
+ });
1413
+ }
1414
+ const textProperty = getProperty(node, 'text');
1415
+ if (textProperty && elementName !== 'Text') {
1416
+ context.report({
1417
+ messageId: 'elementTextProperty',
1418
+ node: textProperty
1419
+ });
1420
+ }
1421
+ }
1422
+ };
1423
+ };
1424
+
1425
+ const validNodeShape = {
844
1426
  __proto__: null,
845
1427
  create,
846
1428
  meta
@@ -853,19 +1435,32 @@ const plugin = {
853
1435
  version: '0.0.1'
854
1436
  },
855
1437
  rules: {
1438
+ 'accessible-control-name': accessibleControlName,
856
1439
  'clickable-div-needs-role': clickableDivNeedsRole,
857
1440
  'hoist-static-nodes': hoistStaticNodes,
1441
+ 'no-conditional-spread': noConditionalSpread,
858
1442
  'no-empty-aria': noEmptyAria,
859
1443
  'no-empty-class-name': noEmptyClassName,
860
1444
  'no-inline-event-handlers': noInlineEventHandlers,
861
1445
  'no-inline-style': noInlineStyle,
1446
+ 'no-nullish-attribute-values': noNullishAttributeValues,
862
1447
  'no-object-attribute-values': noObjectAttributeValues,
1448
+ 'no-positive-tab-index': noPositiveTabIndex,
863
1449
  'no-raw-text-children': noRawTextChildren,
1450
+ 'no-shared-event-listener-handlers': noSharedEventListenerHandlers,
1451
+ 'prefer-class-name-constants': preferClassNameConstants,
864
1452
  'prefer-constants': preferConstants,
865
1453
  'prefer-lazy-date-time-format': preferLazyDateTimeFormat,
866
1454
  'prefer-merge-class-names': preferMergeClassNames,
867
1455
  'prefer-state-destructuring': preferStateDestructuring,
868
- 'valid-child-count': validChildCount
1456
+ 'require-event-listener-options': requireEventListenerOptions,
1457
+ 'require-image-alt': requireImageAlt,
1458
+ 'secure-links': secureLinks,
1459
+ 'valid-aria-properties': validAriaProperties$1,
1460
+ 'valid-aria-values': validAriaValues,
1461
+ 'valid-child-count': validChildCount,
1462
+ 'valid-event-properties': validEventProperties,
1463
+ 'valid-node-shape': validNodeShape
869
1464
  }
870
1465
  };
871
1466
  const recommended = [{
@@ -876,16 +1471,20 @@ const recommended = [{
876
1471
  rules: {
877
1472
  'virtual-dom/clickable-div-needs-role': 'error',
878
1473
  'virtual-dom/hoist-static-nodes': 'error',
1474
+ 'virtual-dom/no-conditional-spread': 'error',
879
1475
  'virtual-dom/no-empty-aria': 'error',
880
1476
  'virtual-dom/no-empty-class-name': 'error',
881
1477
  'virtual-dom/no-inline-event-handlers': 'error',
882
1478
  'virtual-dom/no-inline-style': 'error',
883
1479
  'virtual-dom/no-object-attribute-values': 'error',
884
1480
  'virtual-dom/no-raw-text-children': 'error',
1481
+ 'virtual-dom/no-shared-event-listener-handlers': 'error',
885
1482
  'virtual-dom/prefer-constants': 'error',
886
1483
  'virtual-dom/prefer-lazy-date-time-format': 'error',
887
1484
  'virtual-dom/prefer-merge-class-names': 'error',
888
1485
  'virtual-dom/prefer-state-destructuring': 'error',
1486
+ 'virtual-dom/require-event-listener-options': 'error',
1487
+ 'virtual-dom/secure-links': 'error',
889
1488
  'virtual-dom/valid-child-count': 'error'
890
1489
  }
891
1490
  }, {
@@ -894,5 +1493,19 @@ const recommended = [{
894
1493
  'virtual-dom/hoist-static-nodes': 'off'
895
1494
  }
896
1495
  }];
1496
+ const strict = [...recommended, {
1497
+ files: ['**/*.{js,mjs,cjs,ts,mts,cts}'],
1498
+ rules: {
1499
+ 'virtual-dom/accessible-control-name': 'error',
1500
+ 'virtual-dom/no-nullish-attribute-values': 'error',
1501
+ 'virtual-dom/no-positive-tab-index': 'error',
1502
+ 'virtual-dom/prefer-class-name-constants': 'error',
1503
+ 'virtual-dom/require-image-alt': 'error',
1504
+ 'virtual-dom/valid-aria-properties': 'error',
1505
+ 'virtual-dom/valid-aria-values': 'error',
1506
+ 'virtual-dom/valid-event-properties': 'error',
1507
+ 'virtual-dom/valid-node-shape': 'error'
1508
+ }
1509
+ }];
897
1510
 
898
- export { recommended as default };
1511
+ export { recommended as default, strict };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/eslint-plugin-virtual-dom",
3
- "version": "17.1.0",
3
+ "version": "17.3.0",
4
4
  "main": "dist/index.js",
5
5
  "repository": {
6
6
  "type": "git",