@agilebot/eslint-plugin 0.2.6 → 0.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 +1142 -1142
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license @agilebot/eslint-plugin v0.2.6
2
+ * @license @agilebot/eslint-plugin v0.3.0
3
3
  *
4
4
  * Copyright (c) Agilebot, Inc. and its affiliates.
5
5
  *
@@ -521,244 +521,524 @@ var noDefault = {
521
521
  }
522
522
  };
523
523
 
524
- var betterExhaustiveDeps = {
525
- meta: {
526
- type: 'suggestion',
527
- docs: {
528
- description:
529
- 'verifies the list of dependencies for Hooks like useEffect and similar',
530
- recommended: true,
531
- url: 'https://github.com/facebook/react/issues/14920'
532
- },
533
- fixable: 'code',
534
- hasSuggestions: true,
535
- schema: [
524
+ function getBasicIdentifier(node) {
525
+ if (node.type === 'Identifier') {
526
+ return node.name;
527
+ }
528
+ if (node.type === 'Literal') {
529
+ return node.value;
530
+ }
531
+ if (node.type === 'TemplateLiteral') {
532
+ if (node.expressions.length > 0) {
533
+ return null;
534
+ }
535
+ return node.quasis[0].value.raw;
536
+ }
537
+ return null;
538
+ }
539
+ function getBaseIdentifier(node) {
540
+ switch (node.type) {
541
+ case 'Identifier': {
542
+ return node;
543
+ }
544
+ case 'CallExpression': {
545
+ return getBaseIdentifier(node.callee);
546
+ }
547
+ case 'MemberExpression': {
548
+ return getBaseIdentifier(node.object);
549
+ }
550
+ }
551
+ return null;
552
+ }
553
+ function getStyesObj(node) {
554
+ const isMakeStyles = node.callee.name === 'makeStyles';
555
+ const isModernApi =
556
+ node.callee.type === 'MemberExpression' &&
557
+ node.callee.property.name === 'create' &&
558
+ getBaseIdentifier(node.callee.object) &&
559
+ getBaseIdentifier(node.callee.object).name === 'tss';
560
+ if (!isMakeStyles && !isModernApi) {
561
+ return;
562
+ }
563
+ const styles = (() => {
564
+ if (isMakeStyles) {
565
+ return node.parent.arguments[0];
566
+ }
567
+ if (isModernApi) {
568
+ return node.callee.parent.arguments[0];
569
+ }
570
+ })();
571
+ if (!styles) {
572
+ return;
573
+ }
574
+ switch (styles.type) {
575
+ case 'ObjectExpression':
576
+ return styles;
577
+ case 'ArrowFunctionExpression':
536
578
  {
537
- type: 'object',
538
- additionalProperties: false,
539
- enableDangerousAutofixThisMayCauseInfiniteLoops: false,
540
- properties: {
541
- additionalHooks: {
542
- type: 'string'
543
- },
544
- enableDangerousAutofixThisMayCauseInfiniteLoops: {
545
- type: 'boolean'
546
- },
547
- staticHooks: {
548
- type: 'object',
549
- additionalProperties: {
550
- oneOf: [
551
- {
552
- type: 'boolean'
553
- },
554
- {
555
- type: 'array',
556
- items: {
557
- type: 'boolean'
558
- }
559
- },
560
- {
561
- type: 'object',
562
- additionalProperties: {
563
- type: 'boolean'
564
- }
565
- }
566
- ]
567
- }
568
- },
569
- checkMemoizedVariableIsStatic: {
570
- type: 'boolean'
579
+ const { body } = styles;
580
+ switch (body.type) {
581
+ case 'ObjectExpression':
582
+ return body;
583
+ case 'BlockStatement': {
584
+ let stylesObj;
585
+ body.body.forEach(bodyNode => {
586
+ if (
587
+ bodyNode.type === 'ReturnStatement' &&
588
+ bodyNode.argument.type === 'ObjectExpression'
589
+ ) {
590
+ stylesObj = bodyNode.argument;
591
+ }
592
+ });
593
+ return stylesObj;
571
594
  }
572
595
  }
573
596
  }
574
- ]
597
+ break;
598
+ }
599
+ }
600
+
601
+ var classNaming = {
602
+ meta: {
603
+ type: 'problem'
575
604
  },
576
- create(context) {
577
- const additionalHooks =
578
- context.options &&
579
- context.options[0] &&
580
- context.options[0].additionalHooks
581
- ? new RegExp(context.options[0].additionalHooks)
582
- : undefined;
583
- const enableDangerousAutofixThisMayCauseInfiniteLoops =
584
- (context.options &&
585
- context.options[0] &&
586
- context.options[0].enableDangerousAutofixThisMayCauseInfiniteLoops) ||
587
- false;
588
- const staticHooks =
589
- (context.options &&
590
- context.options[0] &&
591
- context.options[0].staticHooks) ||
592
- {};
593
- const checkMemoizedVariableIsStatic =
594
- (context.options &&
595
- context.options[0] &&
596
- context.options[0].checkMemoizedVariableIsStatic) ||
597
- false;
598
- const options = {
599
- additionalHooks,
600
- enableDangerousAutofixThisMayCauseInfiniteLoops,
601
- staticHooks,
602
- checkMemoizedVariableIsStatic
603
- };
604
- function reportProblem(problem) {
605
- if (
606
- enableDangerousAutofixThisMayCauseInfiniteLoops &&
607
- Array.isArray(problem.suggest) &&
608
- problem.suggest.length > 0
609
- ) {
610
- problem.fix = problem.suggest[0].fix;
611
- }
612
- context.report(problem);
613
- }
614
- const scopeManager = context.getSourceCode().scopeManager;
615
- const setStateCallSites = new WeakMap();
616
- const stateVariables = new WeakSet();
617
- const stableKnownValueCache = new WeakMap();
618
- const functionWithoutCapturedValueCache = new WeakMap();
619
- function memoizeWithWeakMap(fn, map) {
620
- return function (arg) {
621
- if (map.has(arg)) {
622
- return map.get(arg);
605
+ create: function rule(context) {
606
+ return {
607
+ CallExpression(node) {
608
+ const stylesObj = getStyesObj(node);
609
+ if (stylesObj === undefined) {
610
+ return;
623
611
  }
624
- const result = fn(arg);
625
- map.set(arg, result);
626
- return result;
627
- };
628
- }
629
- function visitFunctionWithDependencies(
630
- node,
631
- declaredDependenciesNode,
632
- reactiveHook,
633
- reactiveHookName,
634
- isEffect
635
- ) {
636
- if (isEffect && node.async) {
637
- reportProblem({
638
- node: node,
639
- message:
640
- `Effect callbacks are synchronous to prevent race conditions. ` +
641
- `Put the async function inside:\n\n` +
642
- 'useEffect(() => {\n' +
643
- ' async function fetchData() {\n' +
644
- ' // You can await here\n' +
645
- ' const response = await MyAPI.getData(someId);\n' +
646
- ' // ...\n' +
647
- ' }\n' +
648
- ' fetchData();\n' +
649
- `}, [someId]); // Or [] if effect doesn't need props or state\n\n` +
650
- 'Learn more about data fetching with Hooks: https://reactjs.org/link/hooks-data-fetching'
612
+ stylesObj.properties.forEach(property => {
613
+ if (property.computed) {
614
+ return;
615
+ }
616
+ if (
617
+ property.type === 'ExperimentalSpreadProperty' ||
618
+ property.type === 'SpreadElement'
619
+ ) {
620
+ return;
621
+ }
622
+ const className = property.key.value || property.key.name;
623
+ if (!eslintUtils.isCamelCase(className)) {
624
+ context.report({
625
+ node: property,
626
+ message: `Class \`${className}\` must be camelCase in TSS.`
627
+ });
628
+ }
651
629
  });
652
630
  }
653
- const scope = scopeManager.acquire(node);
654
- const pureScopes = new Set();
655
- let componentScope = null;
656
- {
657
- let currentScope = scope.upper;
658
- while (currentScope) {
659
- pureScopes.add(currentScope);
660
- if (currentScope.type === 'function') {
661
- break;
662
- }
663
- currentScope = currentScope.upper;
664
- }
665
- if (!currentScope) {
631
+ };
632
+ }
633
+ };
634
+
635
+ var noColorValue = {
636
+ meta: {
637
+ type: 'problem',
638
+ docs: {
639
+ description:
640
+ 'Enforce the use of color variables instead of color codes within TSS'
641
+ }
642
+ },
643
+ create: function (context) {
644
+ const parserOptions = context.parserOptions;
645
+ if (!parserOptions || !parserOptions.project) {
646
+ return {};
647
+ }
648
+ return {
649
+ CallExpression(node) {
650
+ const stylesObj = getStyesObj(node);
651
+ if (!stylesObj) {
666
652
  return;
667
653
  }
668
- componentScope = currentScope;
669
- }
670
- const isArray = Array.isArray;
671
- const memoizedIsStableKnownHookValue = memoizeWithWeakMap(
672
- isStableKnownHookValue,
673
- stableKnownValueCache
674
- );
675
- const memoizedIsFunctionWithoutCapturedValues = memoizeWithWeakMap(
676
- isFunctionWithoutCapturedValues,
677
- functionWithoutCapturedValueCache
678
- );
679
- function isStableKnownHookValue(resolved) {
680
- if (!isArray(resolved.defs)) {
681
- return false;
654
+ function checkColorLiteral(value) {
655
+ if (value.type === 'Literal' && typeof value.value === 'string') {
656
+ const colorCodePattern =
657
+ /#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\?\(\s*(\d{1,3}\s*,\s*){2}\d{1,3}(?:\s*,\s*\d*(?:\.\d+)?)?\s*\)/g;
658
+ const isColorCode = colorCodePattern.test(value.value);
659
+ if (isColorCode) {
660
+ context.report({
661
+ node: value,
662
+ message: 'Use color variables instead of color codes in TSS.'
663
+ });
664
+ }
665
+ }
682
666
  }
683
- const def = resolved.defs[0];
684
- if (def == null) {
685
- return false;
667
+ function loopStylesObj(obj) {
668
+ if (obj && obj.type === 'ObjectExpression') {
669
+ obj.properties.forEach(property => {
670
+ if (property.type === 'Property' && property.value) {
671
+ if (property.value.type === 'ObjectExpression') {
672
+ loopStylesObj(property.value);
673
+ } else {
674
+ checkColorLiteral(property.value);
675
+ }
676
+ }
677
+ });
678
+ }
686
679
  }
687
- if (def.node.type !== 'VariableDeclarator') {
688
- return false;
680
+ loopStylesObj(stylesObj);
681
+ }
682
+ };
683
+ }
684
+ };
685
+
686
+ var unusedClasses = {
687
+ meta: {
688
+ type: 'problem'
689
+ },
690
+ create: function rule(context) {
691
+ const usedClasses = {};
692
+ const definedClasses = {};
693
+ return {
694
+ CallExpression(node) {
695
+ const stylesObj = getStyesObj(node);
696
+ if (stylesObj === undefined) {
697
+ return;
689
698
  }
690
- let init = def.node.init;
691
- if (init == null) {
692
- return false;
699
+ stylesObj.properties.forEach(property => {
700
+ if (property.computed) {
701
+ return;
702
+ }
703
+ if (
704
+ property.type === 'ExperimentalSpreadProperty' ||
705
+ property.type === 'SpreadElement'
706
+ ) {
707
+ return;
708
+ }
709
+ definedClasses[property.key.value || property.key.name] = property;
710
+ });
711
+ },
712
+ MemberExpression(node) {
713
+ if (
714
+ node.object.type === 'Identifier' &&
715
+ node.object.name === 'classes'
716
+ ) {
717
+ const whichClass = getBasicIdentifier(node.property);
718
+ if (whichClass) {
719
+ usedClasses[whichClass] = true;
720
+ }
721
+ return;
693
722
  }
694
- while (init.type === 'TSAsExpression') {
695
- init = init.expression;
723
+ const classIdentifier = getBasicIdentifier(node.property);
724
+ if (!classIdentifier) {
725
+ return;
696
726
  }
697
- let declaration = def.node.parent;
698
- if (declaration == null) {
699
- fastFindReferenceWithParent(componentScope.block, def.node.id);
700
- declaration = def.node.parent;
701
- if (declaration == null) {
702
- return false;
703
- }
727
+ if (classIdentifier !== 'classes') {
728
+ return;
729
+ }
730
+ const { parent } = node;
731
+ if (parent.type !== 'MemberExpression') {
732
+ return;
704
733
  }
705
734
  if (
706
- declaration.kind === 'const' &&
707
- init.type === 'Literal' &&
708
- (typeof init.value === 'string' ||
709
- typeof init.value === 'number' ||
710
- init.value == null)
735
+ node.object.object &&
736
+ node.object.object.type !== 'ThisExpression'
711
737
  ) {
712
- return true;
738
+ return;
713
739
  }
714
- if (init.type !== 'CallExpression') {
715
- return false;
740
+ const propsIdentifier = getBasicIdentifier(parent.object);
741
+ if (propsIdentifier && propsIdentifier !== 'props') {
742
+ return;
716
743
  }
717
- let callee = init.callee;
744
+ if (!propsIdentifier && parent.object.type !== 'MemberExpression') {
745
+ return;
746
+ }
747
+ if (parent.parent.type === 'MemberExpression') {
748
+ return;
749
+ }
750
+ const parentClassIdentifier = getBasicIdentifier(parent.property);
751
+ if (parentClassIdentifier) {
752
+ usedClasses[parentClassIdentifier] = true;
753
+ }
754
+ },
755
+ 'Program:exit': () => {
756
+ Object.keys(definedClasses).forEach(definedClassKey => {
757
+ if (!usedClasses[definedClassKey]) {
758
+ context.report({
759
+ node: definedClasses[definedClassKey],
760
+ message: `Class \`${definedClassKey}\` is unused`
761
+ });
762
+ }
763
+ });
764
+ }
765
+ };
766
+ }
767
+ };
768
+
769
+ var noUnnecessaryTemplateLiterals = {
770
+ meta: {
771
+ type: 'problem',
772
+ docs: {
773
+ description: 'Check if a template string contains only one ${}',
774
+ recommended: true
775
+ },
776
+ fixable: 'code',
777
+ schema: []
778
+ },
779
+ create(context) {
780
+ return {
781
+ TemplateLiteral(node) {
782
+ const code = context.sourceCode.getText(node);
718
783
  if (
719
- callee.type === 'MemberExpression' &&
720
- callee.object.name === 'React' &&
721
- callee.property != null &&
722
- !callee.computed
784
+ code.startsWith('`${') &&
785
+ code.endsWith('}`') &&
786
+ code.split('${').length === 2
723
787
  ) {
724
- callee = callee.property;
725
- }
726
- if (callee.type !== 'Identifier') {
727
- return false;
788
+ context.report({
789
+ node,
790
+ message: 'Unnecessary template string with only one ${}.',
791
+ fix(fixer) {
792
+ return fixer.replaceText(
793
+ node,
794
+ code.substring(3, code.length - 2)
795
+ );
796
+ }
797
+ });
728
798
  }
729
- const id = def.node.id;
730
- const { name } = callee;
731
- if (name === 'useRef' && id.type === 'Identifier') {
732
- return true;
733
- } else if (name === 'useState' || name === 'useReducer') {
734
- if (
735
- id.type === 'ArrayPattern' &&
736
- id.elements.length === 2 &&
737
- isArray(resolved.identifiers)
738
- ) {
739
- if (id.elements[1] === resolved.identifiers[0]) {
740
- if (name === 'useState') {
741
- const references = resolved.references;
742
- let writeCount = 0;
743
- for (const reference of references) {
744
- if (reference.isWrite()) {
745
- writeCount++;
799
+ }
800
+ };
801
+ }
802
+ };
803
+
804
+ var betterExhaustiveDeps = {
805
+ meta: {
806
+ type: 'suggestion',
807
+ docs: {
808
+ description:
809
+ 'verifies the list of dependencies for Hooks like useEffect and similar',
810
+ recommended: true,
811
+ url: 'https://github.com/facebook/react/issues/14920'
812
+ },
813
+ fixable: 'code',
814
+ hasSuggestions: true,
815
+ schema: [
816
+ {
817
+ type: 'object',
818
+ additionalProperties: false,
819
+ enableDangerousAutofixThisMayCauseInfiniteLoops: false,
820
+ properties: {
821
+ additionalHooks: {
822
+ type: 'string'
823
+ },
824
+ enableDangerousAutofixThisMayCauseInfiniteLoops: {
825
+ type: 'boolean'
826
+ },
827
+ staticHooks: {
828
+ type: 'object',
829
+ additionalProperties: {
830
+ oneOf: [
831
+ {
832
+ type: 'boolean'
833
+ },
834
+ {
835
+ type: 'array',
836
+ items: {
837
+ type: 'boolean'
746
838
  }
747
- if (writeCount > 1) {
748
- return false;
839
+ },
840
+ {
841
+ type: 'object',
842
+ additionalProperties: {
843
+ type: 'boolean'
749
844
  }
750
- setStateCallSites.set(reference.identifier, id.elements[0]);
751
- }
752
- }
753
- return true;
754
- } else if (id.elements[0] === resolved.identifiers[0]) {
755
- if (name === 'useState') {
756
- const references = resolved.references;
757
- for (const reference of references) {
758
- stateVariables.add(reference.identifier);
759
845
  }
760
- }
761
- return false;
846
+ ]
847
+ }
848
+ },
849
+ checkMemoizedVariableIsStatic: {
850
+ type: 'boolean'
851
+ }
852
+ }
853
+ }
854
+ ]
855
+ },
856
+ create(context) {
857
+ const additionalHooks =
858
+ context.options &&
859
+ context.options[0] &&
860
+ context.options[0].additionalHooks
861
+ ? new RegExp(context.options[0].additionalHooks)
862
+ : undefined;
863
+ const enableDangerousAutofixThisMayCauseInfiniteLoops =
864
+ (context.options &&
865
+ context.options[0] &&
866
+ context.options[0].enableDangerousAutofixThisMayCauseInfiniteLoops) ||
867
+ false;
868
+ const staticHooks =
869
+ (context.options &&
870
+ context.options[0] &&
871
+ context.options[0].staticHooks) ||
872
+ {};
873
+ const checkMemoizedVariableIsStatic =
874
+ (context.options &&
875
+ context.options[0] &&
876
+ context.options[0].checkMemoizedVariableIsStatic) ||
877
+ false;
878
+ const options = {
879
+ additionalHooks,
880
+ enableDangerousAutofixThisMayCauseInfiniteLoops,
881
+ staticHooks,
882
+ checkMemoizedVariableIsStatic
883
+ };
884
+ function reportProblem(problem) {
885
+ if (
886
+ enableDangerousAutofixThisMayCauseInfiniteLoops &&
887
+ Array.isArray(problem.suggest) &&
888
+ problem.suggest.length > 0
889
+ ) {
890
+ problem.fix = problem.suggest[0].fix;
891
+ }
892
+ context.report(problem);
893
+ }
894
+ const scopeManager = context.getSourceCode().scopeManager;
895
+ const setStateCallSites = new WeakMap();
896
+ const stateVariables = new WeakSet();
897
+ const stableKnownValueCache = new WeakMap();
898
+ const functionWithoutCapturedValueCache = new WeakMap();
899
+ function memoizeWithWeakMap(fn, map) {
900
+ return function (arg) {
901
+ if (map.has(arg)) {
902
+ return map.get(arg);
903
+ }
904
+ const result = fn(arg);
905
+ map.set(arg, result);
906
+ return result;
907
+ };
908
+ }
909
+ function visitFunctionWithDependencies(
910
+ node,
911
+ declaredDependenciesNode,
912
+ reactiveHook,
913
+ reactiveHookName,
914
+ isEffect
915
+ ) {
916
+ if (isEffect && node.async) {
917
+ reportProblem({
918
+ node: node,
919
+ message:
920
+ `Effect callbacks are synchronous to prevent race conditions. ` +
921
+ `Put the async function inside:\n\n` +
922
+ 'useEffect(() => {\n' +
923
+ ' async function fetchData() {\n' +
924
+ ' // You can await here\n' +
925
+ ' const response = await MyAPI.getData(someId);\n' +
926
+ ' // ...\n' +
927
+ ' }\n' +
928
+ ' fetchData();\n' +
929
+ `}, [someId]); // Or [] if effect doesn't need props or state\n\n` +
930
+ 'Learn more about data fetching with Hooks: https://reactjs.org/link/hooks-data-fetching'
931
+ });
932
+ }
933
+ const scope = scopeManager.acquire(node);
934
+ const pureScopes = new Set();
935
+ let componentScope = null;
936
+ {
937
+ let currentScope = scope.upper;
938
+ while (currentScope) {
939
+ pureScopes.add(currentScope);
940
+ if (currentScope.type === 'function') {
941
+ break;
942
+ }
943
+ currentScope = currentScope.upper;
944
+ }
945
+ if (!currentScope) {
946
+ return;
947
+ }
948
+ componentScope = currentScope;
949
+ }
950
+ const isArray = Array.isArray;
951
+ const memoizedIsStableKnownHookValue = memoizeWithWeakMap(
952
+ isStableKnownHookValue,
953
+ stableKnownValueCache
954
+ );
955
+ const memoizedIsFunctionWithoutCapturedValues = memoizeWithWeakMap(
956
+ isFunctionWithoutCapturedValues,
957
+ functionWithoutCapturedValueCache
958
+ );
959
+ function isStableKnownHookValue(resolved) {
960
+ if (!isArray(resolved.defs)) {
961
+ return false;
962
+ }
963
+ const def = resolved.defs[0];
964
+ if (def == null) {
965
+ return false;
966
+ }
967
+ if (def.node.type !== 'VariableDeclarator') {
968
+ return false;
969
+ }
970
+ let init = def.node.init;
971
+ if (init == null) {
972
+ return false;
973
+ }
974
+ while (init.type === 'TSAsExpression') {
975
+ init = init.expression;
976
+ }
977
+ let declaration = def.node.parent;
978
+ if (declaration == null) {
979
+ fastFindReferenceWithParent(componentScope.block, def.node.id);
980
+ declaration = def.node.parent;
981
+ if (declaration == null) {
982
+ return false;
983
+ }
984
+ }
985
+ if (
986
+ declaration.kind === 'const' &&
987
+ init.type === 'Literal' &&
988
+ (typeof init.value === 'string' ||
989
+ typeof init.value === 'number' ||
990
+ init.value == null)
991
+ ) {
992
+ return true;
993
+ }
994
+ if (init.type !== 'CallExpression') {
995
+ return false;
996
+ }
997
+ let callee = init.callee;
998
+ if (
999
+ callee.type === 'MemberExpression' &&
1000
+ callee.object.name === 'React' &&
1001
+ callee.property != null &&
1002
+ !callee.computed
1003
+ ) {
1004
+ callee = callee.property;
1005
+ }
1006
+ if (callee.type !== 'Identifier') {
1007
+ return false;
1008
+ }
1009
+ const id = def.node.id;
1010
+ const { name } = callee;
1011
+ if (name === 'useRef' && id.type === 'Identifier') {
1012
+ return true;
1013
+ } else if (name === 'useState' || name === 'useReducer') {
1014
+ if (
1015
+ id.type === 'ArrayPattern' &&
1016
+ id.elements.length === 2 &&
1017
+ isArray(resolved.identifiers)
1018
+ ) {
1019
+ if (id.elements[1] === resolved.identifiers[0]) {
1020
+ if (name === 'useState') {
1021
+ const references = resolved.references;
1022
+ let writeCount = 0;
1023
+ for (const reference of references) {
1024
+ if (reference.isWrite()) {
1025
+ writeCount++;
1026
+ }
1027
+ if (writeCount > 1) {
1028
+ return false;
1029
+ }
1030
+ setStateCallSites.set(reference.identifier, id.elements[0]);
1031
+ }
1032
+ }
1033
+ return true;
1034
+ } else if (id.elements[0] === resolved.identifiers[0]) {
1035
+ if (name === 'useState') {
1036
+ const references = resolved.references;
1037
+ for (const reference of references) {
1038
+ stateVariables.add(reference.identifier);
1039
+ }
1040
+ }
1041
+ return false;
762
1042
  }
763
1043
  }
764
1044
  } else if (name === 'useTransition') {
@@ -1531,1003 +1811,723 @@ var betterExhaustiveDeps = {
1531
1811
  reportProblem({
1532
1812
  node: reactiveHook,
1533
1813
  message:
1534
- `React Hook ${reactiveHookName} requires an effect callback. ` +
1535
- `Did you forget to pass a callback to the hook?`
1536
- });
1537
- return;
1538
- }
1539
- if (!declaredDependenciesNode && !isEffect) {
1540
- if (
1541
- reactiveHookName === 'useMemo' ||
1542
- reactiveHookName === 'useCallback'
1543
- ) {
1544
- reportProblem({
1545
- node: reactiveHook,
1546
- message:
1547
- `React Hook ${reactiveHookName} does nothing when called with ` +
1548
- `only one argument. Did you forget to pass an array of ` +
1549
- `dependencies?`
1550
- });
1551
- }
1552
- return;
1553
- }
1554
- switch (callback.type) {
1555
- case 'FunctionExpression':
1556
- case 'ArrowFunctionExpression':
1557
- visitFunctionWithDependencies(
1558
- callback,
1559
- declaredDependenciesNode,
1560
- reactiveHook,
1561
- reactiveHookName,
1562
- isEffect
1563
- );
1564
- return;
1565
- case 'Identifier':
1566
- if (!declaredDependenciesNode) {
1567
- return;
1568
- }
1569
- if (
1570
- declaredDependenciesNode.elements &&
1571
- declaredDependenciesNode.elements.some(
1572
- el => el && el.type === 'Identifier' && el.name === callback.name
1573
- )
1574
- ) {
1575
- return;
1576
- }
1577
- const variable = context.getScope().set.get(callback.name);
1578
- if (variable == null || variable.defs == null) {
1579
- return;
1580
- }
1581
- const def = variable.defs[0];
1582
- if (!def || !def.node) {
1583
- break;
1584
- }
1585
- if (def.type !== 'Variable' && def.type !== 'FunctionName') {
1586
- break;
1587
- }
1588
- switch (def.node.type) {
1589
- case 'FunctionDeclaration':
1590
- visitFunctionWithDependencies(
1591
- def.node,
1592
- declaredDependenciesNode,
1593
- reactiveHook,
1594
- reactiveHookName,
1595
- isEffect
1596
- );
1597
- return;
1598
- case 'VariableDeclarator':
1599
- const init = def.node.init;
1600
- if (!init) {
1601
- break;
1602
- }
1603
- switch (init.type) {
1604
- case 'ArrowFunctionExpression':
1605
- case 'FunctionExpression':
1606
- visitFunctionWithDependencies(
1607
- init,
1608
- declaredDependenciesNode,
1609
- reactiveHook,
1610
- reactiveHookName,
1611
- isEffect
1612
- );
1613
- return;
1614
- }
1615
- break;
1616
- }
1617
- break;
1618
- default:
1619
- reportProblem({
1620
- node: reactiveHook,
1621
- message:
1622
- `React Hook ${reactiveHookName} received a function whose dependencies ` +
1623
- `are unknown. Pass an inline function instead.`
1624
- });
1625
- return;
1626
- }
1627
- reportProblem({
1628
- node: reactiveHook,
1629
- message:
1630
- `React Hook ${reactiveHookName} has a missing dependency: '${callback.name}'. ` +
1631
- `Either include it or remove the dependency array.`,
1632
- suggest: [
1633
- {
1634
- desc: `Update the dependencies array to be: [${callback.name}]`,
1635
- fix(fixer) {
1636
- return fixer.replaceText(
1637
- declaredDependenciesNode,
1638
- `[${callback.name}]`
1639
- );
1640
- }
1641
- }
1642
- ]
1643
- });
1644
- }
1645
- return {
1646
- CallExpression: visitCallExpression
1647
- };
1648
- }
1649
- };
1650
- function collectRecommendations({
1651
- dependencies,
1652
- declaredDependencies,
1653
- stableDependencies,
1654
- externalDependencies,
1655
- isEffect
1656
- }) {
1657
- const depTree = createDepTree();
1658
- function createDepTree() {
1659
- return {
1660
- isUsed: false,
1661
- isSatisfiedRecursively: false,
1662
- isSubtreeUsed: false,
1663
- children: new Map()
1664
- };
1665
- }
1666
- dependencies.forEach((_, key) => {
1667
- const node = getOrCreateNodeByPath(depTree, key);
1668
- node.isUsed = true;
1669
- markAllParentsByPath(depTree, key, parent => {
1670
- parent.isSubtreeUsed = true;
1671
- });
1672
- });
1673
- declaredDependencies.forEach(({ key }) => {
1674
- const node = getOrCreateNodeByPath(depTree, key);
1675
- node.isSatisfiedRecursively = true;
1676
- });
1677
- stableDependencies.forEach(key => {
1678
- const node = getOrCreateNodeByPath(depTree, key);
1679
- node.isSatisfiedRecursively = true;
1680
- });
1681
- function getOrCreateNodeByPath(rootNode, path) {
1682
- const keys = path.split('.');
1683
- let node = rootNode;
1684
- for (const key of keys) {
1685
- let child = node.children.get(key);
1686
- if (!child) {
1687
- child = createDepTree();
1688
- node.children.set(key, child);
1689
- }
1690
- node = child;
1691
- }
1692
- return node;
1693
- }
1694
- function markAllParentsByPath(rootNode, path, fn) {
1695
- const keys = path.split('.');
1696
- let node = rootNode;
1697
- for (const key of keys) {
1698
- const child = node.children.get(key);
1699
- if (!child) {
1700
- return;
1701
- }
1702
- fn(child);
1703
- node = child;
1704
- }
1705
- }
1706
- const missingDependencies = new Set();
1707
- const satisfyingDependencies = new Set();
1708
- scanTreeRecursively(
1709
- depTree,
1710
- missingDependencies,
1711
- satisfyingDependencies,
1712
- key => key
1713
- );
1714
- function scanTreeRecursively(node, missingPaths, satisfyingPaths, keyToPath) {
1715
- node.children.forEach((child, key) => {
1716
- const path = keyToPath(key);
1717
- if (child.isSatisfiedRecursively) {
1718
- if (child.isSubtreeUsed) {
1719
- satisfyingPaths.add(path);
1720
- }
1721
- return;
1722
- }
1723
- if (child.isUsed) {
1724
- missingPaths.add(path);
1725
- return;
1726
- }
1727
- scanTreeRecursively(
1728
- child,
1729
- missingPaths,
1730
- satisfyingPaths,
1731
- childKey => path + '.' + childKey
1732
- );
1733
- });
1734
- }
1735
- const suggestedDependencies = [];
1736
- const unnecessaryDependencies = new Set();
1737
- const duplicateDependencies = new Set();
1738
- declaredDependencies.forEach(({ key }) => {
1739
- if (satisfyingDependencies.has(key)) {
1740
- if (!suggestedDependencies.includes(key)) {
1741
- suggestedDependencies.push(key);
1742
- } else {
1743
- duplicateDependencies.add(key);
1744
- }
1745
- } else if (
1746
- isEffect &&
1747
- !key.endsWith('.current') &&
1748
- !externalDependencies.has(key)
1749
- ) {
1750
- if (!suggestedDependencies.includes(key)) {
1751
- suggestedDependencies.push(key);
1752
- }
1753
- } else {
1754
- unnecessaryDependencies.add(key);
1755
- }
1756
- });
1757
- missingDependencies.forEach(key => {
1758
- suggestedDependencies.push(key);
1759
- });
1760
- return {
1761
- suggestedDependencies,
1762
- unnecessaryDependencies,
1763
- duplicateDependencies,
1764
- missingDependencies
1765
- };
1766
- }
1767
- function getConstructionExpressionType(node) {
1768
- switch (node.type) {
1769
- case 'ObjectExpression':
1770
- return 'object';
1771
- case 'ArrayExpression':
1772
- return 'array';
1773
- case 'ArrowFunctionExpression':
1774
- case 'FunctionExpression':
1775
- return 'function';
1776
- case 'ClassExpression':
1777
- return 'class';
1778
- case 'ConditionalExpression':
1779
- if (
1780
- getConstructionExpressionType(node.consequent) != null ||
1781
- getConstructionExpressionType(node.alternate) != null
1782
- ) {
1783
- return 'conditional';
1784
- }
1785
- return null;
1786
- case 'LogicalExpression':
1787
- if (
1788
- getConstructionExpressionType(node.left) != null ||
1789
- getConstructionExpressionType(node.right) != null
1790
- ) {
1791
- return 'logical expression';
1792
- }
1793
- return null;
1794
- case 'JSXFragment':
1795
- return 'JSX fragment';
1796
- case 'JSXElement':
1797
- return 'JSX element';
1798
- case 'AssignmentExpression':
1799
- if (getConstructionExpressionType(node.right) != null) {
1800
- return 'assignment expression';
1801
- }
1802
- return null;
1803
- case 'NewExpression':
1804
- return 'object construction';
1805
- case 'Literal':
1806
- if (node.value instanceof RegExp) {
1807
- return 'regular expression';
1808
- }
1809
- return null;
1810
- case 'TypeCastExpression':
1811
- return getConstructionExpressionType(node.expression);
1812
- case 'TSAsExpression':
1813
- return getConstructionExpressionType(node.expression);
1814
- }
1815
- return null;
1816
- }
1817
- function scanForConstructions({
1818
- declaredDependencies,
1819
- declaredDependenciesNode,
1820
- componentScope,
1821
- scope
1822
- }) {
1823
- const constructions = declaredDependencies
1824
- .map(({ key }) => {
1825
- const ref = componentScope.variables.find(v => v.name === key);
1826
- if (ref == null) {
1827
- return null;
1828
- }
1829
- const node = ref.defs[0];
1830
- if (node == null) {
1831
- return null;
1832
- }
1833
- if (
1834
- node.type === 'Variable' &&
1835
- node.node.type === 'VariableDeclarator' &&
1836
- node.node.id.type === 'Identifier' &&
1837
- node.node.init != null
1838
- ) {
1839
- const constantExpressionType = getConstructionExpressionType(
1840
- node.node.init
1841
- );
1842
- if (constantExpressionType != null) {
1843
- return [ref, constantExpressionType];
1844
- }
1845
- }
1846
- if (
1847
- node.type === 'FunctionName' &&
1848
- node.node.type === 'FunctionDeclaration'
1849
- ) {
1850
- return [ref, 'function'];
1851
- }
1852
- if (node.type === 'ClassName' && node.node.type === 'ClassDeclaration') {
1853
- return [ref, 'class'];
1854
- }
1855
- return null;
1856
- })
1857
- .filter(Boolean);
1858
- function isUsedOutsideOfHook(ref) {
1859
- let foundWriteExpr = false;
1860
- for (let i = 0; i < ref.references.length; i++) {
1861
- const reference = ref.references[i];
1862
- if (reference.writeExpr) {
1863
- if (foundWriteExpr) {
1864
- return true;
1865
- }
1866
- foundWriteExpr = true;
1867
- continue;
1868
- }
1869
- let currentScope = reference.from;
1870
- while (currentScope !== scope && currentScope != null) {
1871
- currentScope = currentScope.upper;
1872
- }
1873
- if (
1874
- currentScope !== scope &&
1875
- !isAncestorNodeOf(declaredDependenciesNode, reference.identifier)
1876
- ) {
1877
- return true;
1878
- }
1879
- }
1880
- return false;
1881
- }
1882
- return constructions.map(([ref, depType]) => ({
1883
- construction: ref.defs[0],
1884
- depType,
1885
- isUsedOutsideOfHook: isUsedOutsideOfHook(ref)
1886
- }));
1887
- }
1888
- function getDependency(node) {
1889
- if (
1890
- (node.parent.type === 'MemberExpression' ||
1891
- node.parent.type === 'OptionalMemberExpression') &&
1892
- node.parent.object === node &&
1893
- node.parent.property.name !== 'current' &&
1894
- !node.parent.computed &&
1895
- !(
1896
- node.parent.parent != null &&
1897
- (node.parent.parent.type === 'CallExpression' ||
1898
- node.parent.parent.type === 'OptionalCallExpression') &&
1899
- node.parent.parent.callee === node.parent
1900
- )
1901
- ) {
1902
- return getDependency(node.parent);
1903
- } else if (
1904
- node.type === 'MemberExpression' &&
1905
- node.parent &&
1906
- node.parent.type === 'AssignmentExpression' &&
1907
- node.parent.left === node
1908
- ) {
1909
- return node.object;
1910
- }
1911
- return node;
1912
- }
1913
- function markNode(node, optionalChains, result) {
1914
- if (optionalChains) {
1915
- if (node.optional) {
1916
- if (!optionalChains.has(result)) {
1917
- optionalChains.set(result, true);
1918
- }
1919
- } else {
1920
- optionalChains.set(result, false);
1921
- }
1922
- }
1923
- }
1924
- function analyzePropertyChain(node, optionalChains) {
1925
- if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
1926
- const result = node.name;
1927
- if (optionalChains) {
1928
- optionalChains.set(result, false);
1929
- }
1930
- return result;
1931
- } else if (node.type === 'MemberExpression' && !node.computed) {
1932
- const object = analyzePropertyChain(node.object, optionalChains);
1933
- const property = analyzePropertyChain(node.property, null);
1934
- const result = `${object}.${property}`;
1935
- markNode(node, optionalChains, result);
1936
- return result;
1937
- } else if (node.type === 'OptionalMemberExpression' && !node.computed) {
1938
- const object = analyzePropertyChain(node.object, optionalChains);
1939
- const property = analyzePropertyChain(node.property, null);
1940
- const result = `${object}.${property}`;
1941
- markNode(node, optionalChains, result);
1942
- return result;
1943
- } else if (node.type === 'ChainExpression' && !node.computed) {
1944
- const expression = node.expression;
1945
- if (expression.type === 'CallExpression') {
1946
- throw new Error(`Unsupported node type: ${expression.type}`);
1947
- }
1948
- const object = analyzePropertyChain(expression.object, optionalChains);
1949
- const property = analyzePropertyChain(expression.property, null);
1950
- const result = `${object}.${property}`;
1951
- markNode(expression, optionalChains, result);
1952
- return result;
1953
- }
1954
- throw new Error(`Unsupported node type: ${node.type}`);
1955
- }
1956
- function getNodeWithoutReactNamespace(node) {
1957
- if (
1958
- node.type === 'MemberExpression' &&
1959
- node.object.type === 'Identifier' &&
1960
- node.object.name === 'React' &&
1961
- node.property.type === 'Identifier' &&
1962
- !node.computed
1963
- ) {
1964
- return node.property;
1965
- }
1966
- return node;
1967
- }
1968
- function getReactiveHookCallbackIndex(calleeNode, options) {
1969
- const node = getNodeWithoutReactNamespace(calleeNode);
1970
- if (node.type !== 'Identifier') {
1971
- return -1;
1972
- }
1973
- switch (node.name) {
1974
- case 'useEffect':
1975
- case 'useLayoutEffect':
1976
- case 'useCallback':
1977
- case 'useMemo':
1978
- return 0;
1979
- case 'useImperativeHandle':
1980
- return 1;
1981
- default:
1982
- if (node === calleeNode && options && options.additionalHooks) {
1983
- let name;
1984
- try {
1985
- name = analyzePropertyChain(node, null);
1986
- } catch (err) {
1987
- if (/Unsupported node type/.test(err.message)) {
1988
- return 0;
1989
- }
1990
- throw err;
1991
- }
1992
- return options.additionalHooks.test(name) ? 0 : -1;
1993
- }
1994
- return -1;
1995
- }
1996
- }
1997
- function fastFindReferenceWithParent(start, target) {
1998
- const queue = [start];
1999
- let item = null;
2000
- while (queue.length > 0) {
2001
- item = queue.shift();
2002
- if (isSameIdentifier(item, target)) {
2003
- return item;
2004
- }
2005
- if (!isAncestorNodeOf(item, target)) {
2006
- continue;
2007
- }
2008
- for (const [key, value] of Object.entries(item)) {
2009
- if (key === 'parent') {
2010
- continue;
2011
- }
2012
- if (isNodeLike(value)) {
2013
- value.parent = item;
2014
- queue.push(value);
2015
- } else if (Array.isArray(value)) {
2016
- value.forEach(val => {
2017
- if (isNodeLike(val)) {
2018
- val.parent = item;
2019
- queue.push(val);
2020
- }
2021
- });
2022
- }
2023
- }
2024
- }
2025
- return null;
2026
- }
2027
- function joinEnglish(arr) {
2028
- let s = '';
2029
- for (let i = 0; i < arr.length; i++) {
2030
- s += arr[i];
2031
- if (i === 0 && arr.length === 2) {
2032
- s += ' and ';
2033
- } else if (i === arr.length - 2 && arr.length > 2) {
2034
- s += ', and ';
2035
- } else if (i < arr.length - 1) {
2036
- s += ', ';
2037
- }
2038
- }
2039
- return s;
2040
- }
2041
- function isNodeLike(val) {
2042
- return (
2043
- typeof val === 'object' &&
2044
- val != null &&
2045
- !Array.isArray(val) &&
2046
- typeof val.type === 'string'
2047
- );
2048
- }
2049
- function isSameIdentifier(a, b) {
2050
- return (
2051
- (a.type === 'Identifier' || a.type === 'JSXIdentifier') &&
2052
- a.type === b.type &&
2053
- a.name === b.name &&
2054
- a.range[0] === b.range[0] &&
2055
- a.range[1] === b.range[1]
2056
- );
2057
- }
2058
- function isAncestorNodeOf(a, b) {
2059
- return a.range[0] <= b.range[0] && a.range[1] >= b.range[1];
2060
- }
2061
-
2062
- const Components = require('eslint-plugin-react/lib/util/Components');
2063
- var hookUseRef = {
2064
- meta: {
2065
- docs: {
2066
- description: 'Ensure naming of useRef hook value.',
2067
- recommended: false
2068
- },
2069
- schema: [],
2070
- type: 'suggestion',
2071
- hasSuggestions: true
2072
- },
2073
- create: Components.detect((context, component, util) => ({
2074
- CallExpression(node) {
2075
- const isImmediateReturn =
2076
- node.parent && node.parent.type === 'ReturnStatement';
2077
- if (isImmediateReturn || !util.isReactHookCall(node, ['useRef'])) {
2078
- return;
2079
- }
2080
- if (node.parent.id.type !== 'Identifier') {
2081
- return;
2082
- }
2083
- const variable = node.parent.id.name;
2084
- if (!variable.endsWith('Ref')) {
2085
- context.report({
2086
- node: node,
2087
- message: 'useRef call is not end with "Ref"'
1814
+ `React Hook ${reactiveHookName} requires an effect callback. ` +
1815
+ `Did you forget to pass a callback to the hook?`
2088
1816
  });
1817
+ return;
2089
1818
  }
2090
- }
2091
- }))
2092
- };
2093
-
2094
- function* updateImportStatement(context, fixer, key) {
2095
- const sourceCode = context.sourceCode;
2096
- const importNode = sourceCode.ast.body.find(
2097
- node => node.type === 'ImportDeclaration' && node.source.value === 'react'
2098
- );
2099
- if (!importNode) {
2100
- yield fixer.insertTextBefore(
2101
- sourceCode.ast.body[0],
2102
- `import { ${key} } from 'react';\n`
2103
- );
2104
- return;
2105
- }
2106
- if (
2107
- importNode.specifiers.length === 1 &&
2108
- importNode.specifiers[0].type === 'ImportDefaultSpecifier'
2109
- ) {
2110
- yield fixer.insertTextAfter(importNode.specifiers[0], `, { ${key} }`);
2111
- return;
2112
- }
2113
- const alreadyImportedKeys = importNode.specifiers
2114
- .filter(specifier => specifier.type === 'ImportSpecifier')
2115
- .map(specifier => specifier.imported.name);
2116
- if (alreadyImportedKeys.includes(key)) {
2117
- return;
2118
- }
2119
- yield fixer.insertTextAfter([...importNode.specifiers].pop(), `, ${key}`);
2120
- }
2121
- var preferNamedPropertyAccess = utils.ESLintUtils.RuleCreator.withoutDocs({
2122
- defaultOptions: [],
2123
- meta: {
2124
- type: 'layout',
2125
- fixable: 'code',
2126
- docs: {
2127
- description:
2128
- 'Enforce importing each member of React namespace separately instead of accessing them through React namespace'
2129
- },
2130
- messages: {
2131
- illegalReactPropertyAccess:
2132
- 'Illegal React property access: {{name}}. Use named import instead.'
2133
- },
2134
- schema: []
2135
- },
2136
- create(context) {
2137
- return {
2138
- TSQualifiedName(node) {
1819
+ if (!declaredDependenciesNode && !isEffect) {
2139
1820
  if (
2140
- ('name' in node.left && node.left.name !== 'React') ||
2141
- ('name' in node.right && node.right.name.endsWith('Event'))
1821
+ reactiveHookName === 'useMemo' ||
1822
+ reactiveHookName === 'useCallback'
2142
1823
  ) {
2143
- return;
1824
+ reportProblem({
1825
+ node: reactiveHook,
1826
+ message:
1827
+ `React Hook ${reactiveHookName} does nothing when called with ` +
1828
+ `only one argument. Did you forget to pass an array of ` +
1829
+ `dependencies?`
1830
+ });
2144
1831
  }
2145
- context.report({
2146
- node,
2147
- messageId: 'illegalReactPropertyAccess',
2148
- data: {
2149
- name: node.right.name
2150
- },
2151
- *fix(fixer) {
2152
- yield fixer.replaceText(node, node.right.name);
2153
- yield* updateImportStatement(context, fixer, node.right.name);
2154
- }
2155
- });
2156
- },
2157
- MemberExpression(node) {
2158
- if (node.object.name !== 'React') {
1832
+ return;
1833
+ }
1834
+ switch (callback.type) {
1835
+ case 'FunctionExpression':
1836
+ case 'ArrowFunctionExpression':
1837
+ visitFunctionWithDependencies(
1838
+ callback,
1839
+ declaredDependenciesNode,
1840
+ reactiveHook,
1841
+ reactiveHookName,
1842
+ isEffect
1843
+ );
2159
1844
  return;
2160
- }
2161
- context.report({
2162
- node,
2163
- messageId: 'illegalReactPropertyAccess',
2164
- data: {
2165
- name: node.property.name
2166
- },
2167
- *fix(fixer) {
2168
- yield fixer.replaceText(node, node.property.name);
2169
- yield* updateImportStatement(context, fixer, node.property.name);
1845
+ case 'Identifier':
1846
+ if (!declaredDependenciesNode) {
1847
+ return;
2170
1848
  }
2171
- });
1849
+ if (
1850
+ declaredDependenciesNode.elements &&
1851
+ declaredDependenciesNode.elements.some(
1852
+ el => el && el.type === 'Identifier' && el.name === callback.name
1853
+ )
1854
+ ) {
1855
+ return;
1856
+ }
1857
+ const variable = context.getScope().set.get(callback.name);
1858
+ if (variable == null || variable.defs == null) {
1859
+ return;
1860
+ }
1861
+ const def = variable.defs[0];
1862
+ if (!def || !def.node) {
1863
+ break;
1864
+ }
1865
+ if (def.type !== 'Variable' && def.type !== 'FunctionName') {
1866
+ break;
1867
+ }
1868
+ switch (def.node.type) {
1869
+ case 'FunctionDeclaration':
1870
+ visitFunctionWithDependencies(
1871
+ def.node,
1872
+ declaredDependenciesNode,
1873
+ reactiveHook,
1874
+ reactiveHookName,
1875
+ isEffect
1876
+ );
1877
+ return;
1878
+ case 'VariableDeclarator':
1879
+ const init = def.node.init;
1880
+ if (!init) {
1881
+ break;
1882
+ }
1883
+ switch (init.type) {
1884
+ case 'ArrowFunctionExpression':
1885
+ case 'FunctionExpression':
1886
+ visitFunctionWithDependencies(
1887
+ init,
1888
+ declaredDependenciesNode,
1889
+ reactiveHook,
1890
+ reactiveHookName,
1891
+ isEffect
1892
+ );
1893
+ return;
1894
+ }
1895
+ break;
1896
+ }
1897
+ break;
1898
+ default:
1899
+ reportProblem({
1900
+ node: reactiveHook,
1901
+ message:
1902
+ `React Hook ${reactiveHookName} received a function whose dependencies ` +
1903
+ `are unknown. Pass an inline function instead.`
1904
+ });
1905
+ return;
2172
1906
  }
1907
+ reportProblem({
1908
+ node: reactiveHook,
1909
+ message:
1910
+ `React Hook ${reactiveHookName} has a missing dependency: '${callback.name}'. ` +
1911
+ `Either include it or remove the dependency array.`,
1912
+ suggest: [
1913
+ {
1914
+ desc: `Update the dependencies array to be: [${callback.name}]`,
1915
+ fix(fixer) {
1916
+ return fixer.replaceText(
1917
+ declaredDependenciesNode,
1918
+ `[${callback.name}]`
1919
+ );
1920
+ }
1921
+ }
1922
+ ]
1923
+ });
1924
+ }
1925
+ return {
1926
+ CallExpression: visitCallExpression
2173
1927
  };
2174
1928
  }
2175
- });
2176
-
2177
- var preferSxProp = {
2178
- meta: {
2179
- docs: {
2180
- description: 'Prefer using sx prop instead of inline styles',
2181
- category: 'Best Practices',
2182
- recommended: false
2183
- },
2184
- messages: {
2185
- preferSxProp:
2186
- 'Avoid using inline styles, use sx prop or tss-react or styled-component instead'
2187
- },
2188
- schema: [
2189
- {
2190
- type: 'object',
2191
- properties: {
2192
- allowedFor: {
2193
- type: 'array',
2194
- uniqueItems: true,
2195
- items: { type: 'string' }
2196
- }
2197
- }
1929
+ };
1930
+ function collectRecommendations({
1931
+ dependencies,
1932
+ declaredDependencies,
1933
+ stableDependencies,
1934
+ externalDependencies,
1935
+ isEffect
1936
+ }) {
1937
+ const depTree = createDepTree();
1938
+ function createDepTree() {
1939
+ return {
1940
+ isUsed: false,
1941
+ isSatisfiedRecursively: false,
1942
+ isSubtreeUsed: false,
1943
+ children: new Map()
1944
+ };
1945
+ }
1946
+ dependencies.forEach((_, key) => {
1947
+ const node = getOrCreateNodeByPath(depTree, key);
1948
+ node.isUsed = true;
1949
+ markAllParentsByPath(depTree, key, parent => {
1950
+ parent.isSubtreeUsed = true;
1951
+ });
1952
+ });
1953
+ declaredDependencies.forEach(({ key }) => {
1954
+ const node = getOrCreateNodeByPath(depTree, key);
1955
+ node.isSatisfiedRecursively = true;
1956
+ });
1957
+ stableDependencies.forEach(key => {
1958
+ const node = getOrCreateNodeByPath(depTree, key);
1959
+ node.isSatisfiedRecursively = true;
1960
+ });
1961
+ function getOrCreateNodeByPath(rootNode, path) {
1962
+ const keys = path.split('.');
1963
+ let node = rootNode;
1964
+ for (const key of keys) {
1965
+ let child = node.children.get(key);
1966
+ if (!child) {
1967
+ child = createDepTree();
1968
+ node.children.set(key, child);
1969
+ }
1970
+ node = child;
1971
+ }
1972
+ return node;
1973
+ }
1974
+ function markAllParentsByPath(rootNode, path, fn) {
1975
+ const keys = path.split('.');
1976
+ let node = rootNode;
1977
+ for (const key of keys) {
1978
+ const child = node.children.get(key);
1979
+ if (!child) {
1980
+ return;
2198
1981
  }
2199
- ]
2200
- },
2201
- create(context) {
2202
- const configuration = context.options[0] || {};
2203
- const allowedFor = configuration.allowedFor || [];
2204
- function checkComponent(node) {
2205
- const parentName = node.parent.name;
2206
- const tag =
2207
- parentName.name ||
2208
- `${parentName.object.name}.${parentName.property.name}`;
2209
- const componentName = parentName.name || parentName.property.name;
2210
- if (
2211
- componentName &&
2212
- typeof componentName[0] === 'string' &&
2213
- componentName[0] !== componentName[0].toUpperCase()
2214
- ) {
1982
+ fn(child);
1983
+ node = child;
1984
+ }
1985
+ }
1986
+ const missingDependencies = new Set();
1987
+ const satisfyingDependencies = new Set();
1988
+ scanTreeRecursively(
1989
+ depTree,
1990
+ missingDependencies,
1991
+ satisfyingDependencies,
1992
+ key => key
1993
+ );
1994
+ function scanTreeRecursively(node, missingPaths, satisfyingPaths, keyToPath) {
1995
+ node.children.forEach((child, key) => {
1996
+ const path = keyToPath(key);
1997
+ if (child.isSatisfiedRecursively) {
1998
+ if (child.isSubtreeUsed) {
1999
+ satisfyingPaths.add(path);
2000
+ }
2215
2001
  return;
2216
2002
  }
2217
- if (allowedFor.includes(tag)) {
2003
+ if (child.isUsed) {
2004
+ missingPaths.add(path);
2218
2005
  return;
2219
2006
  }
2220
- const prop = node.name.name;
2221
- if (prop === 'style') {
2222
- context.report({
2223
- node,
2224
- messageId: 'preferSxProp'
2225
- });
2007
+ scanTreeRecursively(
2008
+ child,
2009
+ missingPaths,
2010
+ satisfyingPaths,
2011
+ childKey => path + '.' + childKey
2012
+ );
2013
+ });
2014
+ }
2015
+ const suggestedDependencies = [];
2016
+ const unnecessaryDependencies = new Set();
2017
+ const duplicateDependencies = new Set();
2018
+ declaredDependencies.forEach(({ key }) => {
2019
+ if (satisfyingDependencies.has(key)) {
2020
+ if (!suggestedDependencies.includes(key)) {
2021
+ suggestedDependencies.push(key);
2022
+ } else {
2023
+ duplicateDependencies.add(key);
2024
+ }
2025
+ } else if (
2026
+ isEffect &&
2027
+ !key.endsWith('.current') &&
2028
+ !externalDependencies.has(key)
2029
+ ) {
2030
+ if (!suggestedDependencies.includes(key)) {
2031
+ suggestedDependencies.push(key);
2226
2032
  }
2033
+ } else {
2034
+ unnecessaryDependencies.add(key);
2227
2035
  }
2228
- function checkDOMNodes(node) {
2229
- const tag = node.parent.name.name;
2036
+ });
2037
+ missingDependencies.forEach(key => {
2038
+ suggestedDependencies.push(key);
2039
+ });
2040
+ return {
2041
+ suggestedDependencies,
2042
+ unnecessaryDependencies,
2043
+ duplicateDependencies,
2044
+ missingDependencies
2045
+ };
2046
+ }
2047
+ function getConstructionExpressionType(node) {
2048
+ switch (node.type) {
2049
+ case 'ObjectExpression':
2050
+ return 'object';
2051
+ case 'ArrayExpression':
2052
+ return 'array';
2053
+ case 'ArrowFunctionExpression':
2054
+ case 'FunctionExpression':
2055
+ return 'function';
2056
+ case 'ClassExpression':
2057
+ return 'class';
2058
+ case 'ConditionalExpression':
2230
2059
  if (
2231
- !(tag && typeof tag === 'string' && tag[0] !== tag[0].toUpperCase())
2060
+ getConstructionExpressionType(node.consequent) != null ||
2061
+ getConstructionExpressionType(node.alternate) != null
2232
2062
  ) {
2233
- return;
2063
+ return 'conditional';
2234
2064
  }
2235
- if (allowedFor.includes(tag)) {
2236
- return;
2065
+ return null;
2066
+ case 'LogicalExpression':
2067
+ if (
2068
+ getConstructionExpressionType(node.left) != null ||
2069
+ getConstructionExpressionType(node.right) != null
2070
+ ) {
2071
+ return 'logical expression';
2237
2072
  }
2238
- const prop = node.name.name;
2239
- if (prop === 'style') {
2240
- context.report({
2241
- node,
2242
- messageId: 'preferSxProp'
2243
- });
2073
+ return null;
2074
+ case 'JSXFragment':
2075
+ return 'JSX fragment';
2076
+ case 'JSXElement':
2077
+ return 'JSX element';
2078
+ case 'AssignmentExpression':
2079
+ if (getConstructionExpressionType(node.right) != null) {
2080
+ return 'assignment expression';
2244
2081
  }
2245
- }
2246
- return {
2247
- JSXAttribute(node) {
2248
- checkComponent(node);
2249
- checkDOMNodes(node);
2082
+ return null;
2083
+ case 'NewExpression':
2084
+ return 'object construction';
2085
+ case 'Literal':
2086
+ if (node.value instanceof RegExp) {
2087
+ return 'regular expression';
2250
2088
  }
2251
- };
2089
+ return null;
2090
+ case 'TypeCastExpression':
2091
+ return getConstructionExpressionType(node.expression);
2092
+ case 'TSAsExpression':
2093
+ return getConstructionExpressionType(node.expression);
2252
2094
  }
2253
- };
2254
-
2255
- var noUnnecessaryTemplateLiterals = {
2256
- meta: {
2257
- type: 'problem',
2258
- docs: {
2259
- description: 'Check if a template string contains only one ${}',
2260
- recommended: true
2261
- },
2262
- fixable: 'code',
2263
- schema: []
2264
- },
2265
- create(context) {
2266
- return {
2267
- TemplateLiteral(node) {
2268
- const code = context.sourceCode.getText(node);
2269
- if (
2270
- code.startsWith('`${') &&
2271
- code.endsWith('}`') &&
2272
- code.split('${').length === 2
2273
- ) {
2274
- context.report({
2275
- node,
2276
- message: 'Unnecessary template string with only one ${}.',
2277
- fix(fixer) {
2278
- return fixer.replaceText(
2279
- node,
2280
- code.substring(3, code.length - 2)
2281
- );
2282
- }
2283
- });
2095
+ return null;
2096
+ }
2097
+ function scanForConstructions({
2098
+ declaredDependencies,
2099
+ declaredDependenciesNode,
2100
+ componentScope,
2101
+ scope
2102
+ }) {
2103
+ const constructions = declaredDependencies
2104
+ .map(({ key }) => {
2105
+ const ref = componentScope.variables.find(v => v.name === key);
2106
+ if (ref == null) {
2107
+ return null;
2108
+ }
2109
+ const node = ref.defs[0];
2110
+ if (node == null) {
2111
+ return null;
2112
+ }
2113
+ if (
2114
+ node.type === 'Variable' &&
2115
+ node.node.type === 'VariableDeclarator' &&
2116
+ node.node.id.type === 'Identifier' &&
2117
+ node.node.init != null
2118
+ ) {
2119
+ const constantExpressionType = getConstructionExpressionType(
2120
+ node.node.init
2121
+ );
2122
+ if (constantExpressionType != null) {
2123
+ return [ref, constantExpressionType];
2284
2124
  }
2285
2125
  }
2286
- };
2287
- }
2288
- };
2289
-
2290
- function getBasicIdentifier(node) {
2291
- if (node.type === 'Identifier') {
2292
- return node.name;
2126
+ if (
2127
+ node.type === 'FunctionName' &&
2128
+ node.node.type === 'FunctionDeclaration'
2129
+ ) {
2130
+ return [ref, 'function'];
2131
+ }
2132
+ if (node.type === 'ClassName' && node.node.type === 'ClassDeclaration') {
2133
+ return [ref, 'class'];
2134
+ }
2135
+ return null;
2136
+ })
2137
+ .filter(Boolean);
2138
+ function isUsedOutsideOfHook(ref) {
2139
+ let foundWriteExpr = false;
2140
+ for (let i = 0; i < ref.references.length; i++) {
2141
+ const reference = ref.references[i];
2142
+ if (reference.writeExpr) {
2143
+ if (foundWriteExpr) {
2144
+ return true;
2145
+ }
2146
+ foundWriteExpr = true;
2147
+ continue;
2148
+ }
2149
+ let currentScope = reference.from;
2150
+ while (currentScope !== scope && currentScope != null) {
2151
+ currentScope = currentScope.upper;
2152
+ }
2153
+ if (
2154
+ currentScope !== scope &&
2155
+ !isAncestorNodeOf(declaredDependenciesNode, reference.identifier)
2156
+ ) {
2157
+ return true;
2158
+ }
2159
+ }
2160
+ return false;
2293
2161
  }
2294
- if (node.type === 'Literal') {
2295
- return node.value;
2162
+ return constructions.map(([ref, depType]) => ({
2163
+ construction: ref.defs[0],
2164
+ depType,
2165
+ isUsedOutsideOfHook: isUsedOutsideOfHook(ref)
2166
+ }));
2167
+ }
2168
+ function getDependency(node) {
2169
+ if (
2170
+ (node.parent.type === 'MemberExpression' ||
2171
+ node.parent.type === 'OptionalMemberExpression') &&
2172
+ node.parent.object === node &&
2173
+ node.parent.property.name !== 'current' &&
2174
+ !node.parent.computed &&
2175
+ !(
2176
+ node.parent.parent != null &&
2177
+ (node.parent.parent.type === 'CallExpression' ||
2178
+ node.parent.parent.type === 'OptionalCallExpression') &&
2179
+ node.parent.parent.callee === node.parent
2180
+ )
2181
+ ) {
2182
+ return getDependency(node.parent);
2183
+ } else if (
2184
+ node.type === 'MemberExpression' &&
2185
+ node.parent &&
2186
+ node.parent.type === 'AssignmentExpression' &&
2187
+ node.parent.left === node
2188
+ ) {
2189
+ return node.object;
2296
2190
  }
2297
- if (node.type === 'TemplateLiteral') {
2298
- if (node.expressions.length > 0) {
2299
- return null;
2191
+ return node;
2192
+ }
2193
+ function markNode(node, optionalChains, result) {
2194
+ if (optionalChains) {
2195
+ if (node.optional) {
2196
+ if (!optionalChains.has(result)) {
2197
+ optionalChains.set(result, true);
2198
+ }
2199
+ } else {
2200
+ optionalChains.set(result, false);
2300
2201
  }
2301
- return node.quasis[0].value.raw;
2302
2202
  }
2303
- return null;
2304
2203
  }
2305
- function getBaseIdentifier(node) {
2306
- switch (node.type) {
2307
- case 'Identifier': {
2308
- return node;
2309
- }
2310
- case 'CallExpression': {
2311
- return getBaseIdentifier(node.callee);
2204
+ function analyzePropertyChain(node, optionalChains) {
2205
+ if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
2206
+ const result = node.name;
2207
+ if (optionalChains) {
2208
+ optionalChains.set(result, false);
2312
2209
  }
2313
- case 'MemberExpression': {
2314
- return getBaseIdentifier(node.object);
2210
+ return result;
2211
+ } else if (node.type === 'MemberExpression' && !node.computed) {
2212
+ const object = analyzePropertyChain(node.object, optionalChains);
2213
+ const property = analyzePropertyChain(node.property, null);
2214
+ const result = `${object}.${property}`;
2215
+ markNode(node, optionalChains, result);
2216
+ return result;
2217
+ } else if (node.type === 'OptionalMemberExpression' && !node.computed) {
2218
+ const object = analyzePropertyChain(node.object, optionalChains);
2219
+ const property = analyzePropertyChain(node.property, null);
2220
+ const result = `${object}.${property}`;
2221
+ markNode(node, optionalChains, result);
2222
+ return result;
2223
+ } else if (node.type === 'ChainExpression' && !node.computed) {
2224
+ const expression = node.expression;
2225
+ if (expression.type === 'CallExpression') {
2226
+ throw new Error(`Unsupported node type: ${expression.type}`);
2315
2227
  }
2228
+ const object = analyzePropertyChain(expression.object, optionalChains);
2229
+ const property = analyzePropertyChain(expression.property, null);
2230
+ const result = `${object}.${property}`;
2231
+ markNode(expression, optionalChains, result);
2232
+ return result;
2316
2233
  }
2317
- return null;
2234
+ throw new Error(`Unsupported node type: ${node.type}`);
2318
2235
  }
2319
- function getStyesObj(node) {
2320
- const isMakeStyles = node.callee.name === 'makeStyles';
2321
- const isModernApi =
2322
- node.callee.type === 'MemberExpression' &&
2323
- node.callee.property.name === 'create' &&
2324
- getBaseIdentifier(node.callee.object) &&
2325
- getBaseIdentifier(node.callee.object).name === 'tss';
2326
- if (!isMakeStyles && !isModernApi) {
2327
- return;
2236
+ function getNodeWithoutReactNamespace(node) {
2237
+ if (
2238
+ node.type === 'MemberExpression' &&
2239
+ node.object.type === 'Identifier' &&
2240
+ node.object.name === 'React' &&
2241
+ node.property.type === 'Identifier' &&
2242
+ !node.computed
2243
+ ) {
2244
+ return node.property;
2328
2245
  }
2329
- const styles = (() => {
2330
- if (isMakeStyles) {
2331
- return node.parent.arguments[0];
2332
- }
2333
- if (isModernApi) {
2334
- return node.callee.parent.arguments[0];
2335
- }
2336
- })();
2337
- if (!styles) {
2338
- return;
2246
+ return node;
2247
+ }
2248
+ function getReactiveHookCallbackIndex(calleeNode, options) {
2249
+ const node = getNodeWithoutReactNamespace(calleeNode);
2250
+ if (node.type !== 'Identifier') {
2251
+ return -1;
2339
2252
  }
2340
- switch (styles.type) {
2341
- case 'ObjectExpression':
2342
- return styles;
2343
- case 'ArrowFunctionExpression':
2344
- {
2345
- const { body } = styles;
2346
- switch (body.type) {
2347
- case 'ObjectExpression':
2348
- return body;
2349
- case 'BlockStatement': {
2350
- let stylesObj;
2351
- body.body.forEach(bodyNode => {
2352
- if (
2353
- bodyNode.type === 'ReturnStatement' &&
2354
- bodyNode.argument.type === 'ObjectExpression'
2355
- ) {
2356
- stylesObj = bodyNode.argument;
2357
- }
2358
- });
2359
- return stylesObj;
2253
+ switch (node.name) {
2254
+ case 'useEffect':
2255
+ case 'useLayoutEffect':
2256
+ case 'useCallback':
2257
+ case 'useMemo':
2258
+ return 0;
2259
+ case 'useImperativeHandle':
2260
+ return 1;
2261
+ default:
2262
+ if (node === calleeNode && options && options.additionalHooks) {
2263
+ let name;
2264
+ try {
2265
+ name = analyzePropertyChain(node, null);
2266
+ } catch (err) {
2267
+ if (/Unsupported node type/.test(err.message)) {
2268
+ return 0;
2360
2269
  }
2270
+ throw err;
2361
2271
  }
2272
+ return options.additionalHooks.test(name) ? 0 : -1;
2362
2273
  }
2363
- break;
2274
+ return -1;
2275
+ }
2276
+ }
2277
+ function fastFindReferenceWithParent(start, target) {
2278
+ const queue = [start];
2279
+ let item = null;
2280
+ while (queue.length > 0) {
2281
+ item = queue.shift();
2282
+ if (isSameIdentifier(item, target)) {
2283
+ return item;
2284
+ }
2285
+ if (!isAncestorNodeOf(item, target)) {
2286
+ continue;
2287
+ }
2288
+ for (const [key, value] of Object.entries(item)) {
2289
+ if (key === 'parent') {
2290
+ continue;
2291
+ }
2292
+ if (isNodeLike(value)) {
2293
+ value.parent = item;
2294
+ queue.push(value);
2295
+ } else if (Array.isArray(value)) {
2296
+ value.forEach(val => {
2297
+ if (isNodeLike(val)) {
2298
+ val.parent = item;
2299
+ queue.push(val);
2300
+ }
2301
+ });
2302
+ }
2303
+ }
2304
+ }
2305
+ return null;
2306
+ }
2307
+ function joinEnglish(arr) {
2308
+ let s = '';
2309
+ for (let i = 0; i < arr.length; i++) {
2310
+ s += arr[i];
2311
+ if (i === 0 && arr.length === 2) {
2312
+ s += ' and ';
2313
+ } else if (i === arr.length - 2 && arr.length > 2) {
2314
+ s += ', and ';
2315
+ } else if (i < arr.length - 1) {
2316
+ s += ', ';
2317
+ }
2364
2318
  }
2319
+ return s;
2320
+ }
2321
+ function isNodeLike(val) {
2322
+ return (
2323
+ typeof val === 'object' &&
2324
+ val != null &&
2325
+ !Array.isArray(val) &&
2326
+ typeof val.type === 'string'
2327
+ );
2328
+ }
2329
+ function isSameIdentifier(a, b) {
2330
+ return (
2331
+ (a.type === 'Identifier' || a.type === 'JSXIdentifier') &&
2332
+ a.type === b.type &&
2333
+ a.name === b.name &&
2334
+ a.range[0] === b.range[0] &&
2335
+ a.range[1] === b.range[1]
2336
+ );
2337
+ }
2338
+ function isAncestorNodeOf(a, b) {
2339
+ return a.range[0] <= b.range[0] && a.range[1] >= b.range[1];
2365
2340
  }
2366
2341
 
2367
- var classNaming = {
2342
+ const Components = require('eslint-plugin-react/lib/util/Components');
2343
+ var hookUseRef = {
2368
2344
  meta: {
2369
- type: 'problem'
2345
+ docs: {
2346
+ description: 'Ensure naming of useRef hook value.',
2347
+ recommended: false
2348
+ },
2349
+ schema: [],
2350
+ type: 'suggestion',
2351
+ hasSuggestions: true
2370
2352
  },
2371
- create: function rule(context) {
2372
- return {
2373
- CallExpression(node) {
2374
- const stylesObj = getStyesObj(node);
2375
- if (stylesObj === undefined) {
2376
- return;
2377
- }
2378
- stylesObj.properties.forEach(property => {
2379
- if (property.computed) {
2380
- return;
2381
- }
2382
- if (
2383
- property.type === 'ExperimentalSpreadProperty' ||
2384
- property.type === 'SpreadElement'
2385
- ) {
2386
- return;
2387
- }
2388
- const className = property.key.value || property.key.name;
2389
- if (!eslintUtils.isCamelCase(className)) {
2390
- context.report({
2391
- node: property,
2392
- message: `Class \`${className}\` must be camelCase in TSS.`
2393
- });
2394
- }
2353
+ create: Components.detect((context, component, util) => ({
2354
+ CallExpression(node) {
2355
+ const isImmediateReturn =
2356
+ node.parent && node.parent.type === 'ReturnStatement';
2357
+ if (isImmediateReturn || !util.isReactHookCall(node, ['useRef'])) {
2358
+ return;
2359
+ }
2360
+ if (node.parent.id.type !== 'Identifier') {
2361
+ return;
2362
+ }
2363
+ const variable = node.parent.id.name;
2364
+ if (!variable.endsWith('Ref')) {
2365
+ context.report({
2366
+ node: node,
2367
+ message: 'useRef call is not end with "Ref"'
2395
2368
  });
2396
2369
  }
2397
- };
2398
- }
2370
+ }
2371
+ }))
2399
2372
  };
2400
2373
 
2401
- var noColorValue = {
2374
+ function* updateImportStatement(context, fixer, key) {
2375
+ const sourceCode = context.sourceCode;
2376
+ const importNode = sourceCode.ast.body.find(
2377
+ node => node.type === 'ImportDeclaration' && node.source.value === 'react'
2378
+ );
2379
+ if (!importNode) {
2380
+ yield fixer.insertTextBefore(
2381
+ sourceCode.ast.body[0],
2382
+ `import { ${key} } from 'react';\n`
2383
+ );
2384
+ return;
2385
+ }
2386
+ if (
2387
+ importNode.specifiers.length === 1 &&
2388
+ importNode.specifiers[0].type === 'ImportDefaultSpecifier'
2389
+ ) {
2390
+ yield fixer.insertTextAfter(importNode.specifiers[0], `, { ${key} }`);
2391
+ return;
2392
+ }
2393
+ const alreadyImportedKeys = importNode.specifiers
2394
+ .filter(specifier => specifier.type === 'ImportSpecifier')
2395
+ .map(specifier => specifier.imported.name);
2396
+ if (alreadyImportedKeys.includes(key)) {
2397
+ return;
2398
+ }
2399
+ yield fixer.insertTextAfter([...importNode.specifiers].pop(), `, ${key}`);
2400
+ }
2401
+ var preferNamedPropertyAccess = utils.ESLintUtils.RuleCreator.withoutDocs({
2402
+ defaultOptions: [],
2402
2403
  meta: {
2403
- type: 'problem',
2404
+ type: 'layout',
2405
+ fixable: 'code',
2404
2406
  docs: {
2405
2407
  description:
2406
- 'Enforce the use of color variables instead of color codes within TSS'
2407
- }
2408
+ 'Enforce importing each member of React namespace separately instead of accessing them through React namespace'
2409
+ },
2410
+ messages: {
2411
+ illegalReactPropertyAccess:
2412
+ 'Illegal React property access: {{name}}. Use named import instead.'
2413
+ },
2414
+ schema: []
2408
2415
  },
2409
- create: function (context) {
2410
- const parserOptions = context.parserOptions;
2411
- if (!parserOptions || !parserOptions.project) {
2412
- return {};
2413
- }
2416
+ create(context) {
2414
2417
  return {
2415
- CallExpression(node) {
2416
- const stylesObj = getStyesObj(node);
2417
- if (!stylesObj) {
2418
+ TSQualifiedName(node) {
2419
+ if (
2420
+ ('name' in node.left && node.left.name !== 'React') ||
2421
+ ('name' in node.right && node.right.name.endsWith('Event'))
2422
+ ) {
2418
2423
  return;
2419
2424
  }
2420
- function checkColorLiteral(value) {
2421
- if (value.type === 'Literal' && typeof value.value === 'string') {
2422
- const colorCodePattern =
2423
- /#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\?\(\s*(\d{1,3}\s*,\s*){2}\d{1,3}(?:\s*,\s*\d*(?:\.\d+)?)?\s*\)/g;
2424
- const isColorCode = colorCodePattern.test(value.value);
2425
- if (isColorCode) {
2426
- context.report({
2427
- node: value,
2428
- message: 'Use color variables instead of color codes in TSS.'
2429
- });
2430
- }
2425
+ context.report({
2426
+ node,
2427
+ messageId: 'illegalReactPropertyAccess',
2428
+ data: {
2429
+ name: node.right.name
2430
+ },
2431
+ *fix(fixer) {
2432
+ yield fixer.replaceText(node, node.right.name);
2433
+ yield* updateImportStatement(context, fixer, node.right.name);
2431
2434
  }
2435
+ });
2436
+ },
2437
+ MemberExpression(node) {
2438
+ if (node.object.name !== 'React') {
2439
+ return;
2432
2440
  }
2433
- function loopStylesObj(obj) {
2434
- if (obj && obj.type === 'ObjectExpression') {
2435
- obj.properties.forEach(property => {
2436
- if (property.type === 'Property' && property.value) {
2437
- if (property.value.type === 'ObjectExpression') {
2438
- loopStylesObj(property.value);
2439
- } else {
2440
- checkColorLiteral(property.value);
2441
- }
2442
- }
2443
- });
2441
+ context.report({
2442
+ node,
2443
+ messageId: 'illegalReactPropertyAccess',
2444
+ data: {
2445
+ name: node.property.name
2446
+ },
2447
+ *fix(fixer) {
2448
+ yield fixer.replaceText(node, node.property.name);
2449
+ yield* updateImportStatement(context, fixer, node.property.name);
2444
2450
  }
2445
- }
2446
- loopStylesObj(stylesObj);
2451
+ });
2447
2452
  }
2448
2453
  };
2449
2454
  }
2450
- };
2455
+ });
2451
2456
 
2452
- var unusedClasses = {
2457
+ var preferSxProp = {
2453
2458
  meta: {
2454
- type: 'problem'
2455
- },
2456
- create: function rule(context) {
2457
- const usedClasses = {};
2458
- const definedClasses = {};
2459
- return {
2460
- CallExpression(node) {
2461
- const stylesObj = getStyesObj(node);
2462
- if (stylesObj === undefined) {
2463
- return;
2464
- }
2465
- stylesObj.properties.forEach(property => {
2466
- if (property.computed) {
2467
- return;
2468
- }
2469
- if (
2470
- property.type === 'ExperimentalSpreadProperty' ||
2471
- property.type === 'SpreadElement'
2472
- ) {
2473
- return;
2474
- }
2475
- definedClasses[property.key.value || property.key.name] = property;
2476
- });
2477
- },
2478
- MemberExpression(node) {
2479
- if (
2480
- node.object.type === 'Identifier' &&
2481
- node.object.name === 'classes'
2482
- ) {
2483
- const whichClass = getBasicIdentifier(node.property);
2484
- if (whichClass) {
2485
- usedClasses[whichClass] = true;
2459
+ docs: {
2460
+ description: 'Prefer using sx prop instead of inline styles',
2461
+ category: 'Best Practices',
2462
+ recommended: false
2463
+ },
2464
+ messages: {
2465
+ preferSxProp:
2466
+ 'Avoid using inline styles, use sx prop or tss-react or styled-component instead'
2467
+ },
2468
+ schema: [
2469
+ {
2470
+ type: 'object',
2471
+ properties: {
2472
+ allowedFor: {
2473
+ type: 'array',
2474
+ uniqueItems: true,
2475
+ items: { type: 'string' }
2486
2476
  }
2487
- return;
2488
- }
2489
- const classIdentifier = getBasicIdentifier(node.property);
2490
- if (!classIdentifier) {
2491
- return;
2492
- }
2493
- if (classIdentifier !== 'classes') {
2494
- return;
2495
- }
2496
- const { parent } = node;
2497
- if (parent.type !== 'MemberExpression') {
2498
- return;
2499
- }
2500
- if (
2501
- node.object.object &&
2502
- node.object.object.type !== 'ThisExpression'
2503
- ) {
2504
- return;
2505
- }
2506
- const propsIdentifier = getBasicIdentifier(parent.object);
2507
- if (propsIdentifier && propsIdentifier !== 'props') {
2508
- return;
2509
- }
2510
- if (!propsIdentifier && parent.object.type !== 'MemberExpression') {
2511
- return;
2512
- }
2513
- if (parent.parent.type === 'MemberExpression') {
2514
- return;
2515
2477
  }
2516
- const parentClassIdentifier = getBasicIdentifier(parent.property);
2517
- if (parentClassIdentifier) {
2518
- usedClasses[parentClassIdentifier] = true;
2519
- }
2520
- },
2521
- 'Program:exit': () => {
2522
- Object.keys(definedClasses).forEach(definedClassKey => {
2523
- if (!usedClasses[definedClassKey]) {
2524
- context.report({
2525
- node: definedClasses[definedClassKey],
2526
- message: `Class \`${definedClassKey}\` is unused`
2527
- });
2528
- }
2478
+ }
2479
+ ]
2480
+ },
2481
+ create(context) {
2482
+ const configuration = context.options[0] || {};
2483
+ const allowedFor = configuration.allowedFor || [];
2484
+ function checkComponent(node) {
2485
+ const parentName = node.parent.name;
2486
+ const tag =
2487
+ parentName.name ||
2488
+ `${parentName.object.name}.${parentName.property.name}`;
2489
+ const componentName = parentName.name || parentName.property.name;
2490
+ if (
2491
+ componentName &&
2492
+ typeof componentName[0] === 'string' &&
2493
+ componentName[0] !== componentName[0].toUpperCase()
2494
+ ) {
2495
+ return;
2496
+ }
2497
+ if (allowedFor.includes(tag)) {
2498
+ return;
2499
+ }
2500
+ const prop = node.name.name;
2501
+ if (prop === 'style') {
2502
+ context.report({
2503
+ node,
2504
+ messageId: 'preferSxProp'
2505
+ });
2506
+ }
2507
+ }
2508
+ function checkDOMNodes(node) {
2509
+ const tag = node.parent.name.name;
2510
+ if (
2511
+ !(tag && typeof tag === 'string' && tag[0] !== tag[0].toUpperCase())
2512
+ ) {
2513
+ return;
2514
+ }
2515
+ if (allowedFor.includes(tag)) {
2516
+ return;
2517
+ }
2518
+ const prop = node.name.name;
2519
+ if (prop === 'style') {
2520
+ context.report({
2521
+ node,
2522
+ messageId: 'preferSxProp'
2529
2523
  });
2530
2524
  }
2525
+ }
2526
+ return {
2527
+ JSXAttribute(node) {
2528
+ checkComponent(node);
2529
+ checkDOMNodes(node);
2530
+ }
2531
2531
  };
2532
2532
  }
2533
2533
  };