oxlint-plugin-arch 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +241 -102
  3. package/dist/index.js +2212 -269
  4. package/package.json +4 -6
package/dist/index.js CHANGED
@@ -1,10 +1,20 @@
1
1
  // src/index.ts
2
2
  import { eslintCompatPlugin } from "@oxlint/plugins";
3
3
 
4
- // src/rules/declaration-name.ts
4
+ // src/rules/call-array-multiline.ts
5
5
  import { defineRule } from "@oxlint/plugins";
6
6
 
7
7
  // src/utils/helpers/ast.ts
8
+ var astVisitSkip = new Set([
9
+ "parent",
10
+ "type",
11
+ "start",
12
+ "end",
13
+ "range",
14
+ "loc",
15
+ "tokens",
16
+ "comments"
17
+ ]);
8
18
  function astIsNode(value) {
9
19
  return value !== null && typeof value === "object" && typeof Reflect.get(value, "type") === "string";
10
20
  }
@@ -19,11 +29,13 @@ function astVisit(node, ancestors, callback) {
19
29
  return;
20
30
  }
21
31
  callback(node, ancestors);
22
- for (const [key, value] of Object.entries(node)) {
23
- if (!["parent", "type", "start", "end", "range", "loc"].includes(key)) {
24
- astVisit(value, [...ancestors, node], callback);
32
+ ancestors.push(node);
33
+ for (const key in node) {
34
+ if (Object.hasOwn(node, key) && !astVisitSkip.has(key)) {
35
+ astVisit(Reflect.get(node, key), ancestors, callback);
25
36
  }
26
37
  }
38
+ ancestors.pop();
27
39
  }
28
40
  function astContainsJsx(node) {
29
41
  let found = false;
@@ -52,6 +64,17 @@ function astDirectReturnExpressions(node) {
52
64
  function astStaticMemberName(node) {
53
65
  return !node.computed && node.property.type === "Identifier" ? node.property.name : undefined;
54
66
  }
67
+ function astDottedName(node) {
68
+ if (node.type === "Identifier") {
69
+ return node.name;
70
+ }
71
+ if (node.type !== "MemberExpression") {
72
+ return;
73
+ }
74
+ const object = astDottedName(node.object);
75
+ const property = astStaticMemberName(node);
76
+ return object && property ? `${object}.${property}` : undefined;
77
+ }
55
78
  function astCallName(node) {
56
79
  if (node.callee.type === "Identifier") {
57
80
  return node.callee.name;
@@ -233,6 +256,23 @@ function declarationsCollectNamed(root, kinds) {
233
256
  });
234
257
  return result;
235
258
  }
259
+ function declarationsInsideForLoop(node) {
260
+ for (let current = node.parent;current; current = current.parent) {
261
+ if (current.type === "ForStatement" || current.type === "ForInStatement" || current.type === "ForOfStatement") {
262
+ return true;
263
+ }
264
+ }
265
+ return false;
266
+ }
267
+ function declarationsNamedFromNode(node) {
268
+ if (node.type === "VariableDeclarator") {
269
+ const kind = declarationsNamedKind(node, [node.parent]);
270
+ return kind && node.id.type === "Identifier" && !declarationsInsideForLoop(node) ? { name: node.id.name, node, kind } : undefined;
271
+ }
272
+ const kind = declarationsNamedKind(node, []);
273
+ const name = "id" in node && node.id && "name" in node.id ? node.id.name : undefined;
274
+ return kind && name ? { name, node, kind } : undefined;
275
+ }
236
276
  function declarationsCollectFunctions(root) {
237
277
  return declarationsCollectNamed(root).flatMap((item) => {
238
278
  if (item.node.type === "FunctionDeclaration") {
@@ -713,8 +753,301 @@ function schemasIsAllowedElement(node, allowScalars, namespaces, structuralMetho
713
753
  function schemasTypeOperator(node, namespaces, operators) {
714
754
  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;
715
755
  }
756
+ // src/utils/helpers/comments.ts
757
+ 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])/;
758
+ var COMMENTS_DISABLE_NEXT_LINE = /^(?:eslint|oxlint)-disable-next-line\b/;
759
+ var COMMENTS_LEGAL_BLOCK = "\x00legal-block\x00";
760
+ function commentsNormalizedValue(comment) {
761
+ const value = (comment.type === "Block" ? comment.value.replace(/^\s*\* ?/gm, "") : comment.value).trimStart();
762
+ return comment.type === "Block" && comment.value.startsWith("!") ? COMMENTS_LEGAL_BLOCK + value : value;
763
+ }
764
+ function commentsIsDirective(value) {
765
+ return value.startsWith(COMMENTS_LEGAL_BLOCK) || COMMENTS_DIRECTIVE.test(value);
766
+ }
767
+ function commentsIsDisableNextLine(value) {
768
+ return COMMENTS_DISABLE_NEXT_LINE.test(value);
769
+ }
770
+ function commentsIsWhy(value) {
771
+ return value.startsWith("Why: ");
772
+ }
773
+ function commentsIsJsdoc(source, comment) {
774
+ return comment.type === "Block" && source.startsWith("/**", comment.range[0]);
775
+ }
776
+ // src/utils/helpers/layout.ts
777
+ var layoutIndentSchema = {
778
+ anyOf: [
779
+ { type: "integer", minimum: 0 },
780
+ { type: "string", enum: ["tab"] }
781
+ ]
782
+ };
783
+ function layoutLineIndent(text, offset) {
784
+ const lineStart = text.lastIndexOf(`
785
+ `, offset - 1) + 1;
786
+ return /^[\t ]*/.exec(text.slice(lineStart, offset))?.[0] ?? "";
787
+ }
788
+ function layoutIndentUnit(indent = 2) {
789
+ return indent === "tab" ? "\t" : " ".repeat(indent);
790
+ }
791
+ function layoutNewline(text) {
792
+ return text.includes(`\r
793
+ `) ? `\r
794
+ ` : `
795
+ `;
796
+ }
797
+ // src/rules/call-array-multiline.ts
798
+ function callArrayMultilineCallee(node) {
799
+ if (node.type === "Identifier")
800
+ return { name: node.name, root: node };
801
+ if (node.type !== "MemberExpression" || node.computed || node.property.type !== "Identifier")
802
+ return null;
803
+ const object = callArrayMultilineCallee(node.object);
804
+ return object ? { name: `${object.name}.${node.property.name}`, root: object.root } : null;
805
+ }
806
+ function callArrayMultilineIsShadowed(context, root) {
807
+ if (root.type !== "Identifier")
808
+ return false;
809
+ let scope = context.sourceCode.getScope(root);
810
+ while (scope) {
811
+ const variable = scope.set.get(root.name);
812
+ if (variable)
813
+ return variable.defs.length > 0;
814
+ scope = scope.upper;
815
+ }
816
+ return false;
817
+ }
818
+ function callArrayMultilineEdits(array, text, elementIndent, baseIndent, newline) {
819
+ const elements = array.elements;
820
+ if (elements.some((element) => element === null))
821
+ return null;
822
+ const present = elements.filter((element) => element !== null);
823
+ const first = present[0];
824
+ const last = present.at(-1);
825
+ if (!first || !last)
826
+ return null;
827
+ const edits = [];
828
+ const openingRange = [array.range[0] + 1, first.range[0]];
829
+ if (!/^\s*$/.test(text.slice(...openingRange)))
830
+ return null;
831
+ edits.push({ range: openingRange, text: `${newline}${elementIndent}` });
832
+ for (let index = 1;index < present.length; index++) {
833
+ const previous = present[index - 1];
834
+ const current = present[index];
835
+ const gap = text.slice(previous.range[1], current.range[0]);
836
+ if (!/^,\s*$/.test(gap))
837
+ return null;
838
+ edits.push({
839
+ range: [previous.range[1] + 1, current.range[0]],
840
+ text: `${newline}${elementIndent}`
841
+ });
842
+ }
843
+ const closingRange = [last.range[1], array.range[1] - 1];
844
+ const closingGap = text.slice(...closingRange);
845
+ if (!/^,?\s*$/.test(closingGap))
846
+ return null;
847
+ edits.push({
848
+ range: [closingRange[0] + (closingGap.startsWith(",") ? 1 : 0), closingRange[1]],
849
+ text: `${newline}${baseIndent}`
850
+ });
851
+ return edits;
852
+ }
853
+ var callArrayMultiline = defineRule({
854
+ meta: {
855
+ type: "layout",
856
+ fixable: "whitespace",
857
+ schema: [{
858
+ type: "object",
859
+ additionalProperties: false,
860
+ properties: {
861
+ callees: { type: "array", items: { type: "string" } },
862
+ minElements: { type: "integer", minimum: 1 },
863
+ indent: layoutIndentSchema
864
+ }
865
+ }],
866
+ messages: {
867
+ multiline: "Put every array element in this call on its own line."
868
+ }
869
+ },
870
+ createOnce(context) {
871
+ let callees = new Set(["Promise.all", "Promise.allSettled"]);
872
+ let minElements = 2;
873
+ let indentUnit = layoutIndentUnit();
874
+ let newline = `
875
+ `;
876
+ return {
877
+ before() {
878
+ const options = optionsFirst(context, {});
879
+ callees = new Set(options.callees ?? ["Promise.all", "Promise.allSettled"]);
880
+ minElements = options.minElements ?? 2;
881
+ indentUnit = layoutIndentUnit(options.indent);
882
+ newline = layoutNewline(context.sourceCode.text);
883
+ if (callees.size === 0)
884
+ return false;
885
+ },
886
+ CallExpression(node) {
887
+ const callee = callArrayMultilineCallee(node.callee);
888
+ if (!callee || !callees.has(callee.name))
889
+ return;
890
+ const array = node.arguments[0];
891
+ if (array?.type !== "ArrayExpression" || array.elements.length < minElements)
892
+ return;
893
+ if (array.loc.start.line !== array.loc.end.line)
894
+ return;
895
+ if (callee.root.type === "Identifier" && callee.root.name === "Promise" && callArrayMultilineIsShadowed(context, callee.root))
896
+ return;
897
+ const source = context.sourceCode;
898
+ const text = source.text;
899
+ const baseIndent = layoutLineIndent(text, array.range[0]);
900
+ const hasComments = source.getCommentsInside(array).length > 0;
901
+ const edits = hasComments ? null : callArrayMultilineEdits(array, text, baseIndent + indentUnit, baseIndent, newline);
902
+ context.report({
903
+ node: array,
904
+ messageId: "multiline",
905
+ fix: edits ? (fixer) => edits.map((edit) => fixer.replaceTextRange(edit.range, edit.text)) : undefined
906
+ });
907
+ }
908
+ };
909
+ }
910
+ });
911
+
912
+ // src/rules/chain-newline.ts
913
+ import { defineRule as defineRule2 } from "@oxlint/plugins";
914
+ function chainNewlineCollect(node) {
915
+ const links = [];
916
+ let current = node;
917
+ while (current.type === "CallExpression" && current.callee.type === "MemberExpression") {
918
+ const member = current.callee;
919
+ links.push({
920
+ call: current,
921
+ member,
922
+ methodName: !member.computed && member.property.type === "Identifier" ? member.property.name : null
923
+ });
924
+ current = member.object;
925
+ }
926
+ links.reverse();
927
+ return { root: current, links };
928
+ }
929
+ function chainNewlineRootIdentifier(node) {
930
+ let current = node;
931
+ while (current.type === "ChainExpression" || current.type === "ParenthesizedExpression") {
932
+ current = current.expression;
933
+ }
934
+ return current.type === "Identifier" ? current.name : null;
935
+ }
936
+ function chainNewlineMatches(root, links, group) {
937
+ if (links.length < group.minDepth)
938
+ return false;
939
+ const hasCriteria = group.methods !== undefined || group.onlyMethods !== undefined || group.rootPattern !== undefined;
940
+ if (!hasCriteria)
941
+ return true;
942
+ if (group.methods && links.some((link) => link.methodName !== null && group.methods?.has(link.methodName))) {
943
+ return true;
944
+ }
945
+ if (group.onlyMethods && links.every((link) => link.methodName !== null && group.onlyMethods?.has(link.methodName)))
946
+ return true;
947
+ const rootName = chainNewlineRootIdentifier(root);
948
+ return rootName !== null && group.rootPattern !== undefined && group.rootPattern.test(rootName);
949
+ }
950
+ var chainNewline = defineRule2({
951
+ meta: {
952
+ type: "layout",
953
+ fixable: "whitespace",
954
+ schema: [{
955
+ type: "object",
956
+ additionalProperties: false,
957
+ properties: {
958
+ groups: {
959
+ type: "array",
960
+ items: {
961
+ type: "object",
962
+ additionalProperties: false,
963
+ properties: {
964
+ minDepth: { type: "integer", minimum: 1 },
965
+ methods: { type: "array", items: { type: "string" } },
966
+ onlyMethods: { type: "array", items: { type: "string" } },
967
+ rootPattern: { type: "string" }
968
+ },
969
+ required: ["minDepth"]
970
+ }
971
+ },
972
+ indent: layoutIndentSchema
973
+ },
974
+ required: ["groups"]
975
+ }],
976
+ messages: {
977
+ newline: "Put every method in this call chain on its own line."
978
+ }
979
+ },
980
+ createOnce(context) {
981
+ let groups = [];
982
+ let indentUnit = layoutIndentUnit();
983
+ let newline = `
984
+ `;
985
+ let processed = new WeakSet;
986
+ return {
987
+ before() {
988
+ const options = optionsFirst(context, { groups: [] });
989
+ groups = (options.groups ?? []).map((group) => ({
990
+ minDepth: group.minDepth,
991
+ methods: group.methods ? new Set(group.methods) : undefined,
992
+ onlyMethods: group.onlyMethods ? new Set(group.onlyMethods) : undefined,
993
+ rootPattern: group.rootPattern === undefined ? undefined : new RegExp(group.rootPattern)
994
+ }));
995
+ indentUnit = layoutIndentUnit(options.indent);
996
+ newline = layoutNewline(context.sourceCode.text);
997
+ processed = new WeakSet;
998
+ if (groups.length === 0)
999
+ return false;
1000
+ },
1001
+ CallExpression(node) {
1002
+ if (processed.has(node))
1003
+ return;
1004
+ const parent = node.parent;
1005
+ if (parent?.type === "MemberExpression" && parent.object === node && parent.parent?.type === "CallExpression" && parent.parent.callee === parent)
1006
+ return;
1007
+ const { root, links } = chainNewlineCollect(node);
1008
+ for (const link of links)
1009
+ processed.add(link.call);
1010
+ if (!groups.some((group) => chainNewlineMatches(root, links, group)))
1011
+ return;
1012
+ const source = context.sourceCode;
1013
+ const text = source.text;
1014
+ const chainIndent = layoutLineIndent(text, root.range[0]) + indentUnit;
1015
+ const replacements = [];
1016
+ let fixable = true;
1017
+ let needsFix = false;
1018
+ for (const link of links) {
1019
+ if (link.member.computed) {
1020
+ fixable = false;
1021
+ needsFix = true;
1022
+ continue;
1023
+ }
1024
+ const range = [link.member.object.range[1], link.member.property.range[0]];
1025
+ const gap = text.slice(...range);
1026
+ const operator = link.member.optional ? "?." : ".";
1027
+ const expected = `${newline}${chainIndent}${operator}`;
1028
+ if (source.commentsExistBetween(link.member.object, link.member.property) || gap.trim() !== operator) {
1029
+ fixable = false;
1030
+ }
1031
+ if (gap !== expected) {
1032
+ needsFix = true;
1033
+ replacements.push({ range, text: expected });
1034
+ }
1035
+ }
1036
+ if (!needsFix)
1037
+ return;
1038
+ context.report({
1039
+ node,
1040
+ messageId: "newline",
1041
+ fix: fixable ? (fixer) => replacements.map((replacement) => fixer.replaceTextRange(replacement.range, replacement.text)) : undefined
1042
+ });
1043
+ }
1044
+ };
1045
+ }
1046
+ });
1047
+
716
1048
  // src/rules/declaration-name.ts
717
- var declarationName = defineRule({
1049
+ import { defineRule as defineRule3 } from "@oxlint/plugins";
1050
+ var declarationName = defineRule3({
718
1051
  meta: {
719
1052
  type: "problem",
720
1053
  schema: [{
@@ -741,40 +1074,53 @@ var declarationName = defineRule({
741
1074
  }
742
1075
  },
743
1076
  createOnce(context) {
1077
+ let options = {};
1078
+ let kinds = null;
1079
+ let allowed = null;
1080
+ let expected = null;
1081
+ let prefixes = [];
1082
+ function check(node) {
1083
+ const item = declarationsNamedFromNode(node);
1084
+ if (!item || kinds && !kinds.has(item.kind) || allowed?.test(item.name)) {
1085
+ return;
1086
+ }
1087
+ const comparableName = options.normalize === "none" ? item.name : item.name.replaceAll("_", "").toLowerCase();
1088
+ const matches = expected ? expected.test(item.name) : prefixes.some((prefix) => comparableName.startsWith(prefix));
1089
+ if (matches) {
1090
+ return;
1091
+ }
1092
+ context.report({
1093
+ node: item.node,
1094
+ messageId: expected ? "pattern" : "prefix",
1095
+ data: expected ? { name: item.name, pattern: options.pattern ?? "" } : { name: item.name, prefix: prefixes.join(" or ") }
1096
+ });
1097
+ }
744
1098
  return {
745
- Program(program) {
746
- const options = optionsFirst(context, {
1099
+ before() {
1100
+ options = optionsFirst(context, {
747
1101
  stem: "before-first-dot",
748
1102
  normalize: "remove-separators"
749
1103
  });
750
- const allowed = options.allowPattern ? new RegExp(options.allowPattern) : null;
751
- const expected = options.pattern ? new RegExp(options.pattern, options.flags) : null;
1104
+ kinds = options.kinds && options.kinds.length > 0 ? new Set(options.kinds) : null;
1105
+ allowed = options.allowPattern ? new RegExp(options.allowPattern) : null;
1106
+ expected = options.pattern ? new RegExp(options.pattern, options.flags) : null;
752
1107
  const basename = namingFileBasename(context.filename).replace(/\.(tsx?|jsx?)$/, "");
753
1108
  const stem = namingFileStem(basename, options.stem, options.trailingRoles, options.roleSeparators);
754
- const prefixes = namingFilePrefixes(stem, options.normalize ?? "remove-separators", options.singularize ?? "none");
755
- const seen = new Set;
756
- for (const item of declarationsCollectNamed(program, options.kinds)) {
757
- const key = `${item.name}:${item.node.start}:${item.node.end}`;
758
- const comparableName = options.normalize === "none" ? item.name : item.name.replaceAll("_", "").toLowerCase();
759
- const matches = expected ? expected.test(item.name) : prefixes.some((prefix) => comparableName.startsWith(prefix));
760
- if (seen.has(key) || allowed?.test(item.name) || matches) {
761
- continue;
762
- }
763
- seen.add(key);
764
- context.report({
765
- node: item.node,
766
- messageId: expected ? "pattern" : "prefix",
767
- data: expected ? { name: item.name, pattern: options.pattern ?? "" } : { name: item.name, prefix: prefixes.join(" or ") }
768
- });
769
- }
770
- }
1109
+ prefixes = namingFilePrefixes(stem, options.normalize ?? "remove-separators", options.singularize ?? "none");
1110
+ },
1111
+ FunctionDeclaration: check,
1112
+ ClassDeclaration: check,
1113
+ TSTypeAliasDeclaration: check,
1114
+ TSInterfaceDeclaration: check,
1115
+ TSEnumDeclaration: check,
1116
+ VariableDeclarator: check
771
1117
  };
772
1118
  }
773
1119
  });
774
1120
 
775
1121
  // src/rules/export-file-prefix.ts
776
- import { defineRule as defineRule2 } from "@oxlint/plugins";
777
- var exportFilePrefix = defineRule2({
1122
+ import { defineRule as defineRule4 } from "@oxlint/plugins";
1123
+ var exportFilePrefix = defineRule4({
778
1124
  meta: {
779
1125
  type: "problem",
780
1126
  schema: [{
@@ -829,8 +1175,8 @@ var exportFilePrefix = defineRule2({
829
1175
  });
830
1176
 
831
1177
  // src/rules/export-name-pattern.ts
832
- import { defineRule as defineRule3 } from "@oxlint/plugins";
833
- var exportNamePattern = defineRule3({
1178
+ import { defineRule as defineRule5 } from "@oxlint/plugins";
1179
+ var exportNamePattern = defineRule5({
834
1180
  meta: {
835
1181
  type: "problem",
836
1182
  schema: [{
@@ -880,8 +1226,8 @@ var exportNamePattern = defineRule3({
880
1226
  });
881
1227
 
882
1228
  // src/rules/filename-export-name.ts
883
- import { defineRule as defineRule4 } from "@oxlint/plugins";
884
- var filenameExportName = defineRule4({
1229
+ import { defineRule as defineRule6 } from "@oxlint/plugins";
1230
+ var filenameExportName = defineRule6({
885
1231
  meta: {
886
1232
  type: "problem",
887
1233
  schema: [{
@@ -942,8 +1288,8 @@ var filenameExportName = defineRule4({
942
1288
  });
943
1289
 
944
1290
  // src/rules/filename-match.ts
945
- import { defineRule as defineRule5 } from "@oxlint/plugins";
946
- var filenameMatch = defineRule5({
1291
+ import { defineRule as defineRule7 } from "@oxlint/plugins";
1292
+ var filenameMatch = defineRule7({
947
1293
  meta: {
948
1294
  type: "problem",
949
1295
  schema: [{
@@ -977,8 +1323,8 @@ var filenameMatch = defineRule5({
977
1323
  });
978
1324
 
979
1325
  // src/rules/folder-prefix.ts
980
- import { defineRule as defineRule6 } from "@oxlint/plugins";
981
- var folderPrefix = defineRule6({
1326
+ import { defineRule as defineRule8 } from "@oxlint/plugins";
1327
+ var folderPrefix = defineRule8({
982
1328
  meta: {
983
1329
  type: "problem",
984
1330
  schema: [{
@@ -1025,127 +1371,552 @@ var folderPrefix = defineRule6({
1025
1371
  }
1026
1372
  });
1027
1373
 
1028
- // src/rules/no-extra-exports.ts
1029
- import { defineRule as defineRule7 } from "@oxlint/plugins";
1030
- var noExtraExports = defineRule7({
1374
+ // src/rules/jsx-attributes-multiline.ts
1375
+ import { defineRule as defineRule9 } from "@oxlint/plugins";
1376
+ function jsxAttributesMultilineEdits(node, text, attributeIndent, baseIndent, newline) {
1377
+ const first = node.attributes[0];
1378
+ const last = node.attributes.at(-1);
1379
+ if (!first || !last)
1380
+ return null;
1381
+ const prefix = node.typeArguments ?? node.name;
1382
+ const openingRange = [prefix.range[1], first.range[0]];
1383
+ if (!/^\s*$/.test(text.slice(...openingRange)))
1384
+ return null;
1385
+ const edits = [{
1386
+ range: openingRange,
1387
+ text: `${newline}${attributeIndent}`
1388
+ }];
1389
+ for (let index = 1;index < node.attributes.length; index++) {
1390
+ const previous = node.attributes[index - 1];
1391
+ const current = node.attributes[index];
1392
+ const range = [previous.range[1], current.range[0]];
1393
+ if (!/^\s*$/.test(text.slice(...range)))
1394
+ return null;
1395
+ edits.push({ range, text: `${newline}${attributeIndent}` });
1396
+ }
1397
+ const closing = node.range[1] - (node.selfClosing ? 2 : 1);
1398
+ const closingRange = [last.range[1], closing];
1399
+ if (!/^\s*$/.test(text.slice(...closingRange)))
1400
+ return null;
1401
+ edits.push({ range: closingRange, text: `${newline}${baseIndent}` });
1402
+ return edits;
1403
+ }
1404
+ var jsxAttributesMultiline = defineRule9({
1031
1405
  meta: {
1032
- type: "problem",
1406
+ type: "layout",
1407
+ fixable: "whitespace",
1033
1408
  schema: [{
1034
1409
  type: "object",
1035
1410
  additionalProperties: false,
1036
1411
  properties: {
1037
- names: { type: "array", items: { type: "string" } },
1038
- domainStem: { type: "string", enum: [...namingStemModes] },
1039
- trailingRoles: { type: "array", items: { type: "string" } },
1040
- roleSeparators: { type: "array", items: { type: "string" } },
1041
- allowTypeExports: { type: "boolean" }
1042
- },
1043
- required: ["names"]
1412
+ minAttributes: { type: "integer", minimum: 1 },
1413
+ indent: layoutIndentSchema
1414
+ }
1044
1415
  }],
1045
1416
  messages: {
1046
- extraExport: "Export '{{name}}' is not allowed; expected one of: {{allowed}}."
1417
+ multiline: "Put every JSX attribute and the closing token on its own line."
1047
1418
  }
1048
1419
  },
1049
1420
  createOnce(context) {
1421
+ let minAttributes = 3;
1422
+ let indentUnit = layoutIndentUnit();
1423
+ let newline = `
1424
+ `;
1050
1425
  return {
1051
- Program(program) {
1052
- const options = optionsFirst(context);
1053
- const basename = namingFileBasename(context.filename).replace(/\.(tsx?|jsx?)$/, "");
1054
- const domain = namingFileStem(basename, options.domainStem, options.trailingRoles, options.roleSeparators);
1055
- const allowed = options.names.map((template) => template.replaceAll("{Domain}", namingPascalCase(domain)).replaceAll("{domain}", namingCamelCase(domain)));
1056
- for (const binding of exportsCollect(program)) {
1057
- if (options.allowTypeExports && binding.typeOnly) {
1058
- continue;
1059
- }
1060
- const name = binding.localName ?? binding.exportedName;
1061
- if (!allowed.includes(name)) {
1062
- context.report({
1063
- node: binding.node,
1064
- messageId: "extraExport",
1065
- data: { name, allowed: allowed.join(", ") }
1066
- });
1067
- }
1068
- }
1426
+ before() {
1427
+ const options = optionsFirst(context, {});
1428
+ minAttributes = options.minAttributes ?? 3;
1429
+ indentUnit = layoutIndentUnit(options.indent);
1430
+ newline = layoutNewline(context.sourceCode.text);
1431
+ },
1432
+ JSXOpeningElement(node) {
1433
+ if (node.attributes.length < minAttributes || node.loc.start.line !== node.loc.end.line)
1434
+ return;
1435
+ const source = context.sourceCode;
1436
+ const text = source.text;
1437
+ const baseIndent = layoutLineIndent(text, node.range[0]);
1438
+ const hasComments = source.getCommentsInside(node).length > 0;
1439
+ const edits = hasComments ? null : jsxAttributesMultilineEdits(node, text, baseIndent + indentUnit, baseIndent, newline);
1440
+ context.report({
1441
+ node,
1442
+ messageId: "multiline",
1443
+ fix: edits ? (fixer) => edits.map((edit) => fixer.replaceTextRange(edit.range, edit.text)) : undefined
1444
+ });
1069
1445
  }
1070
1446
  };
1071
1447
  }
1072
1448
  });
1073
1449
 
1074
- // src/rules/no-extra-factory-keys.ts
1075
- import { defineRule as defineRule8 } from "@oxlint/plugins";
1076
- var noExtraFactoryKeys = defineRule8({
1450
+ // src/rules/key-value-same-line.ts
1451
+ import { defineRule as defineRule10 } from "@oxlint/plugins";
1452
+ var keyValueSameLine = defineRule10({
1077
1453
  meta: {
1078
- type: "problem",
1079
- schema: [{
1080
- type: "object",
1081
- additionalProperties: false,
1082
- properties: {
1083
- keys: { type: "array", items: { type: "string" } },
1084
- factoryPattern: { type: "string" }
1085
- },
1086
- required: ["keys"]
1087
- }],
1454
+ type: "layout",
1455
+ fixable: "whitespace",
1456
+ schema: [],
1088
1457
  messages: {
1089
- extraKey: "Factory '{{factory}}' must not return key '{{key}}'; allowed keys: {{allowed}}."
1458
+ sameLine: "Keep the object key and the start of its value on the same line."
1090
1459
  }
1091
1460
  },
1092
1461
  createOnce(context) {
1462
+ let source;
1093
1463
  return {
1094
- Program(program) {
1095
- const { keys, factoryPattern = "^make[A-Z]" } = optionsFirst(context);
1096
- for (const factory of factoriesCollectTopLevel(program, new RegExp(factoryPattern))) {
1097
- for (const object of factoriesDirectlyReturnedObjects(factory)) {
1098
- for (const property of object.properties) {
1099
- const key = factoriesObjectPropertyName(property);
1100
- if (!key || !keys.includes(key)) {
1101
- context.report({
1102
- node: property,
1103
- messageId: "extraKey",
1104
- data: { factory: factory.name, key: key ?? "<computed or spread>", allowed: keys.join(", ") }
1105
- });
1106
- }
1107
- }
1464
+ before() {
1465
+ source = context.sourceCode;
1466
+ },
1467
+ Property(node) {
1468
+ if (node.parent.type !== "ObjectExpression" || node.shorthand || node.method || node.kind !== "init") {
1469
+ return;
1470
+ }
1471
+ let keyEnd = node.key.range[1];
1472
+ let keyEndLine = node.key.loc.end.line;
1473
+ if (node.computed) {
1474
+ const bracket = source.getTokensBetween(node.key, node.value).find((token) => token.value === "]");
1475
+ if (bracket) {
1476
+ keyEnd = bracket.range[1];
1477
+ keyEndLine = bracket.loc.end.line;
1108
1478
  }
1109
1479
  }
1480
+ if (keyEndLine === node.value.loc.start.line) {
1481
+ return;
1482
+ }
1483
+ const between = source.text.slice(keyEnd, node.value.range[0]);
1484
+ const commentsBetween = source.commentsExistBetween(node.key, node.value);
1485
+ const onlyColonAndSpace = /^[\s:]*$/.test(between) && between.includes(":");
1486
+ context.report({
1487
+ node,
1488
+ messageId: "sameLine",
1489
+ fix(fixer) {
1490
+ return !commentsBetween && onlyColonAndSpace ? fixer.replaceTextRange([keyEnd, node.value.range[0]], ": ") : null;
1491
+ }
1492
+ });
1110
1493
  }
1111
1494
  };
1112
1495
  }
1113
1496
  });
1114
1497
 
1115
- // src/rules/no-file-level-helpers.ts
1116
- import { defineRule as defineRule9 } from "@oxlint/plugins";
1117
- var noFileLevelHelpers = defineRule9({
1498
+ // src/rules/no-comments.ts
1499
+ import { defineRule as defineRule11 } from "@oxlint/plugins";
1500
+ var NO_COMMENTS_LINE_GAP = /^[\t \r\n\u2028\u2029]*$/;
1501
+ var NO_COMMENTS_LINE_TERMINATOR = /\r\n|[\n\r\u2028\u2029]/;
1502
+ var NO_COMMENTS_TRAILING_LINES = /^[\t ]*(?:\r\n|[\n\r\u2028\u2029])(?:[\t ]*(?:\r\n|[\n\r\u2028\u2029]))*/;
1503
+ var NO_COMMENTS_HORIZONTAL_SPACE = /^[\t ]*$/;
1504
+ function noCommentsCreateLineIndex(text) {
1505
+ const starts = [0];
1506
+ const ends = [];
1507
+ for (let index = 0;index < text.length; index++) {
1508
+ const character = text[index];
1509
+ if (character === "\r") {
1510
+ ends.push(index);
1511
+ if (text[index + 1] === `
1512
+ `)
1513
+ index++;
1514
+ starts.push(index + 1);
1515
+ } else if (character === `
1516
+ ` || character === "\u2028" || character === "\u2029") {
1517
+ ends.push(index);
1518
+ starts.push(index + 1);
1519
+ }
1520
+ }
1521
+ ends.push(text.length);
1522
+ return { starts, ends };
1523
+ }
1524
+ function noCommentsLineNumber(lines, offset) {
1525
+ let low = 0;
1526
+ let high = lines.starts.length;
1527
+ while (low < high) {
1528
+ const middle = low + high >>> 1;
1529
+ if (lines.starts[middle] <= offset)
1530
+ low = middle + 1;
1531
+ else
1532
+ high = middle;
1533
+ }
1534
+ return low - 1;
1535
+ }
1536
+ function noCommentsLineStart(lines, offset) {
1537
+ return lines.starts[noCommentsLineNumber(lines, offset)];
1538
+ }
1539
+ function noCommentsLineEnd(lines, offset) {
1540
+ return lines.ends[noCommentsLineNumber(lines, offset)];
1541
+ }
1542
+ function noCommentsIncludesLine(lines, start, end) {
1543
+ let low = 0;
1544
+ let high = lines.length;
1545
+ while (low < high) {
1546
+ const middle = low + high >>> 1;
1547
+ if (lines[middle] < start)
1548
+ low = middle + 1;
1549
+ else
1550
+ high = middle;
1551
+ }
1552
+ return lines[low] <= end;
1553
+ }
1554
+ function noCommentsFollowsOpeningBrace(text, offset) {
1555
+ for (let index = offset - 1;index >= 0; index--) {
1556
+ const character = text[index];
1557
+ if (!/\s/.test(character))
1558
+ return character === "{";
1559
+ }
1560
+ return false;
1561
+ }
1562
+ function noCommentsGroups(comments, text) {
1563
+ const groups = [];
1564
+ for (const comment of comments) {
1565
+ const current = groups.at(-1);
1566
+ const previous = current?.at(-1);
1567
+ if (current && previous) {
1568
+ const between = text.slice(previous.range[1], comment.range[0]);
1569
+ if (NO_COMMENTS_LINE_GAP.test(between)) {
1570
+ current.push(comment);
1571
+ continue;
1572
+ }
1573
+ }
1574
+ groups.push([comment]);
1575
+ }
1576
+ return groups;
1577
+ }
1578
+ function noCommentsJsxEmptyContainer(containers, start, end) {
1579
+ let low = 0;
1580
+ let high = containers.length;
1581
+ while (low < high) {
1582
+ const middle = low + high >>> 1;
1583
+ if (containers[middle].range[0] <= start)
1584
+ low = middle + 1;
1585
+ else
1586
+ high = middle;
1587
+ }
1588
+ const container = containers[low - 1];
1589
+ return container?.range[1] >= end ? container : undefined;
1590
+ }
1591
+ var noComments = defineRule11({
1118
1592
  meta: {
1119
- type: "problem",
1593
+ type: "suggestion",
1594
+ fixable: "code",
1120
1595
  schema: [{
1121
1596
  type: "object",
1122
1597
  additionalProperties: false,
1123
1598
  properties: {
1124
- allowPattern: { type: "string" },
1125
- detectComponents: { type: "boolean" },
1126
- hookPattern: { type: "string" }
1599
+ allowWhy: { type: "boolean", default: true },
1600
+ allowPatterns: { type: "array", items: { type: "string" } },
1601
+ allowJsdoc: { type: "boolean", default: false }
1127
1602
  }
1128
1603
  }],
1129
1604
  messages: {
1130
- helper: "File-level helper {{name}} must move inside its owning component or hook."
1605
+ comment: "Remove this comment."
1131
1606
  }
1132
1607
  },
1133
1608
  createOnce(context) {
1609
+ let allowWhy = true;
1610
+ let allowJsdoc = false;
1611
+ let allowed = [];
1134
1612
  return {
1613
+ before() {
1614
+ const options = optionsFirst(context, {});
1615
+ allowWhy = options.allowWhy ?? true;
1616
+ allowJsdoc = options.allowJsdoc ?? false;
1617
+ allowed = (options.allowPatterns ?? []).map((pattern) => new RegExp(pattern));
1618
+ },
1135
1619
  Program(program) {
1136
- const {
1137
- allowPattern,
1138
- detectComponents = true,
1139
- hookPattern = "^use[A-Z]"
1140
- } = optionsFirst(context, {});
1141
- const allowed = allowPattern ? new RegExp(allowPattern) : null;
1142
- const hooks = new RegExp(hookPattern);
1143
- for (const candidate of declarationsFileLevelFunctionCandidates(program)) {
1144
- if (detectComponents && reactComponentsIsLike(program, candidate) || hooks.test(candidate.name) || allowed?.test(candidate.name)) {
1145
- continue;
1620
+ const source = context.sourceCode;
1621
+ const text = source.text;
1622
+ const comments = source.getAllComments().filter((comment) => comment.type !== "Shebang");
1623
+ const removable = [];
1624
+ const protectedTargetLines = [];
1625
+ let mightContainJsxComment = false;
1626
+ let whyContinuation = false;
1627
+ let previousComment;
1628
+ for (const comment of comments) {
1629
+ const value = commentsNormalizedValue(comment);
1630
+ if (!mightContainJsxComment && comment.type === "Block" && noCommentsFollowsOpeningBrace(text, comment.range[0])) {
1631
+ mightContainJsxComment = true;
1146
1632
  }
1147
- context.report({
1148
- node: candidate.node,
1633
+ const directlyFollowsPrevious = previousComment !== undefined && comment.loc.start.line === previousComment.loc.end.line + 1 && NO_COMMENTS_LINE_GAP.test(text.slice(previousComment.range[1], comment.range[0]));
1634
+ const continuesWhy = comment.type === "Line" && whyContinuation && directlyFollowsPrevious;
1635
+ const isWhy = commentsIsWhy(value) || continuesWhy;
1636
+ if (commentsIsDisableNextLine(value)) {
1637
+ protectedTargetLines.push(comment.loc.end.line + 1);
1638
+ }
1639
+ whyContinuation = comment.type === "Line" && isWhy;
1640
+ previousComment = comment;
1641
+ if (allowWhy && isWhy)
1642
+ continue;
1643
+ if (commentsIsDirective(value))
1644
+ continue;
1645
+ if (allowJsdoc && commentsIsJsdoc(text, comment))
1646
+ continue;
1647
+ if (allowed.some((pattern) => pattern.test(value.trim())))
1648
+ continue;
1649
+ removable.push(comment);
1650
+ }
1651
+ if (removable.length === 0)
1652
+ return;
1653
+ const lines = noCommentsCreateLineIndex(text);
1654
+ const jsxContainers = [];
1655
+ if (mightContainJsxComment) {
1656
+ astVisit(program, [], (node) => {
1657
+ if (node.type === "JSXExpressionContainer" && node.expression.type === "JSXEmptyExpression") {
1658
+ jsxContainers.push(node);
1659
+ }
1660
+ });
1661
+ jsxContainers.sort((left, right) => left.range[0] - right.range[0]);
1662
+ }
1663
+ for (const group of noCommentsGroups(removable, text)) {
1664
+ const first = group[0];
1665
+ const final = group[group.length - 1];
1666
+ const container = noCommentsJsxEmptyContainer(jsxContainers, first.range[0], final.range[1]);
1667
+ let embeddedUnicodeLineTerminator;
1668
+ for (const comment of group) {
1669
+ if (comment.type === "Block" && embeddedUnicodeLineTerminator === undefined) {
1670
+ embeddedUnicodeLineTerminator = text.slice(comment.range[0], comment.range[1]).match(/[\u2028\u2029]/)?.[0];
1671
+ }
1672
+ }
1673
+ const reportOnly = container !== undefined || noCommentsIncludesLine(protectedTargetLines, first.loc.start.line, final.loc.end.line);
1674
+ const range0 = first.range[0];
1675
+ const range1 = final.range[1];
1676
+ context.report({
1677
+ loc: { start: first.loc.start, end: final.loc.end },
1678
+ messageId: "comment",
1679
+ ...reportOnly ? {} : {
1680
+ fix(fixer) {
1681
+ const lineStart = noCommentsLineStart(lines, range0);
1682
+ const before = text.slice(lineStart, range0);
1683
+ if (NO_COMMENTS_HORIZONTAL_SPACE.test(before)) {
1684
+ const trailingLines = text.slice(range1).match(NO_COMMENTS_TRAILING_LINES);
1685
+ if (trailingLines) {
1686
+ const end = range1 + trailingLines[0].length;
1687
+ return embeddedUnicodeLineTerminator ? fixer.replaceTextRange([lineStart, end], embeddedUnicodeLineTerminator) : fixer.removeRange([lineStart, end]);
1688
+ }
1689
+ }
1690
+ const removed = text.slice(range0, range1);
1691
+ const lineBreak = removed.match(NO_COMMENTS_LINE_TERMINATOR)?.[0];
1692
+ let afterOnLine;
1693
+ if (lineBreak) {
1694
+ afterOnLine = text.slice(range1, noCommentsLineEnd(lines, range1));
1695
+ if (/\S/.test(before) && /\S/.test(afterOnLine)) {
1696
+ return fixer.replaceTextRange([range0, range1], lineBreak);
1697
+ }
1698
+ }
1699
+ if (embeddedUnicodeLineTerminator) {
1700
+ return fixer.replaceTextRange([range0, range1], embeddedUnicodeLineTerminator);
1701
+ }
1702
+ afterOnLine ??= text.slice(range1, noCommentsLineEnd(lines, range1));
1703
+ if (NO_COMMENTS_HORIZONTAL_SPACE.test(afterOnLine)) {
1704
+ const start = range0 - (/[\t ]*$/.exec(before)?.[0].length ?? 0);
1705
+ return fixer.removeRange([start, range1]);
1706
+ }
1707
+ const left = text[range0 - 1];
1708
+ const right = text[range1];
1709
+ const separator = left && right && !/\s/.test(left) && !/\s/.test(right) ? " " : "";
1710
+ return fixer.replaceTextRange([range0, range1], separator);
1711
+ }
1712
+ }
1713
+ });
1714
+ }
1715
+ }
1716
+ };
1717
+ }
1718
+ });
1719
+
1720
+ // src/rules/no-extra-exports.ts
1721
+ import { defineRule as defineRule12 } from "@oxlint/plugins";
1722
+ var noExtraExports = defineRule12({
1723
+ meta: {
1724
+ type: "problem",
1725
+ schema: [{
1726
+ type: "object",
1727
+ additionalProperties: false,
1728
+ properties: {
1729
+ names: { type: "array", items: { type: "string" } },
1730
+ patterns: { type: "array", items: { type: "string" } },
1731
+ domainStem: { type: "string", enum: [...namingStemModes] },
1732
+ trailingRoles: { type: "array", items: { type: "string" } },
1733
+ roleSeparators: { type: "array", items: { type: "string" } },
1734
+ allowTypeExports: { type: "boolean" }
1735
+ },
1736
+ required: ["names"]
1737
+ }],
1738
+ messages: {
1739
+ extraExport: "Export '{{name}}' is not allowed; expected one of: {{allowed}}."
1740
+ }
1741
+ },
1742
+ createOnce(context) {
1743
+ return {
1744
+ Program(program) {
1745
+ const options = optionsFirst(context);
1746
+ const basename = namingFileBasename(context.filename).replace(/\.(tsx?|jsx?)$/, "");
1747
+ const domain = namingFileStem(basename, options.domainStem, options.trailingRoles, options.roleSeparators);
1748
+ const replacements = {
1749
+ Domain: namingPascalCase(domain),
1750
+ domain: namingCamelCase(domain)
1751
+ };
1752
+ const allowed = options.names.map((template) => template.replaceAll("{Domain}", replacements.Domain).replaceAll("{domain}", replacements.domain));
1753
+ const patterns = (options.patterns ?? []).map((template) => {
1754
+ const source = template.replaceAll("{Domain}", replacements.Domain.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&")).replaceAll("{domain}", replacements.domain.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&"));
1755
+ return new RegExp(`^(?:${source})$`);
1756
+ });
1757
+ for (const binding of exportsCollect(program)) {
1758
+ if (options.allowTypeExports && binding.typeOnly) {
1759
+ continue;
1760
+ }
1761
+ const name = binding.localName ?? binding.exportedName;
1762
+ if (!allowed.includes(name) && !patterns.some((pattern) => pattern.test(name))) {
1763
+ context.report({
1764
+ node: binding.node,
1765
+ messageId: "extraExport",
1766
+ data: {
1767
+ name,
1768
+ allowed: [...allowed, ...patterns.map((pattern) => pattern.toString())].join(", ")
1769
+ }
1770
+ });
1771
+ }
1772
+ }
1773
+ }
1774
+ };
1775
+ }
1776
+ });
1777
+
1778
+ // src/rules/no-extra-factory-keys.ts
1779
+ import { defineRule as defineRule13 } from "@oxlint/plugins";
1780
+ function compileFactoryKeys(pattern, keys, requireKeys) {
1781
+ const allowedList = keys.concat(requireKeys.filter((key) => !keys.includes(key)));
1782
+ return {
1783
+ pattern: new RegExp(pattern),
1784
+ allowed: new Set(allowedList),
1785
+ allowedList,
1786
+ requireKeys
1787
+ };
1788
+ }
1789
+ var noExtraFactoryKeys = defineRule13({
1790
+ meta: {
1791
+ type: "problem",
1792
+ schema: [{
1793
+ type: "object",
1794
+ additionalProperties: false,
1795
+ properties: {
1796
+ keys: { type: "array", items: { type: "string" } },
1797
+ factoryPattern: { type: "string" },
1798
+ requireKeys: { type: "array", items: { type: "string" } },
1799
+ keySets: {
1800
+ type: "array",
1801
+ items: {
1802
+ type: "object",
1803
+ additionalProperties: false,
1804
+ properties: {
1805
+ factoryPattern: { type: "string" },
1806
+ keys: { type: "array", items: { type: "string" } },
1807
+ requireKeys: { type: "array", items: { type: "string" } }
1808
+ },
1809
+ required: ["factoryPattern", "keys"]
1810
+ }
1811
+ }
1812
+ }
1813
+ }],
1814
+ messages: {
1815
+ extraKey: "Factory '{{factory}}' must not return key '{{key}}'; allowed keys: {{allowed}}.",
1816
+ missingKey: "Factory '{{factory}}' must return key '{{key}}'.",
1817
+ emptyObject: "Factory '{{factory}}' must not return an empty object."
1818
+ }
1819
+ },
1820
+ createOnce(context) {
1821
+ let keySets = [];
1822
+ let fallback;
1823
+ function resolveFactoryKeys(name) {
1824
+ const matched = keySets.find((item) => item.pattern.test(name));
1825
+ if (matched) {
1826
+ return matched;
1827
+ }
1828
+ if (fallback?.pattern.test(name)) {
1829
+ return fallback;
1830
+ }
1831
+ }
1832
+ return {
1833
+ before() {
1834
+ const options = optionsFirst(context, {});
1835
+ const { keys, factoryPattern = "^make[A-Z]", requireKeys = [], keySets: configured } = options;
1836
+ if (!keys && !configured) {
1837
+ return false;
1838
+ }
1839
+ keySets = (configured ?? []).map((item) => compileFactoryKeys(item.factoryPattern, item.keys, item.requireKeys ?? []));
1840
+ fallback = keys ? compileFactoryKeys(factoryPattern, keys, requireKeys) : undefined;
1841
+ },
1842
+ Program(program) {
1843
+ for (const factory of factoriesCollectTopLevel(program, /^/)) {
1844
+ const resolved = resolveFactoryKeys(factory.name);
1845
+ if (!resolved) {
1846
+ continue;
1847
+ }
1848
+ for (const object of factoriesDirectlyReturnedObjects(factory)) {
1849
+ const present = new Set(object.properties.flatMap((property) => {
1850
+ const key = factoriesObjectPropertyName(property);
1851
+ return key ? [key] : [];
1852
+ }));
1853
+ if (object.properties.length === 0 && resolved.requireKeys.length > 0) {
1854
+ context.report({
1855
+ node: object,
1856
+ messageId: "emptyObject",
1857
+ data: { factory: factory.name }
1858
+ });
1859
+ }
1860
+ for (const property of object.properties) {
1861
+ const key = factoriesObjectPropertyName(property);
1862
+ if (!key || !resolved.allowed.has(key)) {
1863
+ context.report({
1864
+ node: property,
1865
+ messageId: "extraKey",
1866
+ data: { factory: factory.name, key: key ?? "<computed or spread>", allowed: resolved.allowedList.join(", ") }
1867
+ });
1868
+ }
1869
+ }
1870
+ for (const key of resolved.requireKeys) {
1871
+ if (!present.has(key)) {
1872
+ context.report({
1873
+ node: object,
1874
+ messageId: "missingKey",
1875
+ data: { factory: factory.name, key }
1876
+ });
1877
+ }
1878
+ }
1879
+ }
1880
+ }
1881
+ }
1882
+ };
1883
+ }
1884
+ });
1885
+
1886
+ // src/rules/no-file-level-helpers.ts
1887
+ import { defineRule as defineRule14 } from "@oxlint/plugins";
1888
+ var noFileLevelHelpers = defineRule14({
1889
+ meta: {
1890
+ type: "problem",
1891
+ schema: [{
1892
+ type: "object",
1893
+ additionalProperties: false,
1894
+ properties: {
1895
+ allowPattern: { type: "string" },
1896
+ detectComponents: { type: "boolean" },
1897
+ hookPattern: { type: "string" }
1898
+ }
1899
+ }],
1900
+ messages: {
1901
+ helper: "File-level helper {{name}} must move inside its owning component or hook."
1902
+ }
1903
+ },
1904
+ createOnce(context) {
1905
+ return {
1906
+ Program(program) {
1907
+ const {
1908
+ allowPattern,
1909
+ detectComponents = true,
1910
+ hookPattern = "^use[A-Z]"
1911
+ } = optionsFirst(context, {});
1912
+ const allowed = allowPattern ? new RegExp(allowPattern) : null;
1913
+ const hooks = new RegExp(hookPattern);
1914
+ for (const candidate of declarationsFileLevelFunctionCandidates(program)) {
1915
+ if (detectComponents && reactComponentsIsLike(program, candidate) || hooks.test(candidate.name) || allowed?.test(candidate.name)) {
1916
+ continue;
1917
+ }
1918
+ context.report({
1919
+ node: candidate.node,
1149
1920
  messageId: "helper",
1150
1921
  data: { name: candidate.name }
1151
1922
  });
@@ -1156,7 +1927,7 @@ var noFileLevelHelpers = defineRule9({
1156
1927
  });
1157
1928
 
1158
1929
  // src/rules/no-imported-type-alias.ts
1159
- import { defineRule as defineRule10 } from "@oxlint/plugins";
1930
+ import { defineRule as defineRule15 } from "@oxlint/plugins";
1160
1931
  function importedAliasTarget(declaration, imports) {
1161
1932
  if (declaration?.type !== "TSTypeAliasDeclaration" || declaration.typeAnnotation.type !== "TSTypeReference" || declaration.typeAnnotation.typeName.type !== "Identifier" || !imports.has(declaration.typeAnnotation.typeName.name)) {
1162
1933
  return;
@@ -1166,7 +1937,7 @@ function importedAliasTarget(declaration, imports) {
1166
1937
  function specifierLocalName(specifier) {
1167
1938
  return specifier.local.type === "Identifier" ? specifier.local.name : specifier.local.value;
1168
1939
  }
1169
- var noImportedTypeAlias = defineRule10({
1940
+ var noImportedTypeAlias = defineRule15({
1170
1941
  meta: {
1171
1942
  type: "problem",
1172
1943
  schema: [],
@@ -1216,8 +1987,8 @@ var noImportedTypeAlias = defineRule10({
1216
1987
  });
1217
1988
 
1218
1989
  // src/rules/no-inline-schema-elements.ts
1219
- import { defineRule as defineRule11 } from "@oxlint/plugins";
1220
- var noInlineSchemaElements = defineRule11({
1990
+ import { defineRule as defineRule16 } from "@oxlint/plugins";
1991
+ var noInlineSchemaElements = defineRule16({
1221
1992
  meta: {
1222
1993
  type: "problem",
1223
1994
  schema: [{
@@ -1265,7 +2036,7 @@ var noInlineSchemaElements = defineRule11({
1265
2036
  });
1266
2037
 
1267
2038
  // src/rules/no-inline-types.ts
1268
- import { defineRule as defineRule12 } from "@oxlint/plugins";
2039
+ import { defineRule as defineRule17 } from "@oxlint/plugins";
1269
2040
  function parameterAnnotation(parameter) {
1270
2041
  if (parameter.type === "TSParameterProperty") {
1271
2042
  return parameterAnnotation(parameter.parameter);
@@ -1275,7 +2046,7 @@ function parameterAnnotation(parameter) {
1275
2046
  }
1276
2047
  return parameter.typeAnnotation;
1277
2048
  }
1278
- var noInlineTypes = defineRule12({
2049
+ var noInlineTypes = defineRule17({
1279
2050
  meta: {
1280
2051
  type: "problem",
1281
2052
  schema: [{
@@ -1284,7 +2055,8 @@ var noInlineTypes = defineRule12({
1284
2055
  properties: {
1285
2056
  parameters: { type: "boolean" },
1286
2057
  returns: { type: "boolean" },
1287
- functionTypes: { type: "boolean" }
2058
+ functionTypes: { type: "boolean" },
2059
+ minMembers: { type: "integer", minimum: 1 }
1288
2060
  }
1289
2061
  }],
1290
2062
  messages: {
@@ -1292,48 +2064,111 @@ var noInlineTypes = defineRule12({
1292
2064
  }
1293
2065
  },
1294
2066
  createOnce(context) {
2067
+ let checkParameters = true;
2068
+ let checkReturns = true;
2069
+ let checkFunctionTypes = true;
2070
+ let minMembers = 1;
2071
+ function isInline(node) {
2072
+ return node.type === "TSTypeLiteral" && (minMembers === 1 || node.members.length >= minMembers) || checkFunctionTypes && (node.type === "TSFunctionType" || node.type === "TSConstructorType");
2073
+ }
2074
+ function check(annotation, position) {
2075
+ astVisit(annotation, [], (node, ancestors) => {
2076
+ if (!isInline(node) || ancestors.some(isInline)) {
2077
+ return;
2078
+ }
2079
+ context.report({
2080
+ node,
2081
+ messageId: "inlineType",
2082
+ data: { kind: node.type === "TSTypeLiteral" ? "object" : "function", position }
2083
+ });
2084
+ });
2085
+ }
2086
+ function checkFunction(node) {
2087
+ if (checkParameters) {
2088
+ for (const parameter of node.params) {
2089
+ check(parameterAnnotation(parameter), "parameter");
2090
+ }
2091
+ }
2092
+ if (checkReturns) {
2093
+ check(node.returnType, "return");
2094
+ }
2095
+ }
1295
2096
  return {
1296
- Program(program) {
1297
- const { parameters = true, returns = true, functionTypes = true } = optionsFirst(context, {});
1298
- const isInline = (node) => node.type === "TSTypeLiteral" || functionTypes && (node.type === "TSFunctionType" || node.type === "TSConstructorType");
1299
- function check(annotation, position) {
1300
- astVisit(annotation, [], (node, ancestors) => {
1301
- if (!isInline(node) || ancestors.some(isInline)) {
1302
- return;
1303
- }
1304
- context.report({
1305
- node,
1306
- messageId: "inlineType",
1307
- data: { kind: node.type === "TSTypeLiteral" ? "object" : "function", position }
1308
- });
2097
+ before() {
2098
+ const options = optionsFirst(context, {});
2099
+ checkParameters = options.parameters ?? true;
2100
+ checkReturns = options.returns ?? true;
2101
+ checkFunctionTypes = options.functionTypes ?? true;
2102
+ minMembers = options.minMembers ?? 1;
2103
+ if (!checkParameters && !checkReturns) {
2104
+ return false;
2105
+ }
2106
+ },
2107
+ FunctionDeclaration: checkFunction,
2108
+ FunctionExpression: checkFunction,
2109
+ ArrowFunctionExpression: checkFunction,
2110
+ TSDeclareFunction: checkFunction,
2111
+ TSEmptyBodyFunctionExpression: checkFunction
2112
+ };
2113
+ }
2114
+ });
2115
+
2116
+ // src/rules/no-literal-in.ts
2117
+ import { defineRule as defineRule18 } from "@oxlint/plugins";
2118
+ function literalKey(node) {
2119
+ if (node.type === "Literal" && typeof node.value === "string") {
2120
+ return node.value;
2121
+ }
2122
+ if (node.type === "TemplateLiteral" && node.expressions.length === 0) {
2123
+ return node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw;
2124
+ }
2125
+ }
2126
+ var noLiteralIn = defineRule18({
2127
+ meta: {
2128
+ type: "suggestion",
2129
+ schema: [{
2130
+ type: "object",
2131
+ additionalProperties: false,
2132
+ properties: {
2133
+ allow: { type: "array", items: { type: "string" } }
2134
+ }
2135
+ }],
2136
+ defaultOptions: [{ allow: [] }],
2137
+ messages: {
2138
+ literalIn: "Avoid '{{key}}' in value; use Object.hasOwn() or a discriminated union."
2139
+ }
2140
+ },
2141
+ createOnce(context) {
2142
+ let allow = new Set;
2143
+ return {
2144
+ before() {
2145
+ allow = new Set(optionsFirst(context, { allow: [] }).allow);
2146
+ },
2147
+ BinaryExpression(node) {
2148
+ if (node.operator !== "in") {
2149
+ return;
2150
+ }
2151
+ const key = literalKey(node.left);
2152
+ if (key !== undefined && !allow.has(key)) {
2153
+ context.report({
2154
+ node: node.left,
2155
+ messageId: "literalIn",
2156
+ data: { key }
1309
2157
  });
1310
2158
  }
1311
- astVisit(program, [], (node) => {
1312
- if (node.type !== "FunctionDeclaration" && node.type !== "FunctionExpression" && node.type !== "ArrowFunctionExpression" && node.type !== "TSDeclareFunction" && node.type !== "TSEmptyBodyFunctionExpression") {
1313
- return;
1314
- }
1315
- if (parameters) {
1316
- for (const parameter of node.params) {
1317
- check(parameterAnnotation(parameter), "parameter");
1318
- }
1319
- }
1320
- if (returns) {
1321
- check(node.returnType, "return");
1322
- }
1323
- });
1324
2159
  }
1325
2160
  };
1326
2161
  }
1327
2162
  });
1328
2163
 
1329
2164
  // src/rules/no-local-schema-construction.ts
1330
- import { defineRule as defineRule13 } from "@oxlint/plugins";
2165
+ import { defineRule as defineRule19 } from "@oxlint/plugins";
1331
2166
  var defaultRuntimeImportMessage = "Do not import runtime schema constructors from {{source}}; import a named schema instead.";
1332
2167
  var defaultConstructionMessage = "Do not construct schemas locally; import a named schema instead.";
1333
2168
  function formatMessage(template, data) {
1334
2169
  return Object.entries(data).reduce((result, [key, value]) => result.replaceAll(`{{${key}}}`, value), template);
1335
2170
  }
1336
- var noLocalSchemaConstruction = defineRule13({
2171
+ var noLocalSchemaConstruction = defineRule19({
1337
2172
  meta: {
1338
2173
  type: "problem",
1339
2174
  schema: [{
@@ -1445,9 +2280,8 @@ var noLocalSchemaConstruction = defineRule13({
1445
2280
  });
1446
2281
 
1447
2282
  // src/rules/no-member-comments.ts
1448
- import { defineRule as defineRule14 } from "@oxlint/plugins";
1449
- var DIRECTIVE = /^\s*(?:@ts-(?:ignore|expect-error|nocheck)|(?:es|ox)lint-(?:disable|enable)|prettier-ignore|biome-ignore|[cv]8 ignore|istanbul ignore|webpack[A-Z])/;
1450
- var noMemberComments = defineRule14({
2283
+ import { defineRule as defineRule20 } from "@oxlint/plugins";
2284
+ var noMemberComments = defineRule20({
1451
2285
  meta: {
1452
2286
  type: "suggestion",
1453
2287
  fixable: "code",
@@ -1479,8 +2313,8 @@ var noMemberComments = defineRule14({
1479
2313
  let previousLine = -1;
1480
2314
  const removable = [];
1481
2315
  for (const comment of comments) {
1482
- const value = (comment.type === "Block" ? comment.value.replace(/^\s*\* ?/gm, "") : comment.value).trimStart();
1483
- const isWhy = value.startsWith("Why: ") || comment.type === "Line" && whyContinuation && comment.loc.start.line === previousLine + 1;
2316
+ const value = commentsNormalizedValue(comment);
2317
+ const isWhy = commentsIsWhy(value) || comment.type === "Line" && whyContinuation && comment.loc.start.line === previousLine + 1;
1484
2318
  whyContinuation = comment.type === "Line" && isWhy;
1485
2319
  previousLine = comment.loc.end.line;
1486
2320
  if (reported.has(comment.range[0]))
@@ -1488,7 +2322,7 @@ var noMemberComments = defineRule14({
1488
2322
  reported.add(comment.range[0]);
1489
2323
  if (allowWhy && isWhy)
1490
2324
  continue;
1491
- if (DIRECTIVE.test(value))
2325
+ if (commentsIsDirective(value))
1492
2326
  continue;
1493
2327
  removable.push(comment);
1494
2328
  }
@@ -1540,36 +2374,249 @@ var noMemberComments = defineRule14({
1540
2374
  }
1541
2375
  });
1542
2376
 
1543
- // src/rules/no-rederive-schema.ts
1544
- import { defineRule as defineRule15 } from "@oxlint/plugins";
1545
- var noRederiveSchema = defineRule15({
2377
+ // src/rules/no-module-mutable-state.ts
2378
+ import { defineRule as defineRule21 } from "@oxlint/plugins";
2379
+ function moduleMutableStateBindings(pattern) {
2380
+ if (pattern.type === "Identifier") {
2381
+ return [{ name: pattern.name, node: pattern }];
2382
+ }
2383
+ if (pattern.type === "AssignmentPattern") {
2384
+ return moduleMutableStateBindings(pattern.left);
2385
+ }
2386
+ if (pattern.type === "RestElement") {
2387
+ return moduleMutableStateBindings(pattern.argument);
2388
+ }
2389
+ if (pattern.type === "ObjectPattern") {
2390
+ return pattern.properties.flatMap((property) => property.type === "RestElement" ? moduleMutableStateBindings(property) : moduleMutableStateBindings(property.value));
2391
+ }
2392
+ if (pattern.type === "ArrayPattern") {
2393
+ return pattern.elements.flatMap((element) => element ? moduleMutableStateBindings(element) : []);
2394
+ }
2395
+ return [];
2396
+ }
2397
+ function moduleMutableStateInsideFunction(node) {
2398
+ for (let current = node.parent;current; current = current.parent) {
2399
+ if (current.type === "FunctionDeclaration" || current.type === "FunctionExpression" || current.type === "ArrowFunctionExpression" || current.type === "TSDeclareFunction" || current.type === "TSEmptyBodyFunctionExpression" || current.type === "StaticBlock") {
2400
+ return true;
2401
+ }
2402
+ }
2403
+ return false;
2404
+ }
2405
+ function moduleMutableStateIsModuleLet(node) {
2406
+ const parent = node.parent;
2407
+ return parent?.type === "Program" || parent?.type === "ExportNamedDeclaration" && parent.parent?.type === "Program";
2408
+ }
2409
+ function moduleMutableStateInsideAmbient(node) {
2410
+ for (let current = node.parent;current; current = current.parent) {
2411
+ if (current.type === "TSModuleDeclaration" && (current.declare || current.global || current.id.type === "Literal")) {
2412
+ return true;
2413
+ }
2414
+ }
2415
+ return false;
2416
+ }
2417
+ var noModuleMutableState = defineRule21({
1546
2418
  meta: {
1547
2419
  type: "problem",
1548
2420
  schema: [{
1549
2421
  type: "object",
1550
2422
  additionalProperties: false,
1551
2423
  properties: {
1552
- from: { type: "array", items: { type: "string" } },
1553
- namespaces: { type: "array", items: { type: "string" } },
1554
- operators: { type: "array", items: { type: "string" } }
1555
- },
1556
- required: ["from"]
2424
+ kinds: {
2425
+ type: "array",
2426
+ items: { type: "string", enum: ["let", "var"] }
2427
+ },
2428
+ allowNamePattern: { type: "string" }
2429
+ }
1557
2430
  }],
1558
2431
  messages: {
1559
- rederive: "Do not derive {{operator}} from imported schema {{schema}}; import its DTO type."
2432
+ moduleState: "module state is shared across requests/tests; keep it local."
1560
2433
  }
1561
2434
  },
1562
2435
  createOnce(context) {
1563
- let imported = new Set;
2436
+ let kindSet = new Set(["let", "var"]);
2437
+ let allowed = null;
1564
2438
  return {
1565
- Program(program) {
1566
- const { from } = optionsFirst(context);
1567
- imported = new Set(program.body.flatMap((statement) => statement.type === "ImportDeclaration" && from.includes(statement.source.value) ? statement.specifiers.map((specifier) => specifier.local.name) : []));
2439
+ before() {
2440
+ const { kinds = ["let", "var"], allowNamePattern } = optionsFirst(context, {});
2441
+ kindSet = new Set(kinds);
2442
+ allowed = allowNamePattern ? new RegExp(allowNamePattern) : null;
1568
2443
  },
1569
- TSTypeReference(node) {
1570
- const {
1571
- namespaces = ["z"],
1572
- operators = ["infer", "input"]
2444
+ VariableDeclaration(node) {
2445
+ if (node.declare || !kindSet.has(node.kind)) {
2446
+ return;
2447
+ }
2448
+ if (node.kind === "let" && !moduleMutableStateIsModuleLet(node) || node.kind === "var" && moduleMutableStateInsideFunction(node) || moduleMutableStateInsideAmbient(node)) {
2449
+ return;
2450
+ }
2451
+ for (const declarator of node.declarations) {
2452
+ for (const binding of moduleMutableStateBindings(declarator.id)) {
2453
+ if (!allowed?.test(binding.name)) {
2454
+ context.report({
2455
+ node: binding.node,
2456
+ messageId: "moduleState"
2457
+ });
2458
+ }
2459
+ }
2460
+ }
2461
+ }
2462
+ };
2463
+ }
2464
+ });
2465
+
2466
+ // src/rules/no-promise-all-mutation.ts
2467
+ import { defineRule as defineRule22 } from "@oxlint/plugins";
2468
+ function mutationBinding(node) {
2469
+ if (node.type === "Identifier") {
2470
+ return node;
2471
+ }
2472
+ if (node.type === "MemberExpression") {
2473
+ return mutationBinding(node.object);
2474
+ }
2475
+ if (node.type === "ChainExpression" || node.type === "ParenthesizedExpression" || node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "TSTypeAssertion" || node.type === "TSNonNullExpression") {
2476
+ return mutationBinding(node.expression);
2477
+ }
2478
+ }
2479
+ function mutationBindings(node) {
2480
+ switch (node.type) {
2481
+ case "ArrayPattern":
2482
+ return node.elements.flatMap((element) => element ? mutationBindings(element) : []);
2483
+ case "ObjectPattern":
2484
+ return node.properties.flatMap((property) => property.type === "RestElement" ? mutationBindings(property) : mutationBindings(property.value));
2485
+ case "AssignmentPattern":
2486
+ return mutationBindings(node.left);
2487
+ case "RestElement":
2488
+ return mutationBindings(node.argument);
2489
+ default: {
2490
+ const binding = mutationBinding(node);
2491
+ return binding ? [binding] : [];
2492
+ }
2493
+ }
2494
+ }
2495
+ var noPromiseAllMutation = defineRule22({
2496
+ meta: {
2497
+ type: "problem",
2498
+ schema: [{
2499
+ type: "object",
2500
+ additionalProperties: false,
2501
+ properties: {
2502
+ combinators: { type: "array", items: { type: "string" } },
2503
+ methods: { type: "array", items: { type: "string" } },
2504
+ checkAssignments: { type: "boolean" }
2505
+ }
2506
+ }],
2507
+ defaultOptions: [{
2508
+ combinators: ["Promise.all", "Promise.allSettled", "Promise.any", "Promise.race"],
2509
+ methods: ["push", "unshift", "splice", "set", "add"],
2510
+ checkAssignments: true
2511
+ }],
2512
+ messages: {
2513
+ mutation: "Do not mutate outer binding '{{name}}' inside {{combinator}}()."
2514
+ }
2515
+ },
2516
+ createOnce(context) {
2517
+ const references = new Map;
2518
+ const reported = new Set;
2519
+ let combinators = new Set;
2520
+ let methods = new Set;
2521
+ let checkAssignments = true;
2522
+ let referencesReady = false;
2523
+ function ensureReferences() {
2524
+ if (referencesReady) {
2525
+ return;
2526
+ }
2527
+ referencesReady = true;
2528
+ for (const scope of context.sourceCode.scopeManager.scopes) {
2529
+ for (const reference of scope.references) {
2530
+ references.set(reference.identifier.range[0], reference.resolved);
2531
+ }
2532
+ }
2533
+ }
2534
+ function isOuterBinding(identifier, call) {
2535
+ const variable = references.get(identifier.range[0]);
2536
+ return variable !== undefined && variable !== null && variable.identifiers.length > 0 && variable.identifiers.every((declaration) => declaration.range[0] < call.range[0] || declaration.range[1] > call.range[1]);
2537
+ }
2538
+ function reportMutation(node, binding, call, combinator) {
2539
+ if (!binding || reported.has(binding.range[0]) || !isOuterBinding(binding, call)) {
2540
+ return;
2541
+ }
2542
+ reported.add(binding.range[0]);
2543
+ context.report({
2544
+ node,
2545
+ messageId: "mutation",
2546
+ data: { name: binding.name, combinator }
2547
+ });
2548
+ }
2549
+ return {
2550
+ before() {
2551
+ const options = optionsFirst(context);
2552
+ combinators = new Set(options.combinators);
2553
+ methods = new Set(options.methods);
2554
+ checkAssignments = options.checkAssignments;
2555
+ references.clear();
2556
+ reported.clear();
2557
+ referencesReady = false;
2558
+ },
2559
+ CallExpression(node) {
2560
+ const combinator = astDottedName(node.callee);
2561
+ if (!combinator || !combinators.has(combinator)) {
2562
+ return;
2563
+ }
2564
+ ensureReferences();
2565
+ for (const argument of node.arguments) {
2566
+ astVisit(argument, [node], (candidate) => {
2567
+ if (candidate.type === "CallExpression" && candidate.callee.type === "MemberExpression") {
2568
+ const method = astStaticMemberName(candidate.callee);
2569
+ if (method && methods.has(method)) {
2570
+ reportMutation(candidate, mutationBinding(candidate.callee.object), node, combinator);
2571
+ }
2572
+ return;
2573
+ }
2574
+ if (checkAssignments && candidate.type === "AssignmentExpression") {
2575
+ for (const binding of mutationBindings(candidate.left)) {
2576
+ reportMutation(candidate, binding, node, combinator);
2577
+ }
2578
+ return;
2579
+ }
2580
+ if (checkAssignments && candidate.type === "UpdateExpression") {
2581
+ reportMutation(candidate, mutationBinding(candidate.argument), node, combinator);
2582
+ }
2583
+ });
2584
+ }
2585
+ }
2586
+ };
2587
+ }
2588
+ });
2589
+
2590
+ // src/rules/no-rederive-schema.ts
2591
+ import { defineRule as defineRule23 } from "@oxlint/plugins";
2592
+ var noRederiveSchema = defineRule23({
2593
+ meta: {
2594
+ type: "problem",
2595
+ schema: [{
2596
+ type: "object",
2597
+ additionalProperties: false,
2598
+ properties: {
2599
+ from: { type: "array", items: { type: "string" } },
2600
+ namespaces: { type: "array", items: { type: "string" } },
2601
+ operators: { type: "array", items: { type: "string" } }
2602
+ },
2603
+ required: ["from"]
2604
+ }],
2605
+ messages: {
2606
+ rederive: "Do not derive {{operator}} from imported schema {{schema}}; import its DTO type."
2607
+ }
2608
+ },
2609
+ createOnce(context) {
2610
+ let imported = new Set;
2611
+ return {
2612
+ Program(program) {
2613
+ const { from } = optionsFirst(context);
2614
+ imported = new Set(program.body.flatMap((statement) => statement.type === "ImportDeclaration" && from.includes(statement.source.value) ? statement.specifiers.map((specifier) => specifier.local.name) : []));
2615
+ },
2616
+ TSTypeReference(node) {
2617
+ const {
2618
+ namespaces = ["z"],
2619
+ operators = ["infer", "input"]
1573
2620
  } = optionsFirst(context);
1574
2621
  const operator = schemasTypeOperator(node, new Set(namespaces), new Set(operators));
1575
2622
  const parameter = node.typeArguments?.params[0];
@@ -1586,9 +2633,98 @@ var noRederiveSchema = defineRule15({
1586
2633
  }
1587
2634
  });
1588
2635
 
2636
+ // src/rules/no-restricted-constructor.ts
2637
+ import { defineRule as defineRule24 } from "@oxlint/plugins";
2638
+ function restrictedConstructorEntries(constructors) {
2639
+ const entries = new Map;
2640
+ for (const item of constructors) {
2641
+ if (typeof item === "string") {
2642
+ if (!entries.has(item)) {
2643
+ entries.set(item, undefined);
2644
+ }
2645
+ continue;
2646
+ }
2647
+ if (!entries.has(item.name)) {
2648
+ entries.set(item.name, item.message);
2649
+ }
2650
+ }
2651
+ return entries;
2652
+ }
2653
+ var noRestrictedConstructor = defineRule24({
2654
+ meta: {
2655
+ type: "problem",
2656
+ schema: [{
2657
+ type: "object",
2658
+ additionalProperties: false,
2659
+ properties: {
2660
+ constructors: {
2661
+ type: "array",
2662
+ items: {
2663
+ anyOf: [
2664
+ { type: "string" },
2665
+ {
2666
+ type: "object",
2667
+ additionalProperties: false,
2668
+ properties: {
2669
+ name: { type: "string" },
2670
+ message: { type: "string" }
2671
+ },
2672
+ required: ["name"]
2673
+ }
2674
+ ]
2675
+ }
2676
+ },
2677
+ checkCalls: { type: "boolean", default: false },
2678
+ message: { type: "string" }
2679
+ },
2680
+ required: ["constructors"]
2681
+ }],
2682
+ messages: {
2683
+ construct: "Do not construct '{{name}}'.{{suffix}}",
2684
+ call: "Do not call '{{name}}'.{{suffix}}"
2685
+ }
2686
+ },
2687
+ createOnce(context) {
2688
+ let entries = new Map;
2689
+ let checkCalls = false;
2690
+ let fallbackMessage;
2691
+ function report(node, messageId) {
2692
+ const name = astDottedName(node.callee);
2693
+ if (!name || !entries.has(name)) {
2694
+ return;
2695
+ }
2696
+ const extra = entries.get(name) ?? fallbackMessage;
2697
+ context.report({
2698
+ node,
2699
+ messageId,
2700
+ data: { name, suffix: extra ? ` ${extra}` : "" }
2701
+ });
2702
+ }
2703
+ return {
2704
+ before() {
2705
+ const options = optionsFirst(context);
2706
+ entries = restrictedConstructorEntries(options.constructors);
2707
+ checkCalls = options.checkCalls === true;
2708
+ fallbackMessage = options.message;
2709
+ if (entries.size === 0) {
2710
+ return false;
2711
+ }
2712
+ },
2713
+ NewExpression(node) {
2714
+ report(node, "construct");
2715
+ },
2716
+ CallExpression(node) {
2717
+ if (checkCalls) {
2718
+ report(node, "call");
2719
+ }
2720
+ }
2721
+ };
2722
+ }
2723
+ });
2724
+
1589
2725
  // src/rules/no-restricted-files.ts
1590
- import { defineRule as defineRule16 } from "@oxlint/plugins";
1591
- var noRestrictedFiles = defineRule16({
2726
+ import { defineRule as defineRule25 } from "@oxlint/plugins";
2727
+ var noRestrictedFiles = defineRule25({
1592
2728
  meta: {
1593
2729
  type: "problem",
1594
2730
  schema: [{
@@ -1618,7 +2754,7 @@ var noRestrictedFiles = defineRule16({
1618
2754
  });
1619
2755
 
1620
2756
  // src/rules/no-restricted-token.ts
1621
- import { defineRule as defineRule17 } from "@oxlint/plugins";
2757
+ import { defineRule as defineRule26 } from "@oxlint/plugins";
1622
2758
  function tokenIsTypeOnlyImport(node) {
1623
2759
  const parent = node.parent;
1624
2760
  if (parent?.type === "ImportSpecifier") {
@@ -1626,7 +2762,10 @@ function tokenIsTypeOnlyImport(node) {
1626
2762
  }
1627
2763
  return (parent?.type === "ImportDefaultSpecifier" || parent?.type === "ImportNamespaceSpecifier") && parent.parent.type === "ImportDeclaration" && parent.parent.importKind === "type";
1628
2764
  }
1629
- var noRestrictedToken = defineRule17({
2765
+ function memberMatches(node, restriction) {
2766
+ return restriction.startsWith("*.") ? astStaticMemberName(node) === restriction.slice(2) : astDottedName(node) === restriction;
2767
+ }
2768
+ var noRestrictedToken = defineRule26({
1630
2769
  meta: {
1631
2770
  type: "problem",
1632
2771
  schema: [{
@@ -1635,32 +2774,86 @@ var noRestrictedToken = defineRule17({
1635
2774
  properties: {
1636
2775
  token: { type: "string" },
1637
2776
  allowIn: { type: "array", items: { type: "string" } },
1638
- allowPathPatterns: { type: "array", items: { type: "string" } }
2777
+ allowPathPatterns: { type: "array", items: { type: "string" } },
2778
+ restrictions: {
2779
+ type: "array",
2780
+ items: {
2781
+ type: "object",
2782
+ additionalProperties: false,
2783
+ properties: {
2784
+ token: { type: "string" },
2785
+ member: { type: "string" },
2786
+ allowIn: { type: "array", items: { type: "string" } },
2787
+ allowPathPatterns: { type: "array", items: { type: "string" } },
2788
+ message: { type: "string" }
2789
+ },
2790
+ oneOf: [
2791
+ { required: ["token"], not: { required: ["member"] } },
2792
+ { required: ["member"], not: { required: ["token"] } }
2793
+ ]
2794
+ }
2795
+ }
1639
2796
  },
1640
- required: ["token", "allowIn"]
2797
+ anyOf: [
2798
+ { required: ["token"] },
2799
+ { required: ["restrictions"] }
2800
+ ]
1641
2801
  }],
1642
2802
  messages: {
1643
- restricted: "'{{token}}' may only appear in configured owner paths."
2803
+ restricted: "'{{token}}' may only appear in configured owner paths.",
2804
+ restrictedWithMessage: "'{{token}}' may only appear in configured owner paths. {{custom}}"
1644
2805
  }
1645
2806
  },
1646
2807
  createOnce(context) {
2808
+ let restrictions = [];
2809
+ function report(node, restriction) {
2810
+ context.report({
2811
+ node,
2812
+ messageId: restriction.message === undefined ? "restricted" : "restrictedWithMessage",
2813
+ data: { token: restriction.value, custom: restriction.message ?? "" }
2814
+ });
2815
+ }
1647
2816
  return {
1648
2817
  before() {
1649
2818
  const options = optionsFirst(context);
2819
+ const configured = [
2820
+ ...options.token ? [{
2821
+ token: options.token,
2822
+ allowIn: options.allowIn,
2823
+ allowPathPatterns: options.allowPathPatterns
2824
+ }] : [],
2825
+ ...options.restrictions ?? []
2826
+ ];
1650
2827
  const filename = namingPosixPath(context.filename);
1651
- const allowed = options.allowIn.some((suffix) => filename.endsWith(suffix)) || (options.allowPathPatterns ?? []).some((pattern) => new RegExp(pattern).test(filename));
1652
- if (allowed) {
2828
+ restrictions = configured.flatMap((restriction) => {
2829
+ const allowed = (restriction.allowIn ?? []).some((suffix) => filename.endsWith(suffix)) || (restriction.allowPathPatterns ?? []).some((pattern) => new RegExp(pattern).test(filename));
2830
+ if (allowed) {
2831
+ return [];
2832
+ }
2833
+ if (restriction.token) {
2834
+ return [{ kind: "token", value: restriction.token, message: restriction.message }];
2835
+ }
2836
+ return restriction.member ? [{ kind: "member", value: restriction.member, message: restriction.message }] : [];
2837
+ });
2838
+ if (restrictions.length === 0) {
1653
2839
  return false;
1654
2840
  }
1655
2841
  },
1656
2842
  Identifier(node) {
1657
- const { token } = optionsFirst(context);
1658
- if (node.name === token && !tokenIsTypeOnlyImport(node)) {
1659
- context.report({
1660
- node,
1661
- messageId: "restricted",
1662
- data: { token }
1663
- });
2843
+ if (tokenIsTypeOnlyImport(node)) {
2844
+ return;
2845
+ }
2846
+ for (const restriction of restrictions) {
2847
+ if (restriction.kind === "token" && node.name === restriction.value) {
2848
+ report(node, restriction);
2849
+ }
2850
+ }
2851
+ },
2852
+ MemberExpression(node) {
2853
+ for (const restriction of restrictions) {
2854
+ if (restriction.kind === "member" && memberMatches(node, restriction.value)) {
2855
+ report(node, restriction);
2856
+ }
1664
2857
  }
1665
2858
  }
1666
2859
  };
@@ -1668,8 +2861,224 @@ var noRestrictedToken = defineRule17({
1668
2861
  });
1669
2862
 
1670
2863
  // src/rules/no-runtime-in-types.ts
1671
- import { defineRule as defineRule18 } from "@oxlint/plugins";
1672
- var noRuntimeInTypes = defineRule18({
2864
+ import { defineRule as defineRule27 } from "@oxlint/plugins";
2865
+ function isTypeOnlyImport(statement) {
2866
+ return statement.importKind === "type" || statement.specifiers.length > 0 && statement.specifiers.every((specifier) => specifier.type === "ImportSpecifier" && specifier.importKind === "type");
2867
+ }
2868
+ function exportLocalName(specifier) {
2869
+ return specifier.local.type === "Identifier" ? specifier.local.name : specifier.local.value;
2870
+ }
2871
+ function importEqualsSource(declaration) {
2872
+ return declaration.moduleReference.type === "TSExternalModuleReference" ? declaration.moduleReference.expression.value : undefined;
2873
+ }
2874
+ function importEqualsDeclaration(statement) {
2875
+ if (statement.type === "TSImportEqualsDeclaration") {
2876
+ return statement;
2877
+ }
2878
+ return statement.type === "ExportNamedDeclaration" && statement.declaration?.type === "TSImportEqualsDeclaration" ? statement.declaration : undefined;
2879
+ }
2880
+ function isAllowedSource(source, allowedImportSources) {
2881
+ return allowedImportSources.some((pattern) => {
2882
+ pattern.lastIndex = 0;
2883
+ return pattern.test(source);
2884
+ });
2885
+ }
2886
+ function runtimeInTypesIsAmbientFile(filename) {
2887
+ return filename.endsWith(".d.ts") || filename.endsWith(".d.mts") || filename.endsWith(".d.cts");
2888
+ }
2889
+ function runtimeInTypesUnwrapped(statement) {
2890
+ return statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" ? statement.declaration ?? undefined : statement;
2891
+ }
2892
+ function runtimeInTypesEntityRootName(node) {
2893
+ let current = node;
2894
+ while (current.type === "TSQualifiedName") {
2895
+ current = current.left;
2896
+ }
2897
+ return current.type === "Identifier" ? current.name : undefined;
2898
+ }
2899
+ function importEqualsRootName(declaration) {
2900
+ return declaration.moduleReference.type === "TSExternalModuleReference" ? undefined : runtimeInTypesEntityRootName(declaration.moduleReference);
2901
+ }
2902
+ function runtimeInTypesModuleBindingName(node) {
2903
+ return node.type === "TSModuleDeclaration" ? runtimeInTypesEntityRootName(node.id) : undefined;
2904
+ }
2905
+ function runtimeInTypesIsInternalValueAlias(declaration, valueNames) {
2906
+ if (declaration.importKind === "type" || importEqualsSource(declaration) !== undefined) {
2907
+ return false;
2908
+ }
2909
+ const root = importEqualsRootName(declaration);
2910
+ return root !== undefined && valueNames.has(root);
2911
+ }
2912
+ function runtimeInTypesModuleName(node) {
2913
+ if (node.type !== "TSModuleDeclaration") {
2914
+ return "namespace";
2915
+ }
2916
+ return node.id.type === "Identifier" ? node.id.name : node.id.type === "Literal" ? String(node.id.value) : "namespace";
2917
+ }
2918
+ function runtimeInTypesNamespaceIsRuntime(node, isAmbientFile, valueNames) {
2919
+ if (isAmbientFile || node.type !== "TSModuleDeclaration" || node.declare || node.global || node.id.type === "Literal" || !node.body) {
2920
+ return false;
2921
+ }
2922
+ return node.body.body.some((statement) => runtimeInTypesNamespaceMemberIsRuntime(statement, isAmbientFile, valueNames));
2923
+ }
2924
+ function runtimeInTypesNamespaceMemberIsRuntime(node, isAmbientFile, valueNames) {
2925
+ if (node.type === "ExportNamedDeclaration") {
2926
+ if (node.exportKind === "type") {
2927
+ return false;
2928
+ }
2929
+ return node.declaration ? runtimeInTypesNamespaceMemberIsRuntime(node.declaration, isAmbientFile, valueNames) : node.specifiers.some((specifier) => specifier.exportKind !== "type");
2930
+ }
2931
+ if (node.type === "TSModuleDeclaration") {
2932
+ return runtimeInTypesNamespaceIsRuntime(node, isAmbientFile, valueNames);
2933
+ }
2934
+ if (node.type === "TSImportEqualsDeclaration") {
2935
+ return node.importKind !== "type" && (node.moduleReference.type === "TSExternalModuleReference" || runtimeInTypesIsInternalValueAlias(node, valueNames));
2936
+ }
2937
+ if (node.type === "TSExportAssignment") {
2938
+ return true;
2939
+ }
2940
+ return declarationsIsRuntime(node) && !node.declare;
2941
+ }
2942
+ function runtimeInTypesMemberHasValue(node) {
2943
+ if (node.type === "ExportNamedDeclaration") {
2944
+ if (node.exportKind === "type") {
2945
+ return false;
2946
+ }
2947
+ return node.declaration ? runtimeInTypesMemberHasValue(node.declaration) : node.specifiers.some((specifier) => specifier.exportKind !== "type");
2948
+ }
2949
+ if (node.type === "TSModuleDeclaration") {
2950
+ return runtimeInTypesNamespaceHasValueMembers(node);
2951
+ }
2952
+ if (node.type === "TSImportEqualsDeclaration") {
2953
+ return node.importKind !== "type" && node.moduleReference.type === "TSExternalModuleReference";
2954
+ }
2955
+ if (node.type === "TSExportAssignment") {
2956
+ return true;
2957
+ }
2958
+ return declarationsIsRuntime(node);
2959
+ }
2960
+ function runtimeInTypesNamespaceHasValueMembers(node) {
2961
+ if (node.type !== "TSModuleDeclaration" || node.global || node.id.type === "Literal" || !node.body) {
2962
+ return false;
2963
+ }
2964
+ return node.body.body.some((statement) => runtimeInTypesMemberHasValue(statement));
2965
+ }
2966
+ function runtimeInTypesMemberReferencesValue(node, valueNames) {
2967
+ if (node.type === "ExportNamedDeclaration") {
2968
+ return node.declaration ? runtimeInTypesMemberReferencesValue(node.declaration, valueNames) : false;
2969
+ }
2970
+ if (node.type === "TSModuleDeclaration") {
2971
+ return runtimeInTypesNamespaceHasValueMembers(node) || runtimeInTypesNamespaceReferencesValue(node, valueNames);
2972
+ }
2973
+ if (node.type === "TSImportEqualsDeclaration") {
2974
+ return runtimeInTypesIsInternalValueAlias(node, valueNames);
2975
+ }
2976
+ return false;
2977
+ }
2978
+ function runtimeInTypesNamespaceReferencesValue(node, valueNames) {
2979
+ if (node.type !== "TSModuleDeclaration" || !node.body) {
2980
+ return false;
2981
+ }
2982
+ return node.body.body.some((statement) => runtimeInTypesMemberReferencesValue(statement, valueNames));
2983
+ }
2984
+ function runtimeInTypesSeedValueName(declaration, valueNames) {
2985
+ if (declaration.type === "VariableDeclaration") {
2986
+ for (const item of declaration.declarations) {
2987
+ if (item.id.type === "Identifier") {
2988
+ valueNames.add(item.id.name);
2989
+ }
2990
+ }
2991
+ return;
2992
+ }
2993
+ if ((declaration.type === "FunctionDeclaration" || declaration.type === "ClassDeclaration" || declaration.type === "TSEnumDeclaration") && declaration.id) {
2994
+ valueNames.add(declaration.id.name);
2995
+ return;
2996
+ }
2997
+ if (declaration.type === "TSModuleDeclaration") {
2998
+ const name = runtimeInTypesModuleBindingName(declaration);
2999
+ if (name && runtimeInTypesNamespaceHasValueMembers(declaration)) {
3000
+ valueNames.add(name);
3001
+ }
3002
+ }
3003
+ }
3004
+ function runtimeInTypesValueNames(statements) {
3005
+ const valueNames = new Set;
3006
+ for (const statement of statements) {
3007
+ if (statement.type === "ImportDeclaration") {
3008
+ if (isTypeOnlyImport(statement)) {
3009
+ continue;
3010
+ }
3011
+ for (const specifier of statement.specifiers) {
3012
+ if (specifier.type !== "ImportSpecifier" || specifier.importKind !== "type") {
3013
+ valueNames.add(specifier.local.name);
3014
+ }
3015
+ }
3016
+ continue;
3017
+ }
3018
+ const importEquals = importEqualsDeclaration(statement);
3019
+ if (importEquals) {
3020
+ if (importEquals.importKind !== "type" && importEqualsSource(importEquals) !== undefined) {
3021
+ valueNames.add(importEquals.id.name);
3022
+ }
3023
+ continue;
3024
+ }
3025
+ const declaration = runtimeInTypesUnwrapped(statement);
3026
+ if (declaration) {
3027
+ runtimeInTypesSeedValueName(declaration, valueNames);
3028
+ }
3029
+ }
3030
+ let changed = true;
3031
+ while (changed) {
3032
+ changed = false;
3033
+ for (const statement of statements) {
3034
+ const importEquals = importEqualsDeclaration(statement);
3035
+ if (importEquals && runtimeInTypesIsInternalValueAlias(importEquals, valueNames)) {
3036
+ if (!valueNames.has(importEquals.id.name)) {
3037
+ valueNames.add(importEquals.id.name);
3038
+ changed = true;
3039
+ }
3040
+ continue;
3041
+ }
3042
+ const declaration = runtimeInTypesUnwrapped(statement);
3043
+ if (declaration?.type !== "TSModuleDeclaration") {
3044
+ continue;
3045
+ }
3046
+ const name = runtimeInTypesModuleBindingName(declaration);
3047
+ if (name && !valueNames.has(name) && runtimeInTypesNamespaceReferencesValue(declaration, valueNames)) {
3048
+ valueNames.add(name);
3049
+ changed = true;
3050
+ }
3051
+ }
3052
+ }
3053
+ return valueNames;
3054
+ }
3055
+ function runtimeInTypesExportAssignmentExpression(node) {
3056
+ let expression = node.expression;
3057
+ while (expression.type === "ParenthesizedExpression") {
3058
+ expression = expression.expression;
3059
+ }
3060
+ return expression;
3061
+ }
3062
+ function isRuntimeReExport(statement, runtimeImportLocals, valueNames, isAmbientFile) {
3063
+ if (statement.exportKind === "type") {
3064
+ return false;
3065
+ }
3066
+ if (statement.type === "ExportAllDeclaration") {
3067
+ return true;
3068
+ }
3069
+ if (statement.declaration?.type === "TSImportEqualsDeclaration") {
3070
+ const declaration = statement.declaration;
3071
+ if (declaration.importKind === "type" || isAmbientFile) {
3072
+ return false;
3073
+ }
3074
+ return importEqualsSource(declaration) !== undefined || runtimeInTypesIsInternalValueAlias(declaration, valueNames);
3075
+ }
3076
+ if (statement.source) {
3077
+ return statement.specifiers.length === 0 || statement.specifiers.some((specifier) => specifier.exportKind !== "type");
3078
+ }
3079
+ return statement.specifiers.some((specifier) => specifier.exportKind !== "type" && runtimeImportLocals.has(exportLocalName(specifier)));
3080
+ }
3081
+ var noRuntimeInTypes = defineRule27({
1673
3082
  meta: {
1674
3083
  type: "problem",
1675
3084
  schema: [{
@@ -1679,18 +3088,80 @@ var noRuntimeInTypes = defineRule18({
1679
3088
  allow: {
1680
3089
  type: "array",
1681
3090
  items: { type: "string", enum: ["class", "default", "enum", "function", "variable"] }
3091
+ },
3092
+ runtimeImports: { type: "string", enum: ["allow", "ban"] },
3093
+ allowImportSources: { type: "array", items: { type: "string" } },
3094
+ banReExports: { type: "boolean" }
3095
+ }
3096
+ }],
3097
+ messages: {
3098
+ runtimeValue: "Types files must not contain runtime value {{name}}.",
3099
+ runtimeImport: "Types files must not contain runtime imports from {{source}}.",
3100
+ runtimeReExport: "Types files must not contain runtime re-exports."
3101
+ }
3102
+ },
3103
+ createOnce(context) {
3104
+ let allow = [];
3105
+ let runtimeImports = "allow";
3106
+ let allowedImportSources = [];
3107
+ let banReExports = false;
3108
+ let isAmbientFile = false;
3109
+ return {
3110
+ before() {
3111
+ const options = optionsFirst(context, {});
3112
+ allow = options.allow ?? [];
3113
+ runtimeImports = options.runtimeImports ?? "allow";
3114
+ allowedImportSources = (options.allowImportSources ?? []).map((pattern) => new RegExp(pattern));
3115
+ banReExports = options.banReExports === true;
3116
+ isAmbientFile = runtimeInTypesIsAmbientFile(context.filename);
3117
+ },
3118
+ Program(program) {
3119
+ const runtimeImportLocals = new Set;
3120
+ const valueNames = runtimeInTypesValueNames(program.body);
3121
+ for (const statement of program.body) {
3122
+ if (statement.type === "ImportDeclaration") {
3123
+ if (isTypeOnlyImport(statement)) {
3124
+ continue;
3125
+ }
3126
+ for (const specifier of statement.specifiers) {
3127
+ if (specifier.type !== "ImportSpecifier" || specifier.importKind !== "type") {
3128
+ runtimeImportLocals.add(specifier.local.name);
3129
+ }
3130
+ }
3131
+ const source = String(statement.source.value);
3132
+ if (runtimeImports === "ban" && !isAllowedSource(source, allowedImportSources)) {
3133
+ context.report({
3134
+ node: statement,
3135
+ messageId: "runtimeImport",
3136
+ data: { source }
3137
+ });
3138
+ }
3139
+ continue;
3140
+ }
3141
+ const declaration = importEqualsDeclaration(statement);
3142
+ if (!declaration || declaration.importKind === "type") {
3143
+ continue;
3144
+ }
3145
+ const source = importEqualsSource(declaration);
3146
+ if (source === undefined) {
3147
+ continue;
3148
+ }
3149
+ runtimeImportLocals.add(declaration.id.name);
3150
+ if (runtimeImports === "ban" && !isAllowedSource(source, allowedImportSources)) {
3151
+ context.report({
3152
+ node: declaration,
3153
+ messageId: "runtimeImport",
3154
+ data: { source }
3155
+ });
3156
+ }
1682
3157
  }
1683
- }
1684
- }],
1685
- messages: {
1686
- runtimeValue: "Types files must not contain runtime value {{name}}."
1687
- }
1688
- },
1689
- createOnce(context) {
1690
- return {
1691
- Program(program) {
1692
- const { allow = [] } = optionsFirst(context, {});
1693
3158
  for (const statement of program.body) {
3159
+ if (banReExports && (statement.type === "ExportNamedDeclaration" || statement.type === "ExportAllDeclaration") && isRuntimeReExport(statement, runtimeImportLocals, valueNames, isAmbientFile)) {
3160
+ context.report({
3161
+ node: statement,
3162
+ messageId: "runtimeReExport"
3163
+ });
3164
+ }
1694
3165
  const declaration = statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" ? statement.declaration : statement;
1695
3166
  if (!declaration) {
1696
3167
  continue;
@@ -1705,6 +3176,36 @@ var noRuntimeInTypes = defineRule18({
1705
3176
  }
1706
3177
  continue;
1707
3178
  }
3179
+ if (declaration.type === "TSModuleDeclaration") {
3180
+ if (runtimeInTypesNamespaceIsRuntime(declaration, isAmbientFile, valueNames)) {
3181
+ context.report({
3182
+ node: declaration,
3183
+ messageId: "runtimeValue",
3184
+ data: { name: runtimeInTypesModuleName(declaration) }
3185
+ });
3186
+ }
3187
+ continue;
3188
+ }
3189
+ if (statement.type === "TSExportAssignment" && !allow.includes("default")) {
3190
+ const expression = runtimeInTypesExportAssignmentExpression(statement);
3191
+ if (expression.type !== "Identifier" || valueNames.has(expression.name)) {
3192
+ context.report({
3193
+ node: statement,
3194
+ messageId: "runtimeValue",
3195
+ data: { name: expression.type === "Identifier" ? expression.name : "default" }
3196
+ });
3197
+ }
3198
+ continue;
3199
+ }
3200
+ const importEquals = importEqualsDeclaration(statement);
3201
+ if (!isAmbientFile && importEquals && runtimeInTypesIsInternalValueAlias(importEquals, valueNames)) {
3202
+ context.report({
3203
+ node: importEquals,
3204
+ messageId: "runtimeValue",
3205
+ data: { name: importEquals.id.name }
3206
+ });
3207
+ continue;
3208
+ }
1708
3209
  if (!allow.includes("default") && statement.type === "ExportDefaultDeclaration" && declaration.type !== "TSInterfaceDeclaration" && declaration.type !== "TSTypeAliasDeclaration") {
1709
3210
  context.report({
1710
3211
  node: statement,
@@ -1719,7 +3220,7 @@ var noRuntimeInTypes = defineRule18({
1719
3220
  });
1720
3221
 
1721
3222
  // src/rules/no-single-use-scalar-schema.ts
1722
- import { defineRule as defineRule19 } from "@oxlint/plugins";
3223
+ import { defineRule as defineRule28 } from "@oxlint/plugins";
1723
3224
  function isScalarSchema(node, namespaces, structuralMethods) {
1724
3225
  if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") {
1725
3226
  return false;
@@ -1753,7 +3254,7 @@ function isAllowedStructuralElement(identifier, initializer, ancestors, options,
1753
3254
  const method = call.callee.type === "MemberExpression" ? astStaticMemberName(call.callee) : undefined;
1754
3255
  return Boolean(method && options.methods.includes(method) && schemasElements(call).includes(identifier));
1755
3256
  }
1756
- var noSingleUseScalarSchema = defineRule19({
3257
+ var noSingleUseScalarSchema = defineRule28({
1757
3258
  meta: {
1758
3259
  type: "suggestion",
1759
3260
  schema: [{
@@ -1821,8 +3322,8 @@ var noSingleUseScalarSchema = defineRule19({
1821
3322
  });
1822
3323
 
1823
3324
  // src/rules/no-top-level-functions.ts
1824
- import { defineRule as defineRule20 } from "@oxlint/plugins";
1825
- var noTopLevelFunctions = defineRule20({
3325
+ import { defineRule as defineRule29 } from "@oxlint/plugins";
3326
+ var noTopLevelFunctions = defineRule29({
1826
3327
  meta: {
1827
3328
  type: "problem",
1828
3329
  schema: [{
@@ -1879,15 +3380,43 @@ var noTopLevelFunctions = defineRule20({
1879
3380
  });
1880
3381
 
1881
3382
  // src/rules/no-trivial-functions.ts
1882
- import { defineRule as defineRule21 } from "@oxlint/plugins";
1883
- var noTrivialFunctions = defineRule21({
3383
+ import { defineRule as defineRule30 } from "@oxlint/plugins";
3384
+ function forwardedExpression(node) {
3385
+ if (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") {
3386
+ return node.body;
3387
+ }
3388
+ if (!node.body || node.body.type !== "BlockStatement") {
3389
+ return null;
3390
+ }
3391
+ const statements = node.body.body.filter((statement) => statement.type !== "EmptyStatement" && !(statement.type === "ExpressionStatement" && statement.directive));
3392
+ if (statements.length !== 1) {
3393
+ return null;
3394
+ }
3395
+ const statement = statements[0];
3396
+ if (statement.type === "ReturnStatement") {
3397
+ return statement.argument;
3398
+ }
3399
+ return statement.type === "ExpressionStatement" ? statement.expression : null;
3400
+ }
3401
+ function forwardedCall(expression) {
3402
+ if (expression.type === "ParenthesizedExpression" || expression.type === "TSAsExpression" || expression.type === "TSSatisfiesExpression" || expression.type === "TSTypeAssertion" || expression.type === "TSNonNullExpression" || expression.type === "ChainExpression") {
3403
+ return forwardedCall(expression.expression);
3404
+ }
3405
+ if (expression.type === "AwaitExpression") {
3406
+ return forwardedCall(expression.argument);
3407
+ }
3408
+ return expression.type === "CallExpression" ? expression : null;
3409
+ }
3410
+ var noTrivialFunctions = defineRule30({
1884
3411
  meta: {
1885
3412
  type: "problem",
1886
3413
  schema: [{
1887
3414
  type: "object",
1888
3415
  additionalProperties: false,
1889
3416
  properties: {
1890
- allowPattern: { type: "string" }
3417
+ allowPattern: { type: "string" },
3418
+ allowCallees: { type: "array", items: { type: "string" } },
3419
+ allowAsync: { type: "boolean" }
1891
3420
  }
1892
3421
  }],
1893
3422
  messages: {
@@ -1895,16 +3424,29 @@ var noTrivialFunctions = defineRule21({
1895
3424
  }
1896
3425
  },
1897
3426
  createOnce(context) {
3427
+ let allowed = null;
3428
+ let allowedCallees = [];
3429
+ let allowAsync = false;
1898
3430
  return {
3431
+ before() {
3432
+ const { allowPattern, allowCallees: calleePatterns = [], allowAsync: asyncAllowed = false } = optionsFirst(context, {});
3433
+ allowed = allowPattern ? new RegExp(allowPattern) : null;
3434
+ allowedCallees = calleePatterns.map((pattern) => new RegExp(pattern));
3435
+ allowAsync = asyncAllowed;
3436
+ },
1899
3437
  Program(program) {
1900
- const { allowPattern } = optionsFirst(context, {});
1901
- const allowed = allowPattern ? new RegExp(allowPattern) : null;
1902
3438
  const seen = new Set;
1903
3439
  for (const item of [...exportsCollectFunctions(program), ...declarationsTopLevelUnexportedFunctions(program)]) {
1904
3440
  const key = `${item.name}:${item.node.start}:${item.node.end}`;
1905
3441
  if (seen.has(key) || allowed?.test(item.name) || !declarationsIsTrivialFunction(item.node)) {
1906
3442
  continue;
1907
3443
  }
3444
+ const expression = forwardedExpression(item.node);
3445
+ const call = expression && forwardedCall(expression);
3446
+ const path = call && astDottedName(call.callee);
3447
+ if (allowAsync && item.node.async && call || path && allowedCallees.some((pattern) => pattern.test(path))) {
3448
+ continue;
3449
+ }
1908
3450
  seen.add(key);
1909
3451
  context.report({
1910
3452
  node: item.node,
@@ -1918,8 +3460,8 @@ var noTrivialFunctions = defineRule21({
1918
3460
  });
1919
3461
 
1920
3462
  // src/rules/no-type-declarations.ts
1921
- import { defineRule as defineRule22 } from "@oxlint/plugins";
1922
- var noTypeDeclarations = defineRule22({
3463
+ import { defineRule as defineRule31 } from "@oxlint/plugins";
3464
+ var noTypeDeclarations = defineRule31({
1923
3465
  meta: {
1924
3466
  type: "problem",
1925
3467
  schema: [{
@@ -1934,28 +3476,31 @@ var noTypeDeclarations = defineRule22({
1934
3476
  }
1935
3477
  },
1936
3478
  createOnce(context) {
3479
+ let allowed = null;
3480
+ function check(node) {
3481
+ if (allowed?.test(node.id.name)) {
3482
+ return;
3483
+ }
3484
+ context.report({
3485
+ node,
3486
+ messageId: "typeDeclaration",
3487
+ data: { name: node.id.name }
3488
+ });
3489
+ }
1937
3490
  return {
1938
- Program(program) {
3491
+ before() {
1939
3492
  const { allowPattern } = optionsFirst(context, {});
1940
- const allowed = allowPattern ? new RegExp(allowPattern) : null;
1941
- for (const item of declarationsCollectNamed(program)) {
1942
- if (item.node.type !== "TSTypeAliasDeclaration" && item.node.type !== "TSInterfaceDeclaration" || allowed?.test(item.name)) {
1943
- continue;
1944
- }
1945
- context.report({
1946
- node: item.node,
1947
- messageId: "typeDeclaration",
1948
- data: { name: item.name }
1949
- });
1950
- }
1951
- }
3493
+ allowed = allowPattern ? new RegExp(allowPattern) : null;
3494
+ },
3495
+ TSTypeAliasDeclaration: check,
3496
+ TSInterfaceDeclaration: check
1952
3497
  };
1953
3498
  }
1954
3499
  });
1955
3500
 
1956
3501
  // src/rules/no-unescaped-like.ts
1957
- import { defineRule as defineRule23 } from "@oxlint/plugins";
1958
- var noUnescapedLike = defineRule23({
3502
+ import { defineRule as defineRule32 } from "@oxlint/plugins";
3503
+ var noUnescapedLike = defineRule32({
1959
3504
  meta: {
1960
3505
  type: "problem",
1961
3506
  schema: [{
@@ -2000,31 +3545,150 @@ var noUnescapedLike = defineRule23({
2000
3545
  }
2001
3546
  });
2002
3547
 
3548
+ // src/rules/object-multiline.ts
3549
+ import { defineRule as defineRule33 } from "@oxlint/plugins";
3550
+ function objectMultilineIsCallArg(node) {
3551
+ const parent = node.parent;
3552
+ return (parent.type === "CallExpression" || parent.type === "NewExpression") && parent.arguments.includes(node);
3553
+ }
3554
+ function objectMultilineIsWhitespace(text) {
3555
+ return /^[\t\n\r\f ]*$/.test(text);
3556
+ }
3557
+ function objectMultilineGaps(text, node) {
3558
+ const gaps = [];
3559
+ const close = node.range[1] - 1;
3560
+ const first = node.properties[0];
3561
+ if (!objectMultilineIsWhitespace(text.slice(node.range[0] + 1, first.range[0]))) {
3562
+ return null;
3563
+ }
3564
+ gaps.push([node.range[0] + 1, first.range[0]]);
3565
+ for (let index = 0;index < node.properties.length; index++) {
3566
+ const property = node.properties[index];
3567
+ const next = node.properties[index + 1];
3568
+ const after = property.range[1];
3569
+ const limit = next ? next.range[0] : close;
3570
+ const between = text.slice(after, limit);
3571
+ const comma = between.indexOf(",");
3572
+ if (comma === -1) {
3573
+ if (next || !objectMultilineIsWhitespace(between)) {
3574
+ return null;
3575
+ }
3576
+ gaps.push([after, close]);
3577
+ continue;
3578
+ }
3579
+ if (!objectMultilineIsWhitespace(between.slice(0, comma)) || !objectMultilineIsWhitespace(between.slice(comma + 1))) {
3580
+ return null;
3581
+ }
3582
+ gaps.push([after + comma + 1, limit]);
3583
+ }
3584
+ return gaps;
3585
+ }
3586
+ var objectMultiline = defineRule33({
3587
+ meta: {
3588
+ type: "layout",
3589
+ fixable: "whitespace",
3590
+ schema: [{
3591
+ type: "object",
3592
+ additionalProperties: false,
3593
+ properties: {
3594
+ minProperties: { type: "integer", minimum: 1, default: 3 },
3595
+ scope: { type: "string", enum: ["call-args", "all"], default: "call-args" },
3596
+ indent: { ...layoutIndentSchema, default: 2 }
3597
+ }
3598
+ }],
3599
+ messages: {
3600
+ callArgs: "Object with {{min}}+ properties in a call should have each property on its own line.",
3601
+ always: "Object with {{min}}+ properties should have each property on its own line."
3602
+ }
3603
+ },
3604
+ createOnce(context) {
3605
+ let minProperties = 3;
3606
+ let scope = "call-args";
3607
+ let indentUnit = layoutIndentUnit();
3608
+ let newline = `
3609
+ `;
3610
+ return {
3611
+ before() {
3612
+ const options = optionsFirst(context, {});
3613
+ minProperties = options.minProperties ?? 3;
3614
+ scope = options.scope ?? "call-args";
3615
+ indentUnit = layoutIndentUnit(options.indent);
3616
+ newline = layoutNewline(context.sourceCode.text);
3617
+ },
3618
+ ObjectExpression(node) {
3619
+ if (node.properties.length < minProperties || node.loc.start.line !== node.loc.end.line) {
3620
+ return;
3621
+ }
3622
+ if (scope === "call-args" && !objectMultilineIsCallArg(node)) {
3623
+ return;
3624
+ }
3625
+ const source = context.sourceCode;
3626
+ const gaps = source.getCommentsInside(node).length > 0 ? null : objectMultilineGaps(source.text, node);
3627
+ const baseIndent = layoutLineIndent(source.text, node.range[0]);
3628
+ const propIndent = `${baseIndent}${indentUnit}`;
3629
+ context.report({
3630
+ node,
3631
+ messageId: scope === "all" ? "always" : "callArgs",
3632
+ data: { min: String(minProperties) },
3633
+ fix(fixer) {
3634
+ if (!gaps) {
3635
+ return null;
3636
+ }
3637
+ return gaps.map((gap, index) => fixer.replaceTextRange(gap, `${newline}${index === gaps.length - 1 ? baseIndent : propIndent}`));
3638
+ }
3639
+ });
3640
+ }
3641
+ };
3642
+ }
3643
+ });
3644
+
2003
3645
  // src/rules/only-export-components.ts
2004
- import { defineRule as defineRule24 } from "@oxlint/plugins";
2005
- var onlyExportComponents = defineRule24({
3646
+ import { defineRule as defineRule34 } from "@oxlint/plugins";
3647
+ var onlyExportComponents = defineRule34({
2006
3648
  meta: {
2007
3649
  type: "problem",
2008
3650
  schema: [{
2009
3651
  type: "object",
2010
3652
  additionalProperties: false,
2011
3653
  properties: {
2012
- matchFileName: { type: "boolean" }
3654
+ matchFileName: { type: "boolean" },
3655
+ allowTypeExports: { type: "boolean" },
3656
+ denyTypePattern: { type: "string" }
2013
3657
  }
2014
3658
  }],
2015
3659
  messages: {
2016
3660
  nonComponent: "Export '{{name}}' must be a React component or type.",
2017
- nameMismatch: "Component export '{{name}}' must match '{{expected}}' or its prefix."
3661
+ nameMismatch: "Component export '{{name}}' must match '{{expected}}' or its prefix.",
3662
+ typeExport: "Type export '{{name}}' is not allowed.",
3663
+ deniedType: "Type export '{{name}}' is denied by denyTypePattern."
2018
3664
  }
2019
3665
  },
2020
3666
  createOnce(context) {
2021
3667
  return {
2022
3668
  Program(program) {
2023
- const { matchFileName = true } = optionsFirst(context, {});
3669
+ const {
3670
+ matchFileName = true,
3671
+ allowTypeExports = true,
3672
+ denyTypePattern
3673
+ } = optionsFirst(context, {});
2024
3674
  const expected = reactComponentsPascalFromBasename(namingFileBasename(context.filename));
3675
+ const denied = denyTypePattern ? new RegExp(denyTypePattern) : null;
2025
3676
  const components = [];
2026
3677
  for (const candidate of reactComponentsExportCandidates(program)) {
2027
3678
  if (candidate.typeOnly) {
3679
+ if (!allowTypeExports) {
3680
+ context.report({
3681
+ node: candidate.node,
3682
+ messageId: "typeExport",
3683
+ data: { name: candidate.name, expected }
3684
+ });
3685
+ } else if (denied?.test(candidate.name)) {
3686
+ context.report({
3687
+ node: candidate.node,
3688
+ messageId: "deniedType",
3689
+ data: { name: candidate.name, expected }
3690
+ });
3691
+ }
2028
3692
  continue;
2029
3693
  }
2030
3694
  if (!reactComponentsIsLike(program, candidate)) {
@@ -2058,7 +3722,7 @@ var onlyExportComponents = defineRule24({
2058
3722
  });
2059
3723
 
2060
3724
  // src/rules/only-export-constants.ts
2061
- import { defineRule as defineRule25 } from "@oxlint/plugins";
3725
+ import { defineRule as defineRule35 } from "@oxlint/plugins";
2062
3726
  function bindingNames(pattern) {
2063
3727
  switch (pattern.type) {
2064
3728
  case "Identifier":
@@ -2113,7 +3777,7 @@ function collectBindings(program) {
2113
3777
  }
2114
3778
  return bindings;
2115
3779
  }
2116
- var onlyExportConstants = defineRule25({
3780
+ var onlyExportConstants = defineRule35({
2117
3781
  meta: {
2118
3782
  type: "problem",
2119
3783
  schema: [{
@@ -2194,9 +3858,128 @@ var onlyExportConstants = defineRule25({
2194
3858
  }
2195
3859
  });
2196
3860
 
3861
+ // src/rules/padding-between-statements.ts
3862
+ import { defineRule as defineRule36 } from "@oxlint/plugins";
3863
+ function paddingKind(node) {
3864
+ if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
3865
+ return node.declaration ? paddingKind(node.declaration) : "other";
3866
+ }
3867
+ switch (node.type) {
3868
+ case "VariableDeclaration":
3869
+ return "variable";
3870
+ case "FunctionDeclaration":
3871
+ case "ClassDeclaration":
3872
+ return "block";
3873
+ case "IfStatement":
3874
+ case "ForStatement":
3875
+ case "ForInStatement":
3876
+ case "ForOfStatement":
3877
+ case "WhileStatement":
3878
+ case "DoWhileStatement":
3879
+ case "SwitchStatement":
3880
+ case "TryStatement":
3881
+ return "control";
3882
+ case "ReturnStatement":
3883
+ return "return";
3884
+ default:
3885
+ return "other";
3886
+ }
3887
+ }
3888
+ function paddingHasBlankLine(gap) {
3889
+ const lines = gap.split(`
3890
+ `);
3891
+ for (let index = 1;index < lines.length - 1; index++) {
3892
+ if (/^[\t\r ]*$/.test(lines[index])) {
3893
+ return true;
3894
+ }
3895
+ }
3896
+ return false;
3897
+ }
3898
+ function paddingBlankLineFix(sourceText, comments, prev, curr, newline) {
3899
+ const gap = sourceText.slice(prev.range[1], curr.range[0]);
3900
+ const match = /\r?\n/.exec(gap);
3901
+ if (!match) {
3902
+ return null;
3903
+ }
3904
+ const newlineStart = prev.range[1] + match.index;
3905
+ const newlineEnd = newlineStart + match[0].length;
3906
+ if (comments.some((comment) => comment.range[0] < newlineEnd && comment.range[1] > newlineStart)) {
3907
+ return null;
3908
+ }
3909
+ if (comments.some((comment) => comment.loc.start.line === prev.loc.end.line && /^\s*[A-Za-z][\w-]*-disable-next-line\b/.test(commentsNormalizedValue(comment)))) {
3910
+ return null;
3911
+ }
3912
+ return { newlineEnd, newline };
3913
+ }
3914
+ var paddingBetweenStatements = defineRule36({
3915
+ meta: {
3916
+ type: "layout",
3917
+ fixable: "whitespace",
3918
+ schema: [{
3919
+ type: "object",
3920
+ additionalProperties: false,
3921
+ properties: {
3922
+ returnMinStatements: { type: "integer", minimum: 1, default: 3 }
3923
+ }
3924
+ }],
3925
+ messages: {
3926
+ variableAndBlock: "Expected a blank line between a variable declaration and a function/class.",
3927
+ beforeBlock: "Expected a blank line before a function/class declaration.",
3928
+ afterBlock: "Expected a blank line after a function/class declaration.",
3929
+ variableAndControl: "Expected a blank line between a variable declaration and control flow.",
3930
+ controlAndVariable: "Expected a blank line between control flow and a variable declaration.",
3931
+ consecutiveControl: "Expected a blank line between control flow statements.",
3932
+ beforeReturn: "Expected a blank line before return statement."
3933
+ }
3934
+ },
3935
+ createOnce(context) {
3936
+ let returnMinStatements = 3;
3937
+ let newline = `
3938
+ `;
3939
+ let source;
3940
+ function checkStatements(statements) {
3941
+ for (let index = 1;index < statements.length; index++) {
3942
+ const prev = statements[index - 1];
3943
+ const curr = statements[index];
3944
+ const gap = source.text.slice(prev.range[1], curr.range[0]);
3945
+ if (!/\r?\n/.test(gap) || paddingHasBlankLine(gap)) {
3946
+ continue;
3947
+ }
3948
+ const prevKind = paddingKind(prev);
3949
+ const currKind = paddingKind(curr);
3950
+ const messageId = prevKind === "variable" && currKind === "block" || prevKind === "block" && currKind === "variable" ? "variableAndBlock" : currKind === "block" ? "beforeBlock" : prevKind === "block" ? "afterBlock" : prevKind === "variable" && currKind === "control" ? "variableAndControl" : prevKind === "control" && currKind === "variable" ? "controlAndVariable" : prevKind === "control" && currKind === "control" ? "consecutiveControl" : currKind === "return" && prevKind !== "variable" && statements.length >= returnMinStatements ? "beforeReturn" : null;
3951
+ if (!messageId) {
3952
+ continue;
3953
+ }
3954
+ const comments = source.getCommentsAfter(prev).filter((comment) => comment.range[1] <= curr.range[0]);
3955
+ const insert = paddingBlankLineFix(source.text, comments, prev, curr, newline);
3956
+ context.report({
3957
+ node: curr,
3958
+ messageId,
3959
+ fix: insert ? (fixer) => fixer.insertTextAfterRange([insert.newlineEnd, insert.newlineEnd], insert.newline) : undefined
3960
+ });
3961
+ }
3962
+ }
3963
+ return {
3964
+ before() {
3965
+ const options = optionsFirst(context, {});
3966
+ returnMinStatements = options.returnMinStatements ?? 3;
3967
+ source = context.sourceCode;
3968
+ newline = layoutNewline(source.text);
3969
+ },
3970
+ Program(node) {
3971
+ checkStatements(node.body);
3972
+ },
3973
+ BlockStatement(node) {
3974
+ checkStatements(node.body);
3975
+ }
3976
+ };
3977
+ }
3978
+ });
3979
+
2197
3980
  // src/rules/require-file-factory.ts
2198
- import { defineRule as defineRule26 } from "@oxlint/plugins";
2199
- var requireFileFactory = defineRule26({
3981
+ import { defineRule as defineRule37 } from "@oxlint/plugins";
3982
+ var requireFileFactory = defineRule37({
2200
3983
  meta: {
2201
3984
  type: "problem",
2202
3985
  schema: [{
@@ -2242,24 +4025,46 @@ var requireFileFactory = defineRule26({
2242
4025
  });
2243
4026
 
2244
4027
  // src/rules/require-object-params.ts
2245
- import { defineRule as defineRule27 } from "@oxlint/plugins";
2246
- var requireObjectParams = defineRule27({
4028
+ import { defineRule as defineRule38 } from "@oxlint/plugins";
4029
+ var requireObjectParams = defineRule38({
2247
4030
  meta: {
2248
4031
  type: "problem",
2249
- schema: [],
4032
+ schema: [{
4033
+ type: "object",
4034
+ additionalProperties: false,
4035
+ properties: {
4036
+ maxParams: { type: "integer", minimum: 1 },
4037
+ allDeclarations: { type: "boolean" },
4038
+ allowPattern: { type: "string" }
4039
+ }
4040
+ }],
2250
4041
  messages: {
2251
- objectParams: "Exported function {{name}} must accept at most one object parameter."
4042
+ objectParams: "{{label}} {{name}} must accept at most {{maxParams}} object parameter{{s}}."
2252
4043
  }
2253
4044
  },
2254
4045
  createOnce(context) {
2255
4046
  return {
2256
4047
  Program(program) {
2257
- for (const item of exportsCollectFunctions(program)) {
2258
- if (item.node.params.length > 1 || item.node.params.some((param) => !declarationsIsObjectParam(param))) {
4048
+ const { maxParams = 1, allDeclarations = false, allowPattern } = optionsFirst(context, {});
4049
+ const allowed = allowPattern ? new RegExp(allowPattern) : null;
4050
+ const seen = new Set;
4051
+ const functions = allDeclarations ? [...exportsCollectFunctions(program), ...declarationsTopLevelUnexportedFunctions(program)] : exportsCollectFunctions(program);
4052
+ for (const item of functions) {
4053
+ const key = `${item.name}:${item.node.start}:${item.node.end}`;
4054
+ if (seen.has(key) || allowed?.test(item.name)) {
4055
+ continue;
4056
+ }
4057
+ seen.add(key);
4058
+ if (item.node.params.length > maxParams || item.node.params.some((param) => !declarationsIsObjectParam(param))) {
2259
4059
  context.report({
2260
4060
  node: item.node,
2261
4061
  messageId: "objectParams",
2262
- data: { name: item.name }
4062
+ data: {
4063
+ label: allDeclarations ? "Function" : "Exported function",
4064
+ name: item.name,
4065
+ maxParams: maxParams === 1 ? "one" : String(maxParams),
4066
+ s: maxParams === 1 ? "" : "s"
4067
+ }
2263
4068
  });
2264
4069
  }
2265
4070
  }
@@ -2269,8 +4074,8 @@ var requireObjectParams = defineRule27({
2269
4074
  });
2270
4075
 
2271
4076
  // src/rules/require-orpc-output.ts
2272
- import { defineRule as defineRule28 } from "@oxlint/plugins";
2273
- var requireOrpcOutput = defineRule28({
4077
+ import { defineRule as defineRule39 } from "@oxlint/plugins";
4078
+ var requireOrpcOutput = defineRule39({
2274
4079
  meta: {
2275
4080
  type: "problem",
2276
4081
  schema: [{
@@ -2312,8 +4117,8 @@ var requireOrpcOutput = defineRule28({
2312
4117
  });
2313
4118
 
2314
4119
  // src/rules/require-paired-call.ts
2315
- import { defineRule as defineRule29 } from "@oxlint/plugins";
2316
- var requirePairedCall = defineRule29({
4120
+ import { defineRule as defineRule40 } from "@oxlint/plugins";
4121
+ var requirePairedCall = defineRule40({
2317
4122
  meta: {
2318
4123
  type: "problem",
2319
4124
  schema: [{
@@ -2321,9 +4126,24 @@ var requirePairedCall = defineRule29({
2321
4126
  additionalProperties: false,
2322
4127
  properties: {
2323
4128
  when: { type: "string" },
2324
- require: { type: "string" }
4129
+ require: { type: "string" },
4130
+ pairs: {
4131
+ type: "array",
4132
+ items: {
4133
+ type: "object",
4134
+ additionalProperties: false,
4135
+ properties: {
4136
+ when: { type: "string" },
4137
+ require: { type: "string" }
4138
+ },
4139
+ required: ["when", "require"]
4140
+ }
4141
+ }
2325
4142
  },
2326
- required: ["when", "require"]
4143
+ anyOf: [
4144
+ { required: ["when", "require"] },
4145
+ { required: ["pairs"] }
4146
+ ]
2327
4147
  }],
2328
4148
  messages: {
2329
4149
  paired: "{{when}} requires a call to {{require}} in the same file."
@@ -2331,45 +4151,50 @@ var requirePairedCall = defineRule29({
2331
4151
  },
2332
4152
  createOnce(context) {
2333
4153
  let aliases = new Map;
2334
- let firstWhen = null;
2335
- let sawRequired = false;
4154
+ let pairs = [];
2336
4155
  return {
2337
4156
  before() {
4157
+ const options = optionsFirst(context);
4158
+ const configured = [
4159
+ ...options.when && options.require ? [{ when: options.when, require: options.require }] : [],
4160
+ ...options.pairs ?? []
4161
+ ];
2338
4162
  aliases = new Map;
2339
- firstWhen = null;
2340
- sawRequired = false;
4163
+ pairs = configured.map((pair) => ({ ...pair, firstWhen: null, sawRequired: false }));
2341
4164
  },
2342
4165
  Program(program) {
2343
4166
  aliases = astImportedCallAliases(program);
2344
4167
  },
2345
4168
  CallExpression(node) {
2346
- const { when, require: require2 } = optionsFirst(context);
2347
4169
  const name = astResolvedCallName(node, aliases);
2348
- if (name === when && !firstWhen) {
2349
- firstWhen = node;
2350
- }
2351
- if (name === require2) {
2352
- sawRequired = true;
4170
+ for (const pair of pairs) {
4171
+ if (name === pair.when && !pair.firstWhen) {
4172
+ pair.firstWhen = node;
4173
+ }
4174
+ if (name === pair.require) {
4175
+ pair.sawRequired = true;
4176
+ }
2353
4177
  }
2354
4178
  },
2355
4179
  after() {
2356
- if (!firstWhen || sawRequired) {
2357
- return;
4180
+ for (const pair of pairs) {
4181
+ if (!pair.firstWhen || pair.sawRequired) {
4182
+ continue;
4183
+ }
4184
+ context.report({
4185
+ node: pair.firstWhen,
4186
+ messageId: "paired",
4187
+ data: { when: pair.when, require: pair.require }
4188
+ });
2358
4189
  }
2359
- const { when, require: require2 } = optionsFirst(context);
2360
- context.report({
2361
- node: firstWhen,
2362
- messageId: "paired",
2363
- data: { when, require: require2 }
2364
- });
2365
4190
  }
2366
4191
  };
2367
4192
  }
2368
4193
  });
2369
4194
 
2370
4195
  // src/rules/route-surface.ts
2371
- import { defineRule as defineRule30 } from "@oxlint/plugins";
2372
- var routeSurface = defineRule30({
4196
+ import { defineRule as defineRule41 } from "@oxlint/plugins";
4197
+ var routeSurface = defineRule41({
2373
4198
  meta: {
2374
4199
  type: "problem",
2375
4200
  schema: [{
@@ -2410,25 +4235,140 @@ var routeSurface = defineRule30({
2410
4235
  }
2411
4236
  });
2412
4237
 
4238
+ // src/rules/test-title-pattern.ts
4239
+ import { defineRule as defineRule42 } from "@oxlint/plugins";
4240
+ var TEST_MODIFIERS = new Set(["only", "skip", "concurrent", "todo", "each"]);
4241
+ function isConfiguredName(name, callees) {
4242
+ if (!name) {
4243
+ return false;
4244
+ }
4245
+ return callees.some((callee) => {
4246
+ if (name === callee) {
4247
+ return true;
4248
+ }
4249
+ if (!name.startsWith(`${callee}.`)) {
4250
+ return false;
4251
+ }
4252
+ return name.slice(callee.length + 1).split(".").every((part) => TEST_MODIFIERS.has(part));
4253
+ });
4254
+ }
4255
+ function isConfiguredCall(node, callees) {
4256
+ if (isConfiguredName(astDottedName(node.callee), callees)) {
4257
+ return true;
4258
+ }
4259
+ if (node.callee.type === "CallExpression") {
4260
+ return isConfiguredName(astDottedName(node.callee.callee), callees);
4261
+ }
4262
+ return node.callee.type === "TaggedTemplateExpression" && isConfiguredName(astDottedName(node.callee.tag), callees);
4263
+ }
4264
+ function titleText(node) {
4265
+ if (node.type === "Literal" && typeof node.value === "string") {
4266
+ return node.value;
4267
+ }
4268
+ if (node.type === "TemplateLiteral" && node.expressions.length === 0) {
4269
+ return node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw;
4270
+ }
4271
+ }
4272
+ function regexMatches(regex, value) {
4273
+ regex.lastIndex = 0;
4274
+ return regex.test(value);
4275
+ }
4276
+ var testTitlePattern = defineRule42({
4277
+ meta: {
4278
+ type: "suggestion",
4279
+ schema: [{
4280
+ type: "object",
4281
+ additionalProperties: false,
4282
+ properties: {
4283
+ callees: { type: "array", items: { type: "string" } },
4284
+ forbid: { type: "string" },
4285
+ require: { type: "string" },
4286
+ flags: { type: "string", pattern: "^[dgimsuvy]*$" }
4287
+ }
4288
+ }],
4289
+ defaultOptions: [{
4290
+ callees: ["describe", "it", "test"],
4291
+ flags: ""
4292
+ }],
4293
+ messages: {
4294
+ forbidden: "Test title matches forbidden pattern '{{pattern}}'.",
4295
+ required: "Test title must match required pattern '{{pattern}}'."
4296
+ }
4297
+ },
4298
+ createOnce(context) {
4299
+ let callees = [];
4300
+ let forbid;
4301
+ let required;
4302
+ return {
4303
+ before() {
4304
+ const options = optionsFirst(context);
4305
+ callees = options.callees;
4306
+ const flags = options.flags ?? "";
4307
+ forbid = options.forbid === undefined ? undefined : { regex: new RegExp(options.forbid, flags), pattern: options.forbid };
4308
+ required = options.require === undefined ? undefined : { regex: new RegExp(options.require, flags), pattern: options.require };
4309
+ if (forbid === undefined && required === undefined) {
4310
+ return false;
4311
+ }
4312
+ },
4313
+ CallExpression(node) {
4314
+ if (!isConfiguredCall(node, callees)) {
4315
+ return;
4316
+ }
4317
+ const title = node.arguments[0];
4318
+ if (!title) {
4319
+ return;
4320
+ }
4321
+ const text = titleText(title);
4322
+ if (text === undefined) {
4323
+ return;
4324
+ }
4325
+ if (forbid && regexMatches(forbid.regex, text)) {
4326
+ context.report({
4327
+ node: title,
4328
+ messageId: "forbidden",
4329
+ data: { pattern: forbid.pattern }
4330
+ });
4331
+ }
4332
+ if (required && !regexMatches(required.regex, text)) {
4333
+ context.report({
4334
+ node: title,
4335
+ messageId: "required",
4336
+ data: { pattern: required.pattern }
4337
+ });
4338
+ }
4339
+ }
4340
+ };
4341
+ }
4342
+ });
4343
+
2413
4344
  // src/index.ts
2414
4345
  var src_default = eslintCompatPlugin({
2415
4346
  meta: { name: "arch" },
2416
4347
  rules: {
4348
+ "call-array-multiline": callArrayMultiline,
4349
+ "chain-newline": chainNewline,
2417
4350
  "declaration-name": declarationName,
2418
4351
  "export-file-prefix": exportFilePrefix,
2419
4352
  "export-name-pattern": exportNamePattern,
2420
4353
  "filename-export-name": filenameExportName,
2421
4354
  "filename-match": filenameMatch,
2422
4355
  "folder-prefix": folderPrefix,
4356
+ "jsx-attributes-multiline": jsxAttributesMultiline,
4357
+ "key-value-same-line": keyValueSameLine,
4358
+ "no-comments": noComments,
2423
4359
  "no-extra-exports": noExtraExports,
2424
4360
  "no-extra-factory-keys": noExtraFactoryKeys,
2425
4361
  "no-file-level-helpers": noFileLevelHelpers,
2426
4362
  "no-imported-type-alias": noImportedTypeAlias,
2427
4363
  "no-inline-schema-elements": noInlineSchemaElements,
2428
4364
  "no-inline-types": noInlineTypes,
4365
+ "no-literal-in": noLiteralIn,
2429
4366
  "no-local-schema-construction": noLocalSchemaConstruction,
2430
4367
  "no-member-comments": noMemberComments,
4368
+ "no-module-mutable-state": noModuleMutableState,
4369
+ "no-promise-all-mutation": noPromiseAllMutation,
2431
4370
  "no-rederive-schema": noRederiveSchema,
4371
+ "no-restricted-constructor": noRestrictedConstructor,
2432
4372
  "no-restricted-files": noRestrictedFiles,
2433
4373
  "no-restricted-token": noRestrictedToken,
2434
4374
  "no-runtime-in-types": noRuntimeInTypes,
@@ -2437,13 +4377,16 @@ var src_default = eslintCompatPlugin({
2437
4377
  "no-trivial-functions": noTrivialFunctions,
2438
4378
  "no-type-declarations": noTypeDeclarations,
2439
4379
  "no-unescaped-like": noUnescapedLike,
4380
+ "object-multiline": objectMultiline,
2440
4381
  "only-export-components": onlyExportComponents,
2441
4382
  "only-export-constants": onlyExportConstants,
4383
+ "padding-between-statements": paddingBetweenStatements,
2442
4384
  "require-file-factory": requireFileFactory,
2443
4385
  "require-object-params": requireObjectParams,
2444
4386
  "require-orpc-output": requireOrpcOutput,
2445
4387
  "require-paired-call": requirePairedCall,
2446
- "route-surface": routeSurface
4388
+ "route-surface": routeSurface,
4389
+ "test-title-pattern": testTitlePattern
2447
4390
  }
2448
4391
  });
2449
4392
  export {