oxlint-plugin-arch 0.7.0 → 0.9.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.
- package/CHANGELOG.md +9 -0
- package/README.md +5 -1
- package/dist/constants/preset.constants.d.ts +5 -1
- package/dist/index.js +5 -0
- package/dist/presets/index.d.ts +1 -1
- package/dist/presets/index.js +44 -5
- package/dist/presets/next-react-modules-preset.js +44 -5
- package/dist/presets/react-modules-preset.js +44 -5
- package/dist/presets/tanstack-start-react-modules-preset.js +44 -5
- package/dist/rules/dillon-anti-slop/index.js +1155 -24
- package/dist/rules/dillon-anti-slop/rules/no-module-mocking.d.ts +7 -0
- package/dist/rules/module-policy/index.js +5 -0
- package/dist/types/preset.types.d.ts +21 -0
- package/dist/utils/helpers/index.d.ts +1 -0
- package/dist/utils/helpers/mocking.d.ts +2 -0
- package/package.json +1 -1
- package/src/rules/dillon-anti-slop/UPSTREAM.md +6 -0
|
@@ -594,7 +594,1039 @@ var noKnownValueWideningRule = defineRule3({
|
|
|
594
594
|
|
|
595
595
|
// src/rules/dillon-anti-slop/rules/no-module-mocking.ts
|
|
596
596
|
import { defineRule as defineRule4 } from "@oxlint/plugins";
|
|
597
|
+
|
|
598
|
+
// src/utils/helpers/ast.ts
|
|
599
|
+
var astVisitSkip = new Set(["parent", "type", "start", "end", "range", "loc", "tokens", "comments"]);
|
|
600
|
+
function astIsNode(value) {
|
|
601
|
+
return value !== null && typeof value === "object" && typeof Reflect.get(value, "type") === "string";
|
|
602
|
+
}
|
|
603
|
+
function astVisit(node, ancestors, callback) {
|
|
604
|
+
if (Array.isArray(node)) {
|
|
605
|
+
for (const child of node) {
|
|
606
|
+
astVisit(child, ancestors, callback);
|
|
607
|
+
}
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (!astIsNode(node)) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
callback(node, ancestors);
|
|
614
|
+
ancestors.push(node);
|
|
615
|
+
for (const key in node) {
|
|
616
|
+
if (Object.hasOwn(node, key) && !astVisitSkip.has(key)) {
|
|
617
|
+
astVisit(Reflect.get(node, key), ancestors, callback);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
ancestors.pop();
|
|
621
|
+
}
|
|
622
|
+
function astContainsJsx(node) {
|
|
623
|
+
let found = false;
|
|
624
|
+
astVisit(node, [], (candidate) => {
|
|
625
|
+
if (candidate.type === "JSXElement" || candidate.type === "JSXFragment") {
|
|
626
|
+
found = true;
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
return found;
|
|
630
|
+
}
|
|
631
|
+
function astNearestFunction(ancestors) {
|
|
632
|
+
return [...ancestors].reverse().find((node) => node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression");
|
|
633
|
+
}
|
|
634
|
+
function astDirectReturnExpressions(node) {
|
|
635
|
+
if (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") {
|
|
636
|
+
return [node.body];
|
|
637
|
+
}
|
|
638
|
+
const expressions = [];
|
|
639
|
+
astVisit(node.body, [node], (candidate, ancestors) => {
|
|
640
|
+
if (candidate.type === "ReturnStatement" && candidate.argument && astNearestFunction(ancestors) === node) {
|
|
641
|
+
expressions.push(candidate.argument);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
return expressions;
|
|
645
|
+
}
|
|
646
|
+
function astStaticMemberName(node) {
|
|
647
|
+
return !node.computed && node.property.type === "Identifier" ? node.property.name : undefined;
|
|
648
|
+
}
|
|
649
|
+
function astDottedName(node) {
|
|
650
|
+
if (node.type === "Identifier") {
|
|
651
|
+
return node.name;
|
|
652
|
+
}
|
|
653
|
+
if (node.type !== "MemberExpression") {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const object = astDottedName(node.object);
|
|
657
|
+
const property = astStaticMemberName(node);
|
|
658
|
+
return object && property ? `${object}.${property}` : undefined;
|
|
659
|
+
}
|
|
660
|
+
function astCallName(node) {
|
|
661
|
+
if (node.callee.type === "Identifier") {
|
|
662
|
+
return node.callee.name;
|
|
663
|
+
}
|
|
664
|
+
return node.callee.type === "MemberExpression" ? astStaticMemberName(node.callee) : undefined;
|
|
665
|
+
}
|
|
666
|
+
function astImportedCallAliases(program) {
|
|
667
|
+
const aliases = new Map;
|
|
668
|
+
for (const statement of program.body) {
|
|
669
|
+
if (statement.type !== "ImportDeclaration") {
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
for (const specifier of statement.specifiers) {
|
|
673
|
+
if (specifier.type !== "ImportSpecifier") {
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
const imported = specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value;
|
|
677
|
+
if (imported !== specifier.local.name) {
|
|
678
|
+
aliases.set(specifier.local.name, imported);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return aliases;
|
|
683
|
+
}
|
|
684
|
+
function astResolvedCallName(node, aliases) {
|
|
685
|
+
const name = astCallName(node);
|
|
686
|
+
return name ? aliases.get(name) ?? name : undefined;
|
|
687
|
+
}
|
|
688
|
+
// src/utils/helpers/comments.ts
|
|
689
|
+
var COMMENTS_DIRECTIVE = /^\s*(?:\/\s*<(?:reference\b|amd-[\w-]+\b)|@ts-(?:check|ignore|expect-error|nocheck)\b|(?:#|@)__(?:PURE|NO_SIDE_EFFECTS)__\b|@jsx(?:ImportSource|Runtime|Frag)?\b|@vite-ignore\b|(?:[#@]\s*)?sourceMappingURL=|(?:es|ox)lint-(?:disable|enable)|prettier-ignore|biome-ignore|[cv]8 ignore|istanbul ignore|webpack[A-Z])/;
|
|
690
|
+
var COMMENTS_DISABLE_NEXT_LINE = /^(?:eslint|oxlint)-disable-next-line\b/;
|
|
691
|
+
var COMMENTS_LEGAL_BLOCK = "\x00legal-block\x00";
|
|
692
|
+
function commentsNormalizedValue(comment) {
|
|
693
|
+
const value = (comment.type === "Block" ? comment.value.replace(/^\s*\* ?/gm, "") : comment.value).trimStart();
|
|
694
|
+
return comment.type === "Block" && comment.value.startsWith("!") ? COMMENTS_LEGAL_BLOCK + value : value;
|
|
695
|
+
}
|
|
696
|
+
function commentsIsDirective(value) {
|
|
697
|
+
return value.startsWith(COMMENTS_LEGAL_BLOCK) || COMMENTS_DIRECTIVE.test(value);
|
|
698
|
+
}
|
|
699
|
+
function commentsIsDisableNextLine(value) {
|
|
700
|
+
return COMMENTS_DISABLE_NEXT_LINE.test(value);
|
|
701
|
+
}
|
|
702
|
+
function commentsIsWhy(value) {
|
|
703
|
+
return value.startsWith("Why: ");
|
|
704
|
+
}
|
|
705
|
+
function commentsIsJsdoc(source, comment) {
|
|
706
|
+
return comment.type === "Block" && source.startsWith("/**", comment.range[0]);
|
|
707
|
+
}
|
|
708
|
+
// src/utils/helpers/trivial.ts
|
|
709
|
+
function trivialExpression(node) {
|
|
710
|
+
if (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") {
|
|
711
|
+
return node.body;
|
|
712
|
+
}
|
|
713
|
+
if (!node.body || node.body.type !== "BlockStatement")
|
|
714
|
+
return null;
|
|
715
|
+
const statements = node.body.body.filter((statement2) => statement2.type !== "EmptyStatement" && !(statement2.type === "ExpressionStatement" && statement2.directive));
|
|
716
|
+
if (statements.length !== 1)
|
|
717
|
+
return null;
|
|
718
|
+
const statement = statements[0];
|
|
719
|
+
if (statement.type === "ReturnStatement")
|
|
720
|
+
return statement.argument;
|
|
721
|
+
return statement.type === "ExpressionStatement" ? statement.expression : null;
|
|
722
|
+
}
|
|
723
|
+
function trivialUnwrap(expression) {
|
|
724
|
+
if (expression.type === "ParenthesizedExpression" || expression.type === "TSAsExpression" || expression.type === "TSSatisfiesExpression" || expression.type === "TSTypeAssertion" || expression.type === "TSNonNullExpression" || expression.type === "ChainExpression") {
|
|
725
|
+
return trivialUnwrap(expression.expression);
|
|
726
|
+
}
|
|
727
|
+
return expression.type === "AwaitExpression" ? trivialUnwrap(expression.argument) : expression;
|
|
728
|
+
}
|
|
729
|
+
function trivialCallRoot(expression) {
|
|
730
|
+
if (expression.type === "Identifier")
|
|
731
|
+
return expression.name;
|
|
732
|
+
if (expression.type === "MemberExpression" && !expression.computed && !expression.optional) {
|
|
733
|
+
return trivialCallRoot(expression.object);
|
|
734
|
+
}
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
function trivialIsForwarder(node, expression) {
|
|
738
|
+
const value = trivialUnwrap(expression);
|
|
739
|
+
if (value.type === "Literal")
|
|
740
|
+
return !("regex" in value);
|
|
741
|
+
if (value.type === "TemplateLiteral")
|
|
742
|
+
return value.expressions.length === 0;
|
|
743
|
+
if (value.type === "Identifier")
|
|
744
|
+
return true;
|
|
745
|
+
if (value.type !== "CallExpression" || value.optional)
|
|
746
|
+
return false;
|
|
747
|
+
const root = trivialCallRoot(value.callee);
|
|
748
|
+
if (!root || node.params.some((param) => param.type !== "Identifier"))
|
|
749
|
+
return false;
|
|
750
|
+
const parameters = node.params.flatMap((param) => param.type === "Identifier" ? [param.name] : []);
|
|
751
|
+
const arguments_ = value.arguments;
|
|
752
|
+
const receiver = value.callee.type === "MemberExpression" ? root : undefined;
|
|
753
|
+
const forwarded = parameters.filter((name) => name !== receiver);
|
|
754
|
+
return arguments_.length === forwarded.length && arguments_.every((argument, index) => argument.type === "Identifier" && argument.name === forwarded[index]);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/utils/helpers/declarations.ts
|
|
758
|
+
function declarationsTopLevelUnexportedFunctions(program) {
|
|
759
|
+
return program.body.flatMap((statement) => {
|
|
760
|
+
if (statement.type === "FunctionDeclaration") {
|
|
761
|
+
return [{ name: statement.id?.name ?? "<anonymous>", node: statement }];
|
|
762
|
+
}
|
|
763
|
+
if (statement.type !== "VariableDeclaration") {
|
|
764
|
+
return [];
|
|
765
|
+
}
|
|
766
|
+
return statement.declarations.flatMap((item) => item.init?.type === "FunctionExpression" || item.init?.type === "ArrowFunctionExpression" ? [{ name: item.id.type === "Identifier" ? item.id.name : "<anonymous>", node: item.init }] : []);
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
function declarationsFileLevelFunctionCandidates(program) {
|
|
770
|
+
const result = [];
|
|
771
|
+
for (const statement of program.body) {
|
|
772
|
+
const declaration = statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" ? statement.declaration : statement;
|
|
773
|
+
if (declaration?.type === "FunctionDeclaration") {
|
|
774
|
+
result.push({ name: declaration.id?.name ?? "<anonymous>", node: declaration });
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
if (declaration?.type !== "VariableDeclaration" || declaration.kind !== "const") {
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
for (const item of declaration.declarations) {
|
|
781
|
+
if (item.init?.type === "FunctionExpression" || item.init?.type === "ArrowFunctionExpression") {
|
|
782
|
+
result.push({
|
|
783
|
+
name: item.id.type === "Identifier" ? item.id.name : "<anonymous>",
|
|
784
|
+
node: declaration,
|
|
785
|
+
initializer: item.init
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return result;
|
|
791
|
+
}
|
|
792
|
+
function declarationsIsObjectParam(node) {
|
|
793
|
+
if (node.type === "ObjectPattern") {
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
796
|
+
if (node.type === "TSParameterProperty") {
|
|
797
|
+
return declarationsIsObjectParam(node.parameter);
|
|
798
|
+
}
|
|
799
|
+
const annotation = node.typeAnnotation?.typeAnnotation;
|
|
800
|
+
return !annotation || annotation.type === "TSTypeReference" || annotation.type === "TSTypeLiteral" || annotation.type === "TSIntersectionType";
|
|
801
|
+
}
|
|
802
|
+
function declarationsFunctionName(node) {
|
|
803
|
+
if (node.type === "VariableDeclarator") {
|
|
804
|
+
return node.id.type === "Identifier" ? node.id.name : "<anonymous>";
|
|
805
|
+
}
|
|
806
|
+
return node.id?.name ?? "<anonymous>";
|
|
807
|
+
}
|
|
808
|
+
function declarationsIsRuntime(node) {
|
|
809
|
+
return node.type === "VariableDeclaration" || node.type === "FunctionDeclaration" || node.type === "ClassDeclaration" || node.type === "TSEnumDeclaration";
|
|
810
|
+
}
|
|
811
|
+
function declarationsRuntimeName(node) {
|
|
812
|
+
if (node.type === "VariableDeclaration") {
|
|
813
|
+
return node.declarations.map((item) => item.id.type === "Identifier" ? item.id.name : "<anonymous>").join(", ");
|
|
814
|
+
}
|
|
815
|
+
return node.id?.name ?? "<anonymous>";
|
|
816
|
+
}
|
|
817
|
+
function declarationsRuntimeKind(node) {
|
|
818
|
+
if (node.type === "ClassDeclaration") {
|
|
819
|
+
return "class";
|
|
820
|
+
}
|
|
821
|
+
if (node.type === "TSEnumDeclaration") {
|
|
822
|
+
return "enum";
|
|
823
|
+
}
|
|
824
|
+
if (node.type === "FunctionDeclaration") {
|
|
825
|
+
return "function";
|
|
826
|
+
}
|
|
827
|
+
return "variable";
|
|
828
|
+
}
|
|
829
|
+
function declarationsUnwrapExpression(node) {
|
|
830
|
+
if (node.type === "ParenthesizedExpression" || node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "TSTypeAssertion" || node.type === "TSNonNullExpression") {
|
|
831
|
+
return declarationsUnwrapExpression(node.expression);
|
|
832
|
+
}
|
|
833
|
+
if (node.type === "ChainExpression") {
|
|
834
|
+
return declarationsUnwrapExpression(node.expression);
|
|
835
|
+
}
|
|
836
|
+
return node.type === "AwaitExpression" && node.argument ? declarationsUnwrapExpression(node.argument) : node;
|
|
837
|
+
}
|
|
838
|
+
function declarationsIsTrivialExpression(node) {
|
|
839
|
+
const value = declarationsUnwrapExpression(node);
|
|
840
|
+
if (value.type === "Identifier" || value.type === "ThisExpression" || value.type === "MetaProperty" || value.type.endsWith("Literal")) {
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
if (value.type === "TemplateLiteral") {
|
|
844
|
+
return value.expressions.length === 0;
|
|
845
|
+
}
|
|
846
|
+
if (value.type === "MemberExpression" || value.type === "CallExpression" || value.type === "NewExpression" || value.type === "TaggedTemplateExpression") {
|
|
847
|
+
return true;
|
|
848
|
+
}
|
|
849
|
+
return value.type === "LogicalExpression" && declarationsIsTrivialExpression(value.left) && declarationsIsTrivialExpression(value.right);
|
|
850
|
+
}
|
|
851
|
+
function declarationsIsTrivialFunction(node) {
|
|
852
|
+
if (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") {
|
|
853
|
+
return declarationsIsTrivialExpression(node.body);
|
|
854
|
+
}
|
|
855
|
+
if (!node.body || node.body.type !== "BlockStatement") {
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
const statements = node.body.body.filter((statement2) => statement2.type !== "EmptyStatement" && !(statement2.type === "ExpressionStatement" && statement2.directive));
|
|
859
|
+
if (statements.length === 0) {
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
if (statements.length > 1) {
|
|
863
|
+
return false;
|
|
864
|
+
}
|
|
865
|
+
const statement = statements[0];
|
|
866
|
+
if (statement.type === "ReturnStatement") {
|
|
867
|
+
return !statement.argument || declarationsIsTrivialExpression(statement.argument);
|
|
868
|
+
}
|
|
869
|
+
return statement.type === "ExpressionStatement" && declarationsIsTrivialExpression(statement.expression);
|
|
870
|
+
}
|
|
871
|
+
var declarationsKinds = ["function", "const", "let", "var", "class", "type", "interface", "enum"];
|
|
872
|
+
function declarationsNamedKind(node, ancestors) {
|
|
873
|
+
if (node.type === "FunctionDeclaration") {
|
|
874
|
+
return "function";
|
|
875
|
+
}
|
|
876
|
+
if (node.type === "ClassDeclaration") {
|
|
877
|
+
return "class";
|
|
878
|
+
}
|
|
879
|
+
if (node.type === "TSTypeAliasDeclaration") {
|
|
880
|
+
return "type";
|
|
881
|
+
}
|
|
882
|
+
if (node.type === "TSInterfaceDeclaration") {
|
|
883
|
+
return "interface";
|
|
884
|
+
}
|
|
885
|
+
if (node.type === "TSEnumDeclaration") {
|
|
886
|
+
return "enum";
|
|
887
|
+
}
|
|
888
|
+
if (node.type !== "VariableDeclarator") {
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
const declaration = ancestors.findLast((ancestor) => ancestor.type === "VariableDeclaration");
|
|
892
|
+
return declaration?.type === "VariableDeclaration" && (declaration.kind === "const" || declaration.kind === "let" || declaration.kind === "var") ? declaration.kind : undefined;
|
|
893
|
+
}
|
|
894
|
+
function declarationsCollectNamed(root, kinds) {
|
|
895
|
+
const allowed = kinds && kinds.length > 0 ? new Set(kinds) : null;
|
|
896
|
+
const result = [];
|
|
897
|
+
astVisit(root, [], (node, ancestors) => {
|
|
898
|
+
const kind = declarationsNamedKind(node, ancestors);
|
|
899
|
+
const name = node.type === "VariableDeclarator" && node.id.type === "Identifier" ? node.id.name : ("id" in node) && node.id && ("name" in node.id) ? node.id.name : undefined;
|
|
900
|
+
if (!kind || !name || allowed && !allowed.has(kind)) {
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (node.type === "VariableDeclarator" && ancestors.some((ancestor) => ancestor.type === "ForStatement" || ancestor.type === "ForInStatement" || ancestor.type === "ForOfStatement")) {
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
result.push({ name, node, kind });
|
|
907
|
+
});
|
|
908
|
+
return result;
|
|
909
|
+
}
|
|
910
|
+
function declarationsInsideForLoop(node) {
|
|
911
|
+
for (let current = node.parent;current; current = current.parent) {
|
|
912
|
+
if (current.type === "ForStatement" || current.type === "ForInStatement" || current.type === "ForOfStatement") {
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
function declarationsNamedFromNode(node) {
|
|
919
|
+
if (node.type === "VariableDeclarator") {
|
|
920
|
+
const kind2 = declarationsNamedKind(node, [node.parent]);
|
|
921
|
+
return kind2 && node.id.type === "Identifier" && !declarationsInsideForLoop(node) ? { name: node.id.name, node, kind: kind2 } : undefined;
|
|
922
|
+
}
|
|
923
|
+
const kind = declarationsNamedKind(node, []);
|
|
924
|
+
const name = "id" in node && node.id && "name" in node.id ? node.id.name : undefined;
|
|
925
|
+
return kind && name ? { name, node, kind } : undefined;
|
|
926
|
+
}
|
|
927
|
+
function declarationsCollectFunctions(root) {
|
|
928
|
+
return declarationsCollectNamed(root).flatMap((item) => {
|
|
929
|
+
if (item.node.type === "FunctionDeclaration") {
|
|
930
|
+
return [{ name: item.name, node: item.node }];
|
|
931
|
+
}
|
|
932
|
+
if (item.node.type === "VariableDeclarator" && (item.node.init?.type === "FunctionExpression" || item.node.init?.type === "ArrowFunctionExpression")) {
|
|
933
|
+
return [{ name: item.name, node: item.node.init }];
|
|
934
|
+
}
|
|
935
|
+
return [];
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
function declarationsPublicServiceMethods(program) {
|
|
939
|
+
return program.body.flatMap((statement) => {
|
|
940
|
+
if (statement.type !== "ExportNamedDeclaration" || statement.declaration?.type !== "VariableDeclaration" || statement.declaration.kind !== "const")
|
|
941
|
+
return [];
|
|
942
|
+
return statement.declaration.declarations.flatMap((binding) => {
|
|
943
|
+
if (binding.id.type !== "Identifier" || !binding.init)
|
|
944
|
+
return [];
|
|
945
|
+
const value = trivialUnwrap(binding.init);
|
|
946
|
+
if (value.type !== "ObjectExpression" || value.properties.length === 0)
|
|
947
|
+
return [];
|
|
948
|
+
const methods = value.properties.flatMap((property) => property.type === "Property" && property.method && property.kind === "init" && !property.computed && property.key.type === "Identifier" && property.value.type === "FunctionExpression" ? [{ name: property.key.name, node: property.value, binding }] : []);
|
|
949
|
+
return methods.length === value.properties.length ? methods : [];
|
|
950
|
+
});
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
// src/utils/helpers/exports.ts
|
|
954
|
+
function moduleExportName(node) {
|
|
955
|
+
return node.type === "Identifier" ? node.name : node.value;
|
|
956
|
+
}
|
|
957
|
+
function declarationBindings(declaration) {
|
|
958
|
+
if (!declaration || declaration.type === "TSModuleDeclaration") {
|
|
959
|
+
return [];
|
|
960
|
+
}
|
|
961
|
+
if (declaration.type === "VariableDeclaration") {
|
|
962
|
+
return declaration.declarations.map((item) => ({
|
|
963
|
+
exportedName: item.id.type === "Identifier" ? item.id.name : "<anonymous>",
|
|
964
|
+
localName: item.id.type === "Identifier" ? item.id.name : undefined,
|
|
965
|
+
node: declaration,
|
|
966
|
+
declaration,
|
|
967
|
+
initializer: item.init,
|
|
968
|
+
typeOnly: false,
|
|
969
|
+
default: false,
|
|
970
|
+
reExport: false
|
|
971
|
+
}));
|
|
972
|
+
}
|
|
973
|
+
const name = declaration.id?.name ?? "<anonymous>";
|
|
974
|
+
return [
|
|
975
|
+
{
|
|
976
|
+
exportedName: name,
|
|
977
|
+
localName: declaration.id?.name,
|
|
978
|
+
node: declaration,
|
|
979
|
+
declaration,
|
|
980
|
+
typeOnly: declaration.type === "TSInterfaceDeclaration" || declaration.type === "TSTypeAliasDeclaration" || declaration.type === "TSDeclareFunction",
|
|
981
|
+
default: false,
|
|
982
|
+
reExport: false
|
|
983
|
+
}
|
|
984
|
+
];
|
|
985
|
+
}
|
|
986
|
+
function localBindings(program) {
|
|
987
|
+
const result = new Map;
|
|
988
|
+
for (const statement of program.body) {
|
|
989
|
+
if (statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration") {
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
if (statement.type.endsWith("Declaration")) {
|
|
993
|
+
for (const binding of declarationBindings(statement)) {
|
|
994
|
+
if (binding.localName) {
|
|
995
|
+
result.set(binding.localName, binding);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return result;
|
|
1001
|
+
}
|
|
1002
|
+
function exportsCollect(program) {
|
|
1003
|
+
const locals = localBindings(program);
|
|
1004
|
+
return program.body.flatMap((statement) => {
|
|
1005
|
+
if (statement.type === "ExportAllDeclaration") {
|
|
1006
|
+
return [
|
|
1007
|
+
{
|
|
1008
|
+
exportedName: "*",
|
|
1009
|
+
node: statement,
|
|
1010
|
+
typeOnly: statement.exportKind === "type",
|
|
1011
|
+
default: false,
|
|
1012
|
+
reExport: true
|
|
1013
|
+
}
|
|
1014
|
+
];
|
|
1015
|
+
}
|
|
1016
|
+
if (statement.type === "ExportDefaultDeclaration") {
|
|
1017
|
+
if (statement.declaration.type === "Identifier") {
|
|
1018
|
+
const local = locals.get(statement.declaration.name);
|
|
1019
|
+
return [
|
|
1020
|
+
{
|
|
1021
|
+
...local ?? {
|
|
1022
|
+
exportedName: "default",
|
|
1023
|
+
node: statement,
|
|
1024
|
+
typeOnly: false,
|
|
1025
|
+
default: true,
|
|
1026
|
+
reExport: false
|
|
1027
|
+
},
|
|
1028
|
+
exportedName: "default",
|
|
1029
|
+
localName: local?.localName ?? statement.declaration.name,
|
|
1030
|
+
default: true
|
|
1031
|
+
}
|
|
1032
|
+
];
|
|
1033
|
+
}
|
|
1034
|
+
if (statement.declaration.type.endsWith("Declaration")) {
|
|
1035
|
+
const bindings = declarationBindings(statement.declaration);
|
|
1036
|
+
return bindings.length > 0 ? bindings.map((binding) => ({ ...binding, exportedName: "default", default: true })) : [{ exportedName: "default", node: statement, typeOnly: false, default: true, reExport: false }];
|
|
1037
|
+
}
|
|
1038
|
+
return [{ exportedName: "default", node: statement, typeOnly: false, default: true, reExport: false }];
|
|
1039
|
+
}
|
|
1040
|
+
if (statement.type !== "ExportNamedDeclaration") {
|
|
1041
|
+
return [];
|
|
1042
|
+
}
|
|
1043
|
+
return [
|
|
1044
|
+
...declarationBindings(statement.declaration),
|
|
1045
|
+
...statement.specifiers.map((specifier) => {
|
|
1046
|
+
const localName = moduleExportName(specifier.local);
|
|
1047
|
+
const local = locals.get(localName);
|
|
1048
|
+
return {
|
|
1049
|
+
...local ?? {
|
|
1050
|
+
exportedName: moduleExportName(specifier.exported),
|
|
1051
|
+
node: specifier,
|
|
1052
|
+
typeOnly: false,
|
|
1053
|
+
default: false,
|
|
1054
|
+
reExport: Boolean(statement.source)
|
|
1055
|
+
},
|
|
1056
|
+
exportedName: moduleExportName(specifier.exported),
|
|
1057
|
+
localName,
|
|
1058
|
+
node: local?.node ?? specifier,
|
|
1059
|
+
typeOnly: specifier.exportKind === "type" || statement.exportKind === "type" || Boolean(local?.typeOnly),
|
|
1060
|
+
default: moduleExportName(specifier.exported) === "default",
|
|
1061
|
+
reExport: Boolean(statement.source)
|
|
1062
|
+
};
|
|
1063
|
+
})
|
|
1064
|
+
];
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
function exportsCollectFunctions(program) {
|
|
1068
|
+
return exportsCollect(program).flatMap((binding) => {
|
|
1069
|
+
const name = binding.localName ?? binding.exportedName;
|
|
1070
|
+
if (binding.declaration?.type === "FunctionDeclaration") {
|
|
1071
|
+
return [{ name, node: binding.declaration }];
|
|
1072
|
+
}
|
|
1073
|
+
return binding.initializer?.type === "FunctionExpression" || binding.initializer?.type === "ArrowFunctionExpression" ? [{ name, node: binding.initializer }] : [];
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
// src/utils/helpers/naming.ts
|
|
1077
|
+
function namingPosixPath(filePath) {
|
|
1078
|
+
return filePath.replaceAll("\\", "/");
|
|
1079
|
+
}
|
|
1080
|
+
function namingFileBasename(filePath) {
|
|
1081
|
+
const normalized = namingPosixPath(filePath);
|
|
1082
|
+
const slash = normalized.lastIndexOf("/");
|
|
1083
|
+
return slash === -1 ? normalized : normalized.slice(slash + 1);
|
|
1084
|
+
}
|
|
1085
|
+
function namingDirSegments(filePath) {
|
|
1086
|
+
const parts = namingPosixPath(filePath).split("/").filter(Boolean);
|
|
1087
|
+
return parts.slice(0, -1);
|
|
1088
|
+
}
|
|
1089
|
+
function namingSegmentsAfter(filePath, after) {
|
|
1090
|
+
const dirs = namingDirSegments(filePath);
|
|
1091
|
+
const marker = after.split("/").filter(Boolean);
|
|
1092
|
+
if (marker.length === 0) {
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
for (let index = dirs.length - marker.length;index >= 0; index -= 1) {
|
|
1096
|
+
if (marker.every((part, offset) => dirs[index + offset] === part)) {
|
|
1097
|
+
return dirs.slice(index + marker.length);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
function namingFolderPrefixes(folders, separator, singularize) {
|
|
1102
|
+
const joined = folders.join(separator);
|
|
1103
|
+
const last = folders.at(-1) ?? "";
|
|
1104
|
+
const prefixes = [joined];
|
|
1105
|
+
if (singularize === "trailing-s" && last.endsWith("s") && last.length > 1) {
|
|
1106
|
+
prefixes.push([...folders.slice(0, -1), last.slice(0, -1)].join(separator));
|
|
1107
|
+
}
|
|
1108
|
+
return prefixes.filter(Boolean);
|
|
1109
|
+
}
|
|
1110
|
+
function namingPascalCase(value) {
|
|
1111
|
+
return value.split(/[-_\s.]+/).filter(Boolean).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("");
|
|
1112
|
+
}
|
|
1113
|
+
function namingCamelCase(value) {
|
|
1114
|
+
const next = namingPascalCase(value);
|
|
1115
|
+
return `${next.charAt(0).toLowerCase()}${next.slice(1)}`;
|
|
1116
|
+
}
|
|
1117
|
+
function namingMatchTemplate(basename, template, placeholderPattern = "[a-z0-9-]+") {
|
|
1118
|
+
let pattern = "";
|
|
1119
|
+
let index = 0;
|
|
1120
|
+
while (index < template.length) {
|
|
1121
|
+
const char = template[index];
|
|
1122
|
+
if (char === "{") {
|
|
1123
|
+
const end = template.indexOf("}", index);
|
|
1124
|
+
if (end === -1) {
|
|
1125
|
+
return null;
|
|
1126
|
+
}
|
|
1127
|
+
const key = template.slice(index + 1, end);
|
|
1128
|
+
pattern += `(?<${key}>${placeholderPattern})`;
|
|
1129
|
+
index = end + 1;
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
pattern += /[.+^${}()|[\]\\]/.test(char) ? `\\${char}` : char;
|
|
1133
|
+
index += 1;
|
|
1134
|
+
}
|
|
1135
|
+
return new RegExp(`^${pattern}$`).exec(basename)?.groups ?? null;
|
|
1136
|
+
}
|
|
1137
|
+
var namingStemModes = ["before-first-dot", "full-basename"];
|
|
1138
|
+
function namingNormalized(value, normalize) {
|
|
1139
|
+
return normalize === "none" ? value : value.replaceAll(/[-_.]/g, "").toLowerCase();
|
|
1140
|
+
}
|
|
1141
|
+
function namingFileStem(basename, mode = "before-first-dot", trailingRoles = [], roleSeparators = [".", "-"]) {
|
|
1142
|
+
const stem = mode === "full-basename" ? basename : basename.split(".")[0] ?? "";
|
|
1143
|
+
const roles = [...new Set(trailingRoles.filter(Boolean))].sort((left, right) => right.length - left.length);
|
|
1144
|
+
const separators = [...new Set(roleSeparators.filter(Boolean))];
|
|
1145
|
+
if (roles.length === 0 || separators.length === 0) {
|
|
1146
|
+
return stem;
|
|
1147
|
+
}
|
|
1148
|
+
let remaining = stem;
|
|
1149
|
+
let changed = true;
|
|
1150
|
+
while (changed) {
|
|
1151
|
+
changed = false;
|
|
1152
|
+
for (const role of roles) {
|
|
1153
|
+
for (const separator of separators) {
|
|
1154
|
+
const suffix = `${separator}${role}`;
|
|
1155
|
+
if (remaining.length > suffix.length && remaining.endsWith(suffix)) {
|
|
1156
|
+
remaining = remaining.slice(0, -suffix.length);
|
|
1157
|
+
changed = true;
|
|
1158
|
+
break;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (changed) {
|
|
1162
|
+
break;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
return remaining;
|
|
1167
|
+
}
|
|
1168
|
+
function namingFilePrefixes(stem, normalize = "remove-separators", singularize = "none") {
|
|
1169
|
+
const prefixes = [namingNormalized(stem, normalize)];
|
|
1170
|
+
const parts = stem.split(/[-_]+/).filter(Boolean);
|
|
1171
|
+
const last = parts.at(-1) ?? "";
|
|
1172
|
+
if (singularize === "trailing-s" && last.endsWith("s") && last.length > 1) {
|
|
1173
|
+
prefixes.push(namingNormalized([...parts.slice(0, -1), last.slice(0, -1)].join("-"), normalize));
|
|
1174
|
+
}
|
|
1175
|
+
return [...new Set(prefixes.filter(Boolean))];
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// src/utils/helpers/factories.ts
|
|
1179
|
+
function factoriesExpandTemplate(template, groups, camelCase = false) {
|
|
1180
|
+
return template.replaceAll(/\{([^}]+)\}/g, (placeholder, key) => {
|
|
1181
|
+
const group = groups[key.toLowerCase()];
|
|
1182
|
+
if (!group) {
|
|
1183
|
+
return placeholder;
|
|
1184
|
+
}
|
|
1185
|
+
if (/^[A-Z]/.test(key)) {
|
|
1186
|
+
return namingPascalCase(group);
|
|
1187
|
+
}
|
|
1188
|
+
return camelCase ? namingCamelCase(group) : group;
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
function factoriesCollectTopLevel(program, pattern) {
|
|
1192
|
+
return program.body.flatMap((statement) => {
|
|
1193
|
+
const declaration = statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" ? statement.declaration : statement;
|
|
1194
|
+
if (declaration?.type === "FunctionDeclaration") {
|
|
1195
|
+
const name = declaration.id?.name ?? "<anonymous>";
|
|
1196
|
+
return pattern.test(name) ? [{ name, node: declaration }] : [];
|
|
1197
|
+
}
|
|
1198
|
+
if (declaration?.type !== "VariableDeclaration") {
|
|
1199
|
+
return [];
|
|
1200
|
+
}
|
|
1201
|
+
return declaration.declarations.flatMap((item) => item.id.type === "Identifier" && pattern.test(item.id.name) && (item.init?.type === "FunctionExpression" || item.init?.type === "ArrowFunctionExpression") ? [{ name: item.id.name, node: item.init }] : []);
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
function factoriesDirectlyReturnedObjects(factory) {
|
|
1205
|
+
if (factory.node.type === "ArrowFunctionExpression" && factory.node.body.type === "ObjectExpression") {
|
|
1206
|
+
return [factory.node.body];
|
|
1207
|
+
}
|
|
1208
|
+
const result = [];
|
|
1209
|
+
astVisit(factory.node.body, [factory.node], (node, ancestors) => {
|
|
1210
|
+
if (node.type === "ReturnStatement" && node.argument?.type === "ObjectExpression" && astNearestFunction(ancestors) === factory.node) {
|
|
1211
|
+
result.push(node.argument);
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
return result;
|
|
1215
|
+
}
|
|
1216
|
+
function factoriesObjectPropertyName(property) {
|
|
1217
|
+
if (property.type === "SpreadElement" || property.computed) {
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
if (property.key.type === "Identifier") {
|
|
1221
|
+
return property.key.name;
|
|
1222
|
+
}
|
|
1223
|
+
return property.key.type === "Literal" && typeof property.key.value === "string" ? property.key.value : undefined;
|
|
1224
|
+
}
|
|
1225
|
+
// src/utils/helpers/guards.ts
|
|
1226
|
+
var guardsPrimitiveNames = new Set([
|
|
1227
|
+
"string",
|
|
1228
|
+
"number",
|
|
1229
|
+
"boolean",
|
|
1230
|
+
"object",
|
|
1231
|
+
"undefined",
|
|
1232
|
+
"bigint",
|
|
1233
|
+
"symbol",
|
|
1234
|
+
"function"
|
|
1235
|
+
]);
|
|
1236
|
+
function guardsValue(node, state) {
|
|
1237
|
+
const value = trivialUnwrap(node);
|
|
1238
|
+
if (value.type === "Identifier")
|
|
1239
|
+
return state.values.has(value.name);
|
|
1240
|
+
return value.type === "MemberExpression" && value.computed && value.object.type === "Identifier" && state.values.has(value.object.name) && value.property.type === "Identifier" && state.values.has(value.property.name);
|
|
1241
|
+
}
|
|
1242
|
+
function guardsFallback(node) {
|
|
1243
|
+
return node.type === "Literal" && !("regex" in node) || node.type === "Identifier" && node.name === "undefined" || node.type === "ArrayExpression" && node.elements.length === 0 || node.type === "ObjectExpression" && node.properties.length === 0;
|
|
1244
|
+
}
|
|
1245
|
+
function guardsPredicate(expression, state) {
|
|
1246
|
+
const node = trivialUnwrap(expression);
|
|
1247
|
+
if (node.type === "Identifier")
|
|
1248
|
+
return state.predicates.has(node.name);
|
|
1249
|
+
if (node.type === "UnaryExpression" && node.operator === "!")
|
|
1250
|
+
return guardsPredicate(node.argument, state);
|
|
1251
|
+
if (node.type === "LogicalExpression" && (node.operator === "&&" || node.operator === "||")) {
|
|
1252
|
+
return guardsPredicate(node.left, state) && guardsPredicate(node.right, state);
|
|
1253
|
+
}
|
|
1254
|
+
if (node.type === "BinaryExpression" && ["===", "!==", "==", "!="].includes(node.operator)) {
|
|
1255
|
+
for (const [left, right] of [
|
|
1256
|
+
[node.left, node.right],
|
|
1257
|
+
[node.right, node.left]
|
|
1258
|
+
]) {
|
|
1259
|
+
if (left.type === "UnaryExpression" && left.operator === "typeof" && right.type === "Literal" && typeof right.value === "string" && guardsPrimitiveNames.has(right.value) && guardsValue(left.argument, state)) {
|
|
1260
|
+
state.primitive = true;
|
|
1261
|
+
return true;
|
|
1262
|
+
}
|
|
1263
|
+
if (left.type !== "PrivateIdentifier" && guardsValue(left, state) && (right.type === "Literal" && right.value === null || right.type === "Identifier" && right.name === "undefined"))
|
|
1264
|
+
return true;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
if (node.type === "CallExpression" && !node.optional && node.arguments.length === 1 && node.arguments[0].type !== "SpreadElement" && guardsValue(node.arguments[0], state) && node.callee.type === "MemberExpression" && !node.callee.computed && !node.callee.optional && node.callee.object.type === "Identifier" && node.callee.object.name === "Array" && node.callee.property.type === "Identifier" && node.callee.property.name === "isArray" && state.globalArray(node.callee.object)) {
|
|
1268
|
+
state.primitive = true;
|
|
1269
|
+
return true;
|
|
1270
|
+
}
|
|
1271
|
+
return false;
|
|
1272
|
+
}
|
|
1273
|
+
function guardsResult(node, state) {
|
|
1274
|
+
const value = trivialUnwrap(node);
|
|
1275
|
+
if (guardsValue(value, state) || guardsFallback(value) || guardsPredicate(value, state))
|
|
1276
|
+
return true;
|
|
1277
|
+
return value.type === "ConditionalExpression" && guardsPredicate(value.test, state) && guardsResult(value.consequent, state) && guardsResult(value.alternate, state);
|
|
1278
|
+
}
|
|
1279
|
+
function guardsStatement(node, state) {
|
|
1280
|
+
if (node.type === "EmptyStatement" || node.type === "ExpressionStatement" && Boolean(node.directive))
|
|
1281
|
+
return true;
|
|
1282
|
+
if (node.type === "ReturnStatement" && node.argument) {
|
|
1283
|
+
state.returned = true;
|
|
1284
|
+
return guardsResult(node.argument, state);
|
|
1285
|
+
}
|
|
1286
|
+
if (node.type === "BlockStatement")
|
|
1287
|
+
return node.body.every((statement) => guardsStatement(statement, state));
|
|
1288
|
+
if (node.type === "IfStatement") {
|
|
1289
|
+
return guardsPredicate(node.test, state) && guardsStatement(node.consequent, state) && (!node.alternate || guardsStatement(node.alternate, state));
|
|
1290
|
+
}
|
|
1291
|
+
if (node.type === "VariableDeclaration" && node.kind === "const") {
|
|
1292
|
+
return node.declarations.every((declaration) => {
|
|
1293
|
+
if (declaration.id.type !== "Identifier" || !declaration.init)
|
|
1294
|
+
return false;
|
|
1295
|
+
if (guardsValue(declaration.init, state)) {
|
|
1296
|
+
state.values.add(declaration.id.name);
|
|
1297
|
+
return true;
|
|
1298
|
+
}
|
|
1299
|
+
if (guardsPredicate(declaration.init, state)) {
|
|
1300
|
+
state.predicates.add(declaration.id.name);
|
|
1301
|
+
return true;
|
|
1302
|
+
}
|
|
1303
|
+
return guardsFallback(declaration.init);
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
function guardsIsGenericHelper(node, globalArray) {
|
|
1309
|
+
if (!node.body || node.params.length === 0 || node.params.some((param) => param.type !== "Identifier"))
|
|
1310
|
+
return false;
|
|
1311
|
+
const state = {
|
|
1312
|
+
values: new Set(node.params.flatMap((param) => param.type === "Identifier" ? [param.name] : [])),
|
|
1313
|
+
predicates: new Set,
|
|
1314
|
+
primitive: false,
|
|
1315
|
+
returned: false,
|
|
1316
|
+
globalArray
|
|
1317
|
+
};
|
|
1318
|
+
if (node.body.type !== "BlockStatement")
|
|
1319
|
+
return guardsResult(node.body, state) && state.primitive;
|
|
1320
|
+
return node.body.body.every((statement) => guardsStatement(statement, state)) && state.primitive && state.returned;
|
|
1321
|
+
}
|
|
1322
|
+
// src/utils/helpers/layout.ts
|
|
1323
|
+
var layoutIndentSchema = {
|
|
1324
|
+
anyOf: [
|
|
1325
|
+
{ type: "integer", minimum: 0 },
|
|
1326
|
+
{ type: "string", enum: ["tab"] }
|
|
1327
|
+
]
|
|
1328
|
+
};
|
|
1329
|
+
function layoutLineIndent(text, offset) {
|
|
1330
|
+
const lineStart = text.lastIndexOf(`
|
|
1331
|
+
`, offset - 1) + 1;
|
|
1332
|
+
return /^[\t ]*/.exec(text.slice(lineStart, offset))?.[0] ?? "";
|
|
1333
|
+
}
|
|
1334
|
+
function layoutIndentUnit(indent = 2) {
|
|
1335
|
+
return indent === "tab" ? "\t" : " ".repeat(indent);
|
|
1336
|
+
}
|
|
1337
|
+
function layoutNewline(text) {
|
|
1338
|
+
return text.includes(`\r
|
|
1339
|
+
`) ? `\r
|
|
1340
|
+
` : `
|
|
1341
|
+
`;
|
|
1342
|
+
}
|
|
1343
|
+
// src/utils/helpers/mocking.ts
|
|
1344
|
+
function mockingApiPatternsAssert(patterns) {
|
|
1345
|
+
if (patterns.some((pattern) => !/^(?:\*|(?:vi|jest|\*)\.(?:[A-Za-z_$][\w$]*|\*))$/.test(pattern)))
|
|
1346
|
+
throw new Error("Test mocking APIs must be `*` or `<vi|jest|*>.<method|*>`.");
|
|
1347
|
+
}
|
|
1348
|
+
// src/utils/helpers/options.ts
|
|
1349
|
+
function optionsFirst(context, fallback) {
|
|
1350
|
+
return context.options[0] ?? fallback;
|
|
1351
|
+
}
|
|
1352
|
+
var optionsPatternSchema = {
|
|
1353
|
+
anyOf: [{ type: "string" }, { type: "array", items: { type: "string" }, minItems: 1 }]
|
|
1354
|
+
};
|
|
1355
|
+
function optionsPatterns(pattern, flags) {
|
|
1356
|
+
return pattern === undefined ? [] : [pattern].flat().map((source) => new RegExp(source, flags));
|
|
1357
|
+
}
|
|
1358
|
+
function optionsOptionalPatterns(pattern, flags) {
|
|
1359
|
+
return optionsPatterns(pattern === undefined ? undefined : [pattern].flat().filter((source) => source !== ""), flags);
|
|
1360
|
+
}
|
|
1361
|
+
function optionsPatternLabel(pattern) {
|
|
1362
|
+
return pattern === undefined ? "" : [pattern].flat().join(" | ");
|
|
1363
|
+
}
|
|
1364
|
+
function optionsPatternsTest(patterns, value) {
|
|
1365
|
+
return patterns.some((pattern) => {
|
|
1366
|
+
pattern.lastIndex = 0;
|
|
1367
|
+
return pattern.test(value);
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
// src/utils/helpers/orpc.ts
|
|
1371
|
+
function orpcFindChainCall(expression, method) {
|
|
1372
|
+
if (expression.type === "CallExpression") {
|
|
1373
|
+
if (expression.callee.type === "MemberExpression" && astStaticMemberName(expression.callee) === method) {
|
|
1374
|
+
return expression;
|
|
1375
|
+
}
|
|
1376
|
+
if (expression.callee.type === "MemberExpression") {
|
|
1377
|
+
return orpcFindChainCall(expression.callee.object, method);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
if (expression.type === "MemberExpression") {
|
|
1381
|
+
return orpcFindChainCall(expression.object, method);
|
|
1382
|
+
}
|
|
1383
|
+
if (expression.type === "ChainExpression") {
|
|
1384
|
+
return orpcFindChainCall(expression.expression, method);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
function orpcIsNamedOrComposedArgument(argument, composers) {
|
|
1388
|
+
if (argument?.type === "Identifier") {
|
|
1389
|
+
return true;
|
|
1390
|
+
}
|
|
1391
|
+
return argument?.type === "CallExpression" && argument.callee.type === "Identifier" && composers.has(argument.callee.name) && argument.arguments[0]?.type === "Identifier";
|
|
1392
|
+
}
|
|
1393
|
+
// src/utils/helpers/react-components.ts
|
|
1394
|
+
function reactComponentsExportCandidates(program) {
|
|
1395
|
+
return exportsCollect(program).map((binding) => ({
|
|
1396
|
+
...binding,
|
|
1397
|
+
name: binding.localName ?? binding.exportedName
|
|
1398
|
+
}));
|
|
1399
|
+
}
|
|
1400
|
+
function importedPropsWithChildren(program) {
|
|
1401
|
+
return new Set(program.body.flatMap((statement) => statement.type === "ImportDeclaration" && statement.source.value === "react" ? statement.specifiers.flatMap((specifier) => specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier" && specifier.imported.name === "PropsWithChildren" ? [specifier.local.name] : []) : []));
|
|
1402
|
+
}
|
|
1403
|
+
function containsNamedTypeReference(node, names) {
|
|
1404
|
+
let found = false;
|
|
1405
|
+
astVisit(node, [], (candidate) => {
|
|
1406
|
+
if (candidate.type === "TSTypeReference" && candidate.typeName.type === "Identifier" && names.has(candidate.typeName.name)) {
|
|
1407
|
+
found = true;
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
return found;
|
|
1411
|
+
}
|
|
1412
|
+
function returnsReactChildren(program, node, expressions) {
|
|
1413
|
+
const returnedNames = new Set(expressions.flatMap((expression) => expression.type === "Identifier" ? [expression.name] : []));
|
|
1414
|
+
const propsWithChildren = importedPropsWithChildren(program);
|
|
1415
|
+
return node.params.some((param) => {
|
|
1416
|
+
if (param.type !== "ObjectPattern") {
|
|
1417
|
+
return false;
|
|
1418
|
+
}
|
|
1419
|
+
const returnsChild = param.properties.some((property) => property.type === "Property" && property.value.type === "Identifier" && returnedNames.has(property.value.name));
|
|
1420
|
+
return returnsChild && containsNamedTypeReference(param.typeAnnotation, propsWithChildren);
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
function reactComponentsFunctionReturnsOutput(program, node) {
|
|
1424
|
+
const expressions = astDirectReturnExpressions(node);
|
|
1425
|
+
return expressions.some((expression) => astContainsJsx(expression) || expression.type === "Literal" && expression.value === null) || returnsReactChildren(program, node, expressions);
|
|
1426
|
+
}
|
|
1427
|
+
function functionReturnsLocalComponent(program, node) {
|
|
1428
|
+
const componentNames = new Set;
|
|
1429
|
+
astVisit(node.body, [node], (candidate, ancestors) => {
|
|
1430
|
+
if (candidate.type === "FunctionDeclaration" && candidate !== node && candidate.id?.name && /^[A-Z]/.test(candidate.id.name) && astNearestFunction(ancestors) === node && reactComponentsFunctionReturnsOutput(program, candidate)) {
|
|
1431
|
+
componentNames.add(candidate.id.name);
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
return astDirectReturnExpressions(node).some((expression) => expression.type === "Identifier" && componentNames.has(expression.name));
|
|
1435
|
+
}
|
|
1436
|
+
function componentFactoryNames(program) {
|
|
1437
|
+
return new Set(program.body.flatMap((statement) => statement.type === "FunctionDeclaration" && statement.id?.name && functionReturnsLocalComponent(program, statement) ? [statement.id.name] : []));
|
|
1438
|
+
}
|
|
1439
|
+
function isComponentInitializer(program, initializer) {
|
|
1440
|
+
if (!initializer) {
|
|
1441
|
+
return false;
|
|
1442
|
+
}
|
|
1443
|
+
if (initializer.type === "ArrowFunctionExpression" || initializer.type === "FunctionExpression") {
|
|
1444
|
+
return reactComponentsFunctionReturnsOutput(program, initializer);
|
|
1445
|
+
}
|
|
1446
|
+
if (initializer.type !== "CallExpression") {
|
|
1447
|
+
return false;
|
|
1448
|
+
}
|
|
1449
|
+
const name = astCallName(initializer);
|
|
1450
|
+
if (name === "forwardRef" || name === "memo") {
|
|
1451
|
+
return initializer.arguments.some((argument) => argument.type !== "SpreadElement" && isComponentInitializer(program, argument));
|
|
1452
|
+
}
|
|
1453
|
+
if (name === "lazy") {
|
|
1454
|
+
return initializer.arguments.some((argument) => argument.type === "ArrowFunctionExpression" || argument.type === "FunctionExpression");
|
|
1455
|
+
}
|
|
1456
|
+
return Boolean(name?.startsWith("create") && componentFactoryNames(program).has(name));
|
|
1457
|
+
}
|
|
1458
|
+
function reactComponentsIsLike(program, candidate) {
|
|
1459
|
+
if (!/^[A-Z]/.test(candidate.name)) {
|
|
1460
|
+
return false;
|
|
1461
|
+
}
|
|
1462
|
+
const declaration = candidate.declaration ?? candidate.node;
|
|
1463
|
+
if (declaration.type === "FunctionDeclaration") {
|
|
1464
|
+
return reactComponentsFunctionReturnsOutput(program, declaration);
|
|
1465
|
+
}
|
|
1466
|
+
return declaration.type === "VariableDeclaration" && isComponentInitializer(program, candidate.initializer);
|
|
1467
|
+
}
|
|
1468
|
+
function reactComponentsPascalFromBasename(basename) {
|
|
1469
|
+
return basename.replace(/\.(tsx?|jsx?)$/, "").split(/[-.]/).filter(Boolean).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("");
|
|
1470
|
+
}
|
|
1471
|
+
// src/utils/helpers/routes.ts
|
|
1472
|
+
function routesInspectFunction(context, item, bannedHooks, banIntrinsicJsx) {
|
|
1473
|
+
astVisit(item.node.body, [item.node], (node) => {
|
|
1474
|
+
if (banIntrinsicJsx && node.type === "JSXOpeningElement" && node.name.type === "JSXIdentifier" && /^[a-z]/.test(node.name.name)) {
|
|
1475
|
+
context.report({
|
|
1476
|
+
node,
|
|
1477
|
+
messageId: "hostJsx",
|
|
1478
|
+
data: { name: item.name }
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
if (node.type === "CallExpression") {
|
|
1482
|
+
const name = astCallName(node);
|
|
1483
|
+
if (name && bannedHooks.has(name)) {
|
|
1484
|
+
context.report({
|
|
1485
|
+
node,
|
|
1486
|
+
messageId: "bannedHook",
|
|
1487
|
+
data: { hook: name, name: item.name }
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
// src/utils/helpers/runtime.ts
|
|
1494
|
+
import { builtinModules } from "node:module";
|
|
1495
|
+
var runtimeServerGlobals = ["process", "Bun", "Deno", "Buffer", "__dirname", "__filename", "require"];
|
|
1496
|
+
var runtimeBrowserGlobals = [
|
|
1497
|
+
"window",
|
|
1498
|
+
"document",
|
|
1499
|
+
"navigator",
|
|
1500
|
+
"localStorage",
|
|
1501
|
+
"sessionStorage",
|
|
1502
|
+
"location",
|
|
1503
|
+
"history",
|
|
1504
|
+
"screen",
|
|
1505
|
+
"HTMLElement",
|
|
1506
|
+
"Element",
|
|
1507
|
+
"HTMLInputElement",
|
|
1508
|
+
"ResizeObserver",
|
|
1509
|
+
"MutationObserver",
|
|
1510
|
+
"IntersectionObserver",
|
|
1511
|
+
"FileReader",
|
|
1512
|
+
"matchMedia",
|
|
1513
|
+
"getComputedStyle",
|
|
1514
|
+
"requestAnimationFrame",
|
|
1515
|
+
"cancelAnimationFrame",
|
|
1516
|
+
"alert",
|
|
1517
|
+
"confirm",
|
|
1518
|
+
"prompt"
|
|
1519
|
+
];
|
|
1520
|
+
function runtimeMatchesImport(values, source) {
|
|
1521
|
+
return Boolean(values?.some((value) => source === value || source.startsWith(value + "/")));
|
|
1522
|
+
}
|
|
1523
|
+
function runtimeIsBackendImport(options, target, source) {
|
|
1524
|
+
return options.backend.some((path) => target.startsWith(path + "/")) || options.backendPackages.some((name) => source === name || source.startsWith(name + "/") && /\/(?:server|rpc)(?:\/|$)/.test(source.slice(name.length)));
|
|
1525
|
+
}
|
|
1526
|
+
function runtimeIsServerPlatformImport(source) {
|
|
1527
|
+
return builtinModules.includes(source) || source === "@orpc/server" || /^(?:node:|bun(?::|$)|cloudflare:)/.test(source) || /^react-dom\/server(?:\.|\/|$)/.test(source);
|
|
1528
|
+
}
|
|
1529
|
+
function runtimeMemberName(node) {
|
|
1530
|
+
if (node?.type !== "MemberExpression")
|
|
1531
|
+
return null;
|
|
1532
|
+
if (!node.computed && node.property.type === "Identifier")
|
|
1533
|
+
return node.property.name;
|
|
1534
|
+
return node.computed && node.property.type === "Literal" && typeof node.property.value === "string" ? node.property.value : null;
|
|
1535
|
+
}
|
|
1536
|
+
// src/utils/helpers/sanitizers.ts
|
|
1537
|
+
function sanitizersCallsConfigured(node, names) {
|
|
1538
|
+
let found = false;
|
|
1539
|
+
astVisit(node, [], (candidate) => {
|
|
1540
|
+
if (candidate.type === "CallExpression" && candidate.callee.type === "Identifier" && names.has(candidate.callee.name)) {
|
|
1541
|
+
found = true;
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
return found;
|
|
1545
|
+
}
|
|
1546
|
+
function sanitizersConfiguredBindings(program, names) {
|
|
1547
|
+
const result = new Set;
|
|
1548
|
+
for (const statement of program.body) {
|
|
1549
|
+
const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
1550
|
+
if (declaration?.type !== "VariableDeclaration") {
|
|
1551
|
+
continue;
|
|
1552
|
+
}
|
|
1553
|
+
for (const item of declaration.declarations) {
|
|
1554
|
+
if (item.id.type === "Identifier" && sanitizersCallsConfigured(item.init, names)) {
|
|
1555
|
+
result.add(item.id.name);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
return result;
|
|
1560
|
+
}
|
|
1561
|
+
// src/utils/helpers/schemas.ts
|
|
1562
|
+
function schemasIsRuntimeSpecifier(specifier) {
|
|
1563
|
+
return specifier.type !== "ImportSpecifier" || specifier.importKind !== "type";
|
|
1564
|
+
}
|
|
1565
|
+
function schemasMemberRootName(node) {
|
|
1566
|
+
if (node.object.type === "Identifier") {
|
|
1567
|
+
return node.object.name;
|
|
1568
|
+
}
|
|
1569
|
+
return node.object.type === "MemberExpression" ? schemasMemberRootName(node.object) : undefined;
|
|
1570
|
+
}
|
|
1571
|
+
function schemasIsPackage(source, packages) {
|
|
1572
|
+
return packages.has(source) || [...packages].some((pkg) => source.startsWith(`${pkg}/`));
|
|
1573
|
+
}
|
|
1574
|
+
function schemasIsCall(node, namespaces) {
|
|
1575
|
+
return node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && namespaces.has(node.callee.object.name);
|
|
1576
|
+
}
|
|
1577
|
+
function schemasRuntimeImportLocals(node) {
|
|
1578
|
+
return node.importKind === "type" ? [] : node.specifiers.flatMap((specifier) => schemasIsRuntimeSpecifier(specifier) ? [specifier.local.name] : []);
|
|
1579
|
+
}
|
|
1580
|
+
function schemasIsRuntimeImport(node, packages) {
|
|
1581
|
+
return schemasIsPackage(node.source.value, packages) && (node.specifiers.length === 0 ? node.importKind !== "type" : schemasRuntimeImportLocals(node).length > 0);
|
|
1582
|
+
}
|
|
1583
|
+
function schemasIsRuntimeReexport(node, packages) {
|
|
1584
|
+
if (!node.source || !schemasIsPackage(node.source.value, packages) || node.exportKind === "type") {
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
return node.type === "ExportAllDeclaration" || node.specifiers.length === 0 || node.specifiers.some((specifier) => specifier.exportKind !== "type");
|
|
1588
|
+
}
|
|
1589
|
+
function schemasIsConstruction(node, constructors) {
|
|
1590
|
+
if (node.callee.type === "Identifier") {
|
|
1591
|
+
return constructors.has(node.callee.name);
|
|
1592
|
+
}
|
|
1593
|
+
if (node.callee.type !== "MemberExpression") {
|
|
1594
|
+
return false;
|
|
1595
|
+
}
|
|
1596
|
+
const root = schemasMemberRootName(node.callee);
|
|
1597
|
+
return Boolean(root && constructors.has(root));
|
|
1598
|
+
}
|
|
1599
|
+
function schemasElements(node) {
|
|
1600
|
+
return node.arguments.flatMap((argument) => argument.type === "ArrayExpression" ? argument.elements.filter((element) => element !== null) : [argument]);
|
|
1601
|
+
}
|
|
1602
|
+
function schemasIsAllowedElement(node, allowScalars, namespaces, structuralMethods) {
|
|
1603
|
+
if (node.type === "Identifier") {
|
|
1604
|
+
return true;
|
|
1605
|
+
}
|
|
1606
|
+
if (!allowScalars || node.type !== "CallExpression" || !schemasIsCall(node, namespaces)) {
|
|
1607
|
+
return false;
|
|
1608
|
+
}
|
|
1609
|
+
const name = node.callee.type === "MemberExpression" ? astStaticMemberName(node.callee) : undefined;
|
|
1610
|
+
return Boolean(name && !structuralMethods.has(name));
|
|
1611
|
+
}
|
|
1612
|
+
function schemasTypeOperator(node, namespaces, operators) {
|
|
1613
|
+
return node.typeName.type === "TSQualifiedName" && node.typeName.left.type === "Identifier" && namespaces.has(node.typeName.left.name) && operators.has(node.typeName.right.name) ? node.typeName.right.name : undefined;
|
|
1614
|
+
}
|
|
1615
|
+
// src/utils/helpers/boundaries.ts
|
|
1616
|
+
import { resolve, dirname } from "node:path";
|
|
1617
|
+
function boundaryResolveTarget(options, file, source) {
|
|
1618
|
+
const owner = [...options.web, ...options.backend].sort((a, b) => b.length - a.length).find((root) => file.startsWith(root + "/"));
|
|
1619
|
+
const web = owner && options.web.includes(owner) ? owner : undefined;
|
|
1620
|
+
const alias = Object.entries(options.aliases).sort(([a], [b]) => b.length - a.length).find(([prefix]) => source === prefix || source.startsWith(`${prefix}/`));
|
|
1621
|
+
return alias ? resolve(alias[1], source.slice(alias[0].length).replace(/^\//, "")).replaceAll("\\", "/") : source.startsWith(".") ? resolve(dirname(file), source).replaceAll("\\", "/") : source.startsWith("@/") && web ? `${web}/${source.slice(2)}` : source.startsWith("#/") && owner ? `${owner}/${source.slice(2)}` : source.startsWith("#services/") && owner ? `${owner}/services/${source.slice(10)}` : source;
|
|
1622
|
+
}
|
|
1623
|
+
// src/rules/dillon-anti-slop/rules/no-module-mocking.ts
|
|
597
1624
|
var moduleMockMethods = new Set(["doMock", "mock", "unstable_mockModule"]);
|
|
1625
|
+
var defaultDeny = [
|
|
1626
|
+
{ api: "*.mock" },
|
|
1627
|
+
{ api: "*.doMock" },
|
|
1628
|
+
{ api: "*.unstable_mockModule" }
|
|
1629
|
+
];
|
|
598
1630
|
function resolveVariable2(sourceCode, identifier) {
|
|
599
1631
|
let scope = sourceCode.getScope(identifier);
|
|
600
1632
|
while (scope !== null) {
|
|
@@ -610,33 +1642,75 @@ function importedName(node) {
|
|
|
610
1642
|
return null;
|
|
611
1643
|
return node.imported.type === "Identifier" ? node.imported.name : node.imported.value;
|
|
612
1644
|
}
|
|
613
|
-
function
|
|
1645
|
+
function testFramework(sourceCode, expression) {
|
|
614
1646
|
if (expression.type !== "Identifier")
|
|
615
|
-
return
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
1647
|
+
return null;
|
|
1648
|
+
const name = expression.name === "vi" || expression.name === "jest" ? expression.name : null;
|
|
1649
|
+
if (name !== null && sourceCode.isGlobalReference(expression))
|
|
1650
|
+
return name;
|
|
619
1651
|
const variable = resolveVariable2(sourceCode, expression);
|
|
620
|
-
if (variable === null || variable.defs.length === 0)
|
|
621
|
-
return
|
|
622
|
-
|
|
623
|
-
return variable.defs.some((definition) => {
|
|
1652
|
+
if (variable === null || variable.defs.length === 0)
|
|
1653
|
+
return name;
|
|
1654
|
+
for (const definition of variable.defs) {
|
|
624
1655
|
if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration") {
|
|
625
|
-
|
|
1656
|
+
continue;
|
|
626
1657
|
}
|
|
627
1658
|
const source = definition.parent.source.value;
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
1659
|
+
const imported = importedName(definition.node);
|
|
1660
|
+
if (source === "vitest" && imported === "vi")
|
|
1661
|
+
return "vi";
|
|
1662
|
+
if (source === "@jest/globals" && imported === "jest")
|
|
1663
|
+
return "jest";
|
|
1664
|
+
}
|
|
1665
|
+
return null;
|
|
631
1666
|
}
|
|
632
|
-
function
|
|
633
|
-
if (
|
|
634
|
-
return
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
1667
|
+
function propertyName(key, computed) {
|
|
1668
|
+
if (key.type === "Literal" && typeof key.value === "string")
|
|
1669
|
+
return key.value;
|
|
1670
|
+
return !computed && key.type === "Identifier" ? key.name : null;
|
|
1671
|
+
}
|
|
1672
|
+
function calledApi(sourceCode, callee) {
|
|
1673
|
+
if (callee.type !== "MemberExpression")
|
|
1674
|
+
return null;
|
|
1675
|
+
const framework = testFramework(sourceCode, callee.object);
|
|
1676
|
+
if (framework === null)
|
|
1677
|
+
return null;
|
|
1678
|
+
const method = propertyName(callee.property, callee.computed);
|
|
1679
|
+
return method === null ? null : `${framework}.${method}`;
|
|
1680
|
+
}
|
|
1681
|
+
function destructuredApi(sourceCode, callee) {
|
|
1682
|
+
if (callee.type !== "Identifier")
|
|
1683
|
+
return null;
|
|
1684
|
+
const definition = resolveVariable2(sourceCode, callee)?.defs[0];
|
|
1685
|
+
if (definition?.parent?.type !== "VariableDeclaration" || definition.parent.kind !== "const")
|
|
1686
|
+
return null;
|
|
1687
|
+
const declarator = definition.node;
|
|
1688
|
+
if (declarator?.type !== "VariableDeclarator" || declarator.id.type !== "ObjectPattern" || declarator.init === null)
|
|
1689
|
+
return null;
|
|
1690
|
+
const framework = testFramework(sourceCode, declarator.init);
|
|
1691
|
+
if (framework === null)
|
|
1692
|
+
return null;
|
|
1693
|
+
for (const property of declarator.id.properties) {
|
|
1694
|
+
if (property.type !== "Property")
|
|
1695
|
+
continue;
|
|
1696
|
+
const binding = property.value.type === "AssignmentPattern" ? property.value.left : property.value;
|
|
1697
|
+
if (binding.type !== "Identifier" || binding.name !== callee.name)
|
|
1698
|
+
continue;
|
|
1699
|
+
const method = propertyName(property.key, property.computed);
|
|
1700
|
+
return method === null ? null : `${framework}.${method}`;
|
|
1701
|
+
}
|
|
1702
|
+
return null;
|
|
1703
|
+
}
|
|
1704
|
+
function apiParts(api) {
|
|
1705
|
+
const dot = api.indexOf(".");
|
|
1706
|
+
return [api.slice(0, dot), api.slice(dot + 1)];
|
|
1707
|
+
}
|
|
1708
|
+
function apiMatches(pattern, api) {
|
|
1709
|
+
if (pattern === "*")
|
|
1710
|
+
return true;
|
|
1711
|
+
const [patternFramework, patternMethod] = apiParts(pattern);
|
|
1712
|
+
const [framework, method] = apiParts(api);
|
|
1713
|
+
return (patternFramework === "*" || patternFramework === framework) && (patternMethod === "*" || patternMethod === method);
|
|
640
1714
|
}
|
|
641
1715
|
var noModuleMockingRule = defineRule4({
|
|
642
1716
|
meta: {
|
|
@@ -644,18 +1718,75 @@ var noModuleMockingRule = defineRule4({
|
|
|
644
1718
|
docs: {
|
|
645
1719
|
description: "Disallow Vitest and Jest module mocking; tests must replace dependencies through real interfaces."
|
|
646
1720
|
},
|
|
1721
|
+
schema: [
|
|
1722
|
+
{
|
|
1723
|
+
type: "object",
|
|
1724
|
+
additionalProperties: false,
|
|
1725
|
+
properties: {
|
|
1726
|
+
deny: {
|
|
1727
|
+
type: "array",
|
|
1728
|
+
items: {
|
|
1729
|
+
anyOf: [
|
|
1730
|
+
{ type: "string" },
|
|
1731
|
+
{
|
|
1732
|
+
type: "object",
|
|
1733
|
+
additionalProperties: false,
|
|
1734
|
+
properties: {
|
|
1735
|
+
api: { type: "string" },
|
|
1736
|
+
message: { type: "string" }
|
|
1737
|
+
},
|
|
1738
|
+
required: ["api"]
|
|
1739
|
+
}
|
|
1740
|
+
]
|
|
1741
|
+
}
|
|
1742
|
+
},
|
|
1743
|
+
allow: { type: "array", items: { type: "string" } },
|
|
1744
|
+
checkDestructured: { type: "boolean" }
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
],
|
|
647
1748
|
messages: {
|
|
648
|
-
moduleMock: "Replace module mocking with dependency injection through a real interface, service layer, or faithful test implementation."
|
|
1749
|
+
moduleMock: "Replace module mocking with dependency injection through a real interface, service layer, or faithful test implementation.",
|
|
1750
|
+
testDouble: "Replace {{api}} with dependency injection through a real interface, service layer, or faithful test implementation.",
|
|
1751
|
+
custom: "{{message}}"
|
|
649
1752
|
}
|
|
650
1753
|
},
|
|
651
1754
|
createOnce(context) {
|
|
1755
|
+
let deny = [];
|
|
1756
|
+
let allow = [];
|
|
1757
|
+
let checkDestructured = false;
|
|
652
1758
|
return {
|
|
1759
|
+
before() {
|
|
1760
|
+
const options2 = optionsFirst(context, {});
|
|
1761
|
+
deny = (options2.deny ?? defaultDeny).map((entry) => typeof entry === "string" ? { api: entry } : entry);
|
|
1762
|
+
allow = options2.allow ?? [];
|
|
1763
|
+
checkDestructured = options2.checkDestructured ?? false;
|
|
1764
|
+
mockingApiPatternsAssert([...deny.map((entry) => entry.api), ...allow]);
|
|
1765
|
+
},
|
|
653
1766
|
CallExpression(node) {
|
|
654
1767
|
if (node.callee.type === "Super" || node.callee.type === "V8IntrinsicExpression")
|
|
655
1768
|
return;
|
|
656
|
-
|
|
657
|
-
|
|
1769
|
+
const api = calledApi(context.sourceCode, node.callee) ?? (checkDestructured ? destructuredApi(context.sourceCode, node.callee) : null);
|
|
1770
|
+
if (api === null)
|
|
1771
|
+
return;
|
|
1772
|
+
if (allow.some((pattern) => apiMatches(pattern, api)))
|
|
1773
|
+
return;
|
|
1774
|
+
const denied = deny.find((entry) => apiMatches(entry.api, api));
|
|
1775
|
+
if (denied === undefined)
|
|
1776
|
+
return;
|
|
1777
|
+
if (denied.message !== undefined) {
|
|
1778
|
+
context.report({
|
|
1779
|
+
node,
|
|
1780
|
+
messageId: "custom",
|
|
1781
|
+
data: { message: denied.message }
|
|
1782
|
+
});
|
|
1783
|
+
return;
|
|
658
1784
|
}
|
|
1785
|
+
context.report({
|
|
1786
|
+
node,
|
|
1787
|
+
messageId: moduleMockMethods.has(apiParts(api)[1]) ? "moduleMock" : "testDouble",
|
|
1788
|
+
data: { api }
|
|
1789
|
+
});
|
|
659
1790
|
}
|
|
660
1791
|
};
|
|
661
1792
|
}
|