@agilebot/eslint-plugin 0.2.6 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
  };