@timmo001/oxlint-rules 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -138,9 +138,10 @@ they only run when the consumer enables `typeAware`.
138
138
  - `no-raw-gh`: reports the raw `execute`, `json` and `stream` methods called on
139
139
  a `yield* Gh` binding from `@timmo001/effect-gh`, `Api.raw` from that package,
140
140
  and calls that launch gh: `"gh"` followed by its arguments, such as
141
- `executor.run("gh", ...)`, or an argument array starting with `"gh"`, such
142
- as `Bun.spawn(["gh", ...])`. Use the
143
- package's typed operations, and add one to effect-gh when a command has none.
141
+ `executor.run("gh", ...)`, or an argument array starting with `"gh"` passed
142
+ to a spawn, exec, run or command function, such as `Bun.spawn(["gh", ...])`.
143
+ Use the package's typed operations, and add one to effect-gh when a command
144
+ has none.
144
145
  effect-gh's own relative imports are not reported.
145
146
  - `no-try-catch-in-effect-generators`: diagnoses synchronous `try/catch` owned
146
147
  by generators passed directly to `Effect.gen` or the curried `Effect.fn`
package/dist/cli.js CHANGED
@@ -2172,7 +2172,8 @@ var StatementTypes = {
2172
2172
  ]
2173
2173
  ]))
2174
2174
  };
2175
- function createPaddingLineRule(options) {
2175
+ function createPaddingLineRule(options, statementTypes) {
2176
+ const customStatementTypes = statementTypes ?? {};
2176
2177
  return {
2177
2178
  meta: {
2178
2179
  type: "layout",
@@ -2288,7 +2289,7 @@ function createPaddingLineRule(options) {
2288
2289
  return !isSingleLine(innerStatementNode);
2289
2290
  return true;
2290
2291
  } else {
2291
- const statementType = StatementTypes[type];
2292
+ const statementType = customStatementTypes[type] ?? StatementTypes[type];
2292
2293
  if (statementType === undefined)
2293
2294
  throw new Error(`Padding rule invariant: unsupported statement type ${type}`);
2294
2295
  return statementType.test(innerStatementNode, sourceCode);
@@ -2387,6 +2388,20 @@ function createPaddingLineRule(options) {
2387
2388
  }
2388
2389
 
2389
2390
  // vendor/anti-slop/src/rules/require-readable-spacing.ts
2391
+ function isSinglelineBinding(node) {
2392
+ const declaration = node.type === "ExportNamedDeclaration" ? node.declaration : node;
2393
+ return declaration?.type === "VariableDeclaration" && node.loc.start.line === node.loc.end.line;
2394
+ }
2395
+ function hasLeadingComment(node, sourceCode) {
2396
+ const previousLine = sourceCode.getTokenBefore(node)?.loc.end.line;
2397
+ return sourceCode.getCommentsBefore(node).some((comment) => comment.loc.start.line !== previousLine);
2398
+ }
2399
+ var singlelineBinding = {
2400
+ test: (node) => isSinglelineBinding(node)
2401
+ };
2402
+ var undocumentedSinglelineBinding = {
2403
+ test: (node, sourceCode) => isSinglelineBinding(node) && !hasLeadingComment(node, sourceCode)
2404
+ };
2390
2405
  var paddingRule = createPaddingLineRule([
2391
2406
  { blankLine: "always", prev: "import", next: "*" },
2392
2407
  { blankLine: "always", prev: "*", next: { selector: "Program > :not(ImportDeclaration)" } },
@@ -2406,6 +2421,7 @@ var paddingRule = createPaddingLineRule([
2406
2421
  { blankLine: "always", prev: "*", next: ["return", "if", "switch", "try", "for", "while", "do"] },
2407
2422
  { blankLine: "always", prev: "block-like", next: "*" },
2408
2423
  { blankLine: "any", prev: "import", next: "import" },
2424
+ { blankLine: "any", prev: "singleline-binding", next: "undocumented-singleline-binding" },
2409
2425
  {
2410
2426
  blankLine: "any",
2411
2427
  prev: {
@@ -2415,7 +2431,10 @@ var paddingRule = createPaddingLineRule([
2415
2431
  selector: ':matches(TSDeclareFunction, FunctionDeclaration, ExportNamedDeclaration[declaration.type="TSDeclareFunction"], ExportNamedDeclaration[declaration.type="FunctionDeclaration"])'
2416
2432
  }
2417
2433
  }
2418
- ]);
2434
+ ], {
2435
+ "singleline-binding": singlelineBinding,
2436
+ "undocumented-singleline-binding": undocumentedSinglelineBinding
2437
+ });
2419
2438
  var requireReadableSpacingRule = {
2420
2439
  ...paddingRule,
2421
2440
  meta: {
@@ -3004,14 +3023,20 @@ function isYieldGh(sourceCode, node) {
3004
3023
  function isGhBinding(sourceCode, identifier) {
3005
3024
  return resolveVariable(sourceCode, identifier)?.defs.some((definition) => definition.type === "Variable" && definition.node.type === "VariableDeclarator" && isYieldGh(sourceCode, definition.node.init)) ?? false;
3006
3025
  }
3007
- function launchesGh(args) {
3008
- const [command, argv] = args;
3026
+ var launcherName = /spawn|exec|run|command/i;
3027
+ function calleeName(callee) {
3028
+ if (callee.type === "Identifier")
3029
+ return callee.name;
3030
+ return staticMemberName(callee);
3031
+ }
3032
+ function launchesGh(node) {
3033
+ const [command, argv] = node.arguments;
3009
3034
  if (command?.type === "Literal")
3010
3035
  return command.value === "gh" && argv !== undefined;
3011
3036
  if (command?.type !== "ArrayExpression")
3012
3037
  return false;
3013
3038
  const first = command.elements[0];
3014
- return first?.type === "Literal" && first.value === "gh";
3039
+ return first?.type === "Literal" && first.value === "gh" && launcherName.test(calleeName(node.callee) ?? "");
3015
3040
  }
3016
3041
  var noRawGhRule = defineRule24({
3017
3042
  meta: {
@@ -3041,7 +3066,7 @@ var noRawGhRule = defineRule24({
3041
3066
  return;
3042
3067
  }
3043
3068
  }
3044
- if (launchesGh(node.arguments))
3069
+ if (launchesGh(node))
3045
3070
  context.report({ node, messageId: "spawnGh" });
3046
3071
  }
3047
3072
  };
@@ -55,14 +55,20 @@ function isYieldGh(sourceCode, node) {
55
55
  function isGhBinding(sourceCode, identifier) {
56
56
  return resolveVariable(sourceCode, identifier)?.defs.some((definition) => definition.type === "Variable" && definition.node.type === "VariableDeclarator" && isYieldGh(sourceCode, definition.node.init)) ?? false;
57
57
  }
58
- function launchesGh(args) {
59
- const [command, argv] = args;
58
+ var launcherName = /spawn|exec|run|command/i;
59
+ function calleeName(callee) {
60
+ if (callee.type === "Identifier")
61
+ return callee.name;
62
+ return staticMemberName(callee);
63
+ }
64
+ function launchesGh(node) {
65
+ const [command, argv] = node.arguments;
60
66
  if (command?.type === "Literal")
61
67
  return command.value === "gh" && argv !== undefined;
62
68
  if (command?.type !== "ArrayExpression")
63
69
  return false;
64
70
  const first = command.elements[0];
65
- return first?.type === "Literal" && first.value === "gh";
71
+ return first?.type === "Literal" && first.value === "gh" && launcherName.test(calleeName(node.callee) ?? "");
66
72
  }
67
73
  var noRawGhRule = defineRule({
68
74
  meta: {
@@ -92,7 +98,7 @@ var noRawGhRule = defineRule({
92
98
  return;
93
99
  }
94
100
  }
95
- if (launchesGh(node.arguments))
101
+ if (launchesGh(node))
96
102
  context.report({ node, messageId: "spawnGh" });
97
103
  }
98
104
  };
@@ -2663,7 +2669,8 @@ var StatementTypes = {
2663
2669
  ]
2664
2670
  ]))
2665
2671
  };
2666
- function createPaddingLineRule(options) {
2672
+ function createPaddingLineRule(options, statementTypes) {
2673
+ const customStatementTypes = statementTypes ?? {};
2667
2674
  return {
2668
2675
  meta: {
2669
2676
  type: "layout",
@@ -2779,7 +2786,7 @@ function createPaddingLineRule(options) {
2779
2786
  return !isSingleLine(innerStatementNode);
2780
2787
  return true;
2781
2788
  } else {
2782
- const statementType = StatementTypes[type];
2789
+ const statementType = customStatementTypes[type] ?? StatementTypes[type];
2783
2790
  if (statementType === undefined)
2784
2791
  throw new Error(`Padding rule invariant: unsupported statement type ${type}`);
2785
2792
  return statementType.test(innerStatementNode, sourceCode);
@@ -2878,6 +2885,20 @@ function createPaddingLineRule(options) {
2878
2885
  }
2879
2886
 
2880
2887
  // vendor/anti-slop/src/rules/require-readable-spacing.ts
2888
+ function isSinglelineBinding(node) {
2889
+ const declaration = node.type === "ExportNamedDeclaration" ? node.declaration : node;
2890
+ return declaration?.type === "VariableDeclaration" && node.loc.start.line === node.loc.end.line;
2891
+ }
2892
+ function hasLeadingComment(node, sourceCode) {
2893
+ const previousLine = sourceCode.getTokenBefore(node)?.loc.end.line;
2894
+ return sourceCode.getCommentsBefore(node).some((comment) => comment.loc.start.line !== previousLine);
2895
+ }
2896
+ var singlelineBinding = {
2897
+ test: (node) => isSinglelineBinding(node)
2898
+ };
2899
+ var undocumentedSinglelineBinding = {
2900
+ test: (node, sourceCode) => isSinglelineBinding(node) && !hasLeadingComment(node, sourceCode)
2901
+ };
2881
2902
  var paddingRule = createPaddingLineRule([
2882
2903
  { blankLine: "always", prev: "import", next: "*" },
2883
2904
  { blankLine: "always", prev: "*", next: { selector: "Program > :not(ImportDeclaration)" } },
@@ -2897,6 +2918,7 @@ var paddingRule = createPaddingLineRule([
2897
2918
  { blankLine: "always", prev: "*", next: ["return", "if", "switch", "try", "for", "while", "do"] },
2898
2919
  { blankLine: "always", prev: "block-like", next: "*" },
2899
2920
  { blankLine: "any", prev: "import", next: "import" },
2921
+ { blankLine: "any", prev: "singleline-binding", next: "undocumented-singleline-binding" },
2900
2922
  {
2901
2923
  blankLine: "any",
2902
2924
  prev: {
@@ -2906,7 +2928,10 @@ var paddingRule = createPaddingLineRule([
2906
2928
  selector: ':matches(TSDeclareFunction, FunctionDeclaration, ExportNamedDeclaration[declaration.type="TSDeclareFunction"], ExportNamedDeclaration[declaration.type="FunctionDeclaration"])'
2907
2929
  }
2908
2930
  }
2909
- ]);
2931
+ ], {
2932
+ "singleline-binding": singlelineBinding,
2933
+ "undocumented-singleline-binding": undocumentedSinglelineBinding
2934
+ });
2910
2935
  var requireReadableSpacingRule = {
2911
2936
  ...paddingRule,
2912
2937
  meta: {
@@ -2167,7 +2167,8 @@ var StatementTypes = {
2167
2167
  ]
2168
2168
  ]))
2169
2169
  };
2170
- function createPaddingLineRule(options) {
2170
+ function createPaddingLineRule(options, statementTypes) {
2171
+ const customStatementTypes = statementTypes ?? {};
2171
2172
  return {
2172
2173
  meta: {
2173
2174
  type: "layout",
@@ -2283,7 +2284,7 @@ function createPaddingLineRule(options) {
2283
2284
  return !isSingleLine(innerStatementNode);
2284
2285
  return true;
2285
2286
  } else {
2286
- const statementType = StatementTypes[type];
2287
+ const statementType = customStatementTypes[type] ?? StatementTypes[type];
2287
2288
  if (statementType === undefined)
2288
2289
  throw new Error(`Padding rule invariant: unsupported statement type ${type}`);
2289
2290
  return statementType.test(innerStatementNode, sourceCode);
@@ -2382,6 +2383,20 @@ function createPaddingLineRule(options) {
2382
2383
  }
2383
2384
 
2384
2385
  // vendor/anti-slop/src/rules/require-readable-spacing.ts
2386
+ function isSinglelineBinding(node) {
2387
+ const declaration = node.type === "ExportNamedDeclaration" ? node.declaration : node;
2388
+ return declaration?.type === "VariableDeclaration" && node.loc.start.line === node.loc.end.line;
2389
+ }
2390
+ function hasLeadingComment(node, sourceCode) {
2391
+ const previousLine = sourceCode.getTokenBefore(node)?.loc.end.line;
2392
+ return sourceCode.getCommentsBefore(node).some((comment) => comment.loc.start.line !== previousLine);
2393
+ }
2394
+ var singlelineBinding = {
2395
+ test: (node) => isSinglelineBinding(node)
2396
+ };
2397
+ var undocumentedSinglelineBinding = {
2398
+ test: (node, sourceCode) => isSinglelineBinding(node) && !hasLeadingComment(node, sourceCode)
2399
+ };
2385
2400
  var paddingRule = createPaddingLineRule([
2386
2401
  { blankLine: "always", prev: "import", next: "*" },
2387
2402
  { blankLine: "always", prev: "*", next: { selector: "Program > :not(ImportDeclaration)" } },
@@ -2401,6 +2416,7 @@ var paddingRule = createPaddingLineRule([
2401
2416
  { blankLine: "always", prev: "*", next: ["return", "if", "switch", "try", "for", "while", "do"] },
2402
2417
  { blankLine: "always", prev: "block-like", next: "*" },
2403
2418
  { blankLine: "any", prev: "import", next: "import" },
2419
+ { blankLine: "any", prev: "singleline-binding", next: "undocumented-singleline-binding" },
2404
2420
  {
2405
2421
  blankLine: "any",
2406
2422
  prev: {
@@ -2410,7 +2426,10 @@ var paddingRule = createPaddingLineRule([
2410
2426
  selector: ':matches(TSDeclareFunction, FunctionDeclaration, ExportNamedDeclaration[declaration.type="TSDeclareFunction"], ExportNamedDeclaration[declaration.type="FunctionDeclaration"])'
2411
2427
  }
2412
2428
  }
2413
- ]);
2429
+ ], {
2430
+ "singleline-binding": singlelineBinding,
2431
+ "undocumented-singleline-binding": undocumentedSinglelineBinding
2432
+ });
2414
2433
  var requireReadableSpacingRule = {
2415
2434
  ...paddingRule,
2416
2435
  meta: {
@@ -50,14 +50,20 @@ function isYieldGh(sourceCode, node) {
50
50
  function isGhBinding(sourceCode, identifier) {
51
51
  return resolveVariable(sourceCode, identifier)?.defs.some((definition) => definition.type === "Variable" && definition.node.type === "VariableDeclarator" && isYieldGh(sourceCode, definition.node.init)) ?? false;
52
52
  }
53
- function launchesGh(args) {
54
- const [command, argv] = args;
53
+ var launcherName = /spawn|exec|run|command/i;
54
+ function calleeName(callee) {
55
+ if (callee.type === "Identifier")
56
+ return callee.name;
57
+ return staticMemberName(callee);
58
+ }
59
+ function launchesGh(node) {
60
+ const [command, argv] = node.arguments;
55
61
  if (command?.type === "Literal")
56
62
  return command.value === "gh" && argv !== undefined;
57
63
  if (command?.type !== "ArrayExpression")
58
64
  return false;
59
65
  const first = command.elements[0];
60
- return first?.type === "Literal" && first.value === "gh";
66
+ return first?.type === "Literal" && first.value === "gh" && launcherName.test(calleeName(node.callee) ?? "");
61
67
  }
62
68
  var noRawGhRule = defineRule({
63
69
  meta: {
@@ -87,7 +93,7 @@ var noRawGhRule = defineRule({
87
93
  return;
88
94
  }
89
95
  }
90
- if (launchesGh(node.arguments))
96
+ if (launchesGh(node))
91
97
  context.report({ node, messageId: "spawnGh" });
92
98
  }
93
99
  };
@@ -2162,7 +2162,8 @@ var StatementTypes = {
2162
2162
  ]
2163
2163
  ]))
2164
2164
  };
2165
- function createPaddingLineRule(options) {
2165
+ function createPaddingLineRule(options, statementTypes) {
2166
+ const customStatementTypes = statementTypes ?? {};
2166
2167
  return {
2167
2168
  meta: {
2168
2169
  type: "layout",
@@ -2278,7 +2279,7 @@ function createPaddingLineRule(options) {
2278
2279
  return !isSingleLine(innerStatementNode);
2279
2280
  return true;
2280
2281
  } else {
2281
- const statementType = StatementTypes[type];
2282
+ const statementType = customStatementTypes[type] ?? StatementTypes[type];
2282
2283
  if (statementType === undefined)
2283
2284
  throw new Error(`Padding rule invariant: unsupported statement type ${type}`);
2284
2285
  return statementType.test(innerStatementNode, sourceCode);
@@ -2377,6 +2378,20 @@ function createPaddingLineRule(options) {
2377
2378
  }
2378
2379
 
2379
2380
  // vendor/anti-slop/src/rules/require-readable-spacing.ts
2381
+ function isSinglelineBinding(node) {
2382
+ const declaration = node.type === "ExportNamedDeclaration" ? node.declaration : node;
2383
+ return declaration?.type === "VariableDeclaration" && node.loc.start.line === node.loc.end.line;
2384
+ }
2385
+ function hasLeadingComment(node, sourceCode) {
2386
+ const previousLine = sourceCode.getTokenBefore(node)?.loc.end.line;
2387
+ return sourceCode.getCommentsBefore(node).some((comment) => comment.loc.start.line !== previousLine);
2388
+ }
2389
+ var singlelineBinding = {
2390
+ test: (node) => isSinglelineBinding(node)
2391
+ };
2392
+ var undocumentedSinglelineBinding = {
2393
+ test: (node, sourceCode) => isSinglelineBinding(node) && !hasLeadingComment(node, sourceCode)
2394
+ };
2380
2395
  var paddingRule = createPaddingLineRule([
2381
2396
  { blankLine: "always", prev: "import", next: "*" },
2382
2397
  { blankLine: "always", prev: "*", next: { selector: "Program > :not(ImportDeclaration)" } },
@@ -2396,6 +2411,7 @@ var paddingRule = createPaddingLineRule([
2396
2411
  { blankLine: "always", prev: "*", next: ["return", "if", "switch", "try", "for", "while", "do"] },
2397
2412
  { blankLine: "always", prev: "block-like", next: "*" },
2398
2413
  { blankLine: "any", prev: "import", next: "import" },
2414
+ { blankLine: "any", prev: "singleline-binding", next: "undocumented-singleline-binding" },
2399
2415
  {
2400
2416
  blankLine: "any",
2401
2417
  prev: {
@@ -2405,7 +2421,10 @@ var paddingRule = createPaddingLineRule([
2405
2421
  selector: ':matches(TSDeclareFunction, FunctionDeclaration, ExportNamedDeclaration[declaration.type="TSDeclareFunction"], ExportNamedDeclaration[declaration.type="FunctionDeclaration"])'
2406
2422
  }
2407
2423
  }
2408
- ]);
2424
+ ], {
2425
+ "singleline-binding": singlelineBinding,
2426
+ "undocumented-singleline-binding": undocumentedSinglelineBinding
2427
+ });
2409
2428
  var requireReadableSpacingRule = {
2410
2429
  ...paddingRule,
2411
2430
  meta: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timmo001/oxlint-rules",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Shared Oxlint plugins and configs with optional Effect rules.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -49,13 +49,13 @@
49
49
  "typecheck": "tsc --noEmit"
50
50
  },
51
51
  "peerDependencies": {
52
- "@oxlint/plugins": "1.87.0",
53
- "oxlint": "1.87.0"
52
+ "@oxlint/plugins": "1.88.0",
53
+ "oxlint": "1.88.0"
54
54
  },
55
55
  "devDependencies": {
56
- "@oxlint/plugins": "1.87.0",
56
+ "@oxlint/plugins": "1.88.0",
57
57
  "@types/bun": "^1.3.14",
58
- "oxlint": "1.87.0",
58
+ "oxlint": "1.88.0",
59
59
  "oxlint-tsgolint": "7.0.2004",
60
60
  "prettier": "^3.9.5",
61
61
  "skills-ref": "0.1.5",
@@ -61,12 +61,23 @@ function isGhBinding(
61
61
  );
62
62
  }
63
63
 
64
+ /** Callee names that take an argv array to launch, such as `Bun.spawn` or `execFileSync`. */
65
+ const launcherName = /spawn|exec|run|command/i;
66
+
67
+ function calleeName(callee: ESTree.Expression): string | null {
68
+ if (callee.type === "Identifier") return callee.name;
69
+
70
+ return staticMemberName(callee);
71
+ }
72
+
64
73
  /**
65
- * Whether call arguments launch gh: `"gh"` followed by its argv, as in
66
- * `run("gh", args)`, or an argv array starting with `"gh"`.
74
+ * Whether a call launches gh: `"gh"` followed by its argv, as in
75
+ * `run("gh", args)`, or an argv array starting with `"gh"` passed to a
76
+ * spawn, exec, run or command function. Recording argv, as in
77
+ * `calls.push(["gh", ...args])`, is not launching it.
67
78
  */
68
- function launchesGh(args: ESTree.CallExpression["arguments"]): boolean {
69
- const [command, argv] = args;
79
+ function launchesGh(node: ESTree.CallExpression): boolean {
80
+ const [command, argv] = node.arguments;
70
81
 
71
82
  if (command?.type === "Literal")
72
83
  return command.value === "gh" && argv !== undefined;
@@ -74,7 +85,11 @@ function launchesGh(args: ESTree.CallExpression["arguments"]): boolean {
74
85
  if (command?.type !== "ArrayExpression") return false;
75
86
  const first = command.elements[0];
76
87
 
77
- return first?.type === "Literal" && first.value === "gh";
88
+ return (
89
+ first?.type === "Literal" &&
90
+ first.value === "gh" &&
91
+ launcherName.test(calleeName(node.callee) ?? "")
92
+ );
78
93
  }
79
94
 
80
95
  /** Route gh through effect-gh's typed operations instead of raw argv or direct spawns. */
@@ -125,8 +140,7 @@ export const noRawGhRule = defineRule({
125
140
  }
126
141
  }
127
142
 
128
- if (launchesGh(node.arguments))
129
- context.report({ node, messageId: "spawnGh" });
143
+ if (launchesGh(node)) context.report({ node, messageId: "spawnGh" });
130
144
  },
131
145
  };
132
146
  },
@@ -1,40 +1,73 @@
1
- import type { CreateRule } from "@oxlint/plugins";
1
+ import type { CreateRule, ESTree, SourceCode } from "@oxlint/plugins";
2
2
 
3
3
  import createPaddingLineRule from "../vendor/eslint-stylistic/padding-line-between-statements.ts";
4
4
 
5
- const paddingRule = createPaddingLineRule([
6
- { blankLine: "always", prev: "import", next: "*" },
7
- { blankLine: "always", prev: "*", next: { selector: "Program > :not(ImportDeclaration)" } },
8
- { blankLine: "always", prev: { selector: "Program > :not(ImportDeclaration)" }, next: "*" },
9
- { blankLine: "always", prev: "*", next: ["function", "class", "interface", "type"] },
10
- { blankLine: "always", prev: ["function", "class", "interface", "type"], next: "*" },
11
- {
12
- blankLine: "always",
13
- prev: "*",
14
- next: ["multiline-const", "multiline-let", "multiline-var", "multiline-using"],
15
- },
16
- {
17
- blankLine: "always",
18
- prev: ["multiline-const", "multiline-let", "multiline-var", "multiline-using"],
19
- next: "*",
20
- },
21
- { blankLine: "always", prev: "*", next: ["return", "if", "switch", "try", "for", "while", "do"] },
22
- { blankLine: "always", prev: "block-like", next: "*" },
23
- { blankLine: "any", prev: "import", next: "import" },
24
- {
25
- blankLine: "any",
26
- prev: {
27
- selector:
28
- ':matches(TSDeclareFunction, ExportNamedDeclaration[declaration.type="TSDeclareFunction"])',
5
+ import type { NodeTestObject } from "../vendor/eslint-stylistic/padding-line-between-statements.ts";
6
+
7
+ /** A `const`/`let`/`var`/`using` declaration, optionally exported, written on one line. */
8
+ function isSinglelineBinding(node: ESTree.Node): boolean {
9
+ const declaration = node.type === "ExportNamedDeclaration" ? node.declaration : node;
10
+ return declaration?.type === "VariableDeclaration" && node.loc.start.line === node.loc.end.line;
11
+ }
12
+
13
+ /** Comments above a statement belong to it; a comment trailing the previous line does not. */
14
+ function hasLeadingComment(node: ESTree.Node, sourceCode: SourceCode): boolean {
15
+ const previousLine = sourceCode.getTokenBefore(node)?.loc.end.line;
16
+ return sourceCode
17
+ .getCommentsBefore(node)
18
+ .some((comment) => comment.loc.start.line !== previousLine);
19
+ }
20
+
21
+ const singlelineBinding: NodeTestObject = {
22
+ test: (node) => isSinglelineBinding(node),
23
+ };
24
+
25
+ const undocumentedSinglelineBinding: NodeTestObject = {
26
+ test: (node, sourceCode) => isSinglelineBinding(node) && !hasLeadingComment(node, sourceCode),
27
+ };
28
+
29
+ const paddingRule = createPaddingLineRule(
30
+ [
31
+ { blankLine: "always", prev: "import", next: "*" },
32
+ { blankLine: "always", prev: "*", next: { selector: "Program > :not(ImportDeclaration)" } },
33
+ { blankLine: "always", prev: { selector: "Program > :not(ImportDeclaration)" }, next: "*" },
34
+ { blankLine: "always", prev: "*", next: ["function", "class", "interface", "type"] },
35
+ { blankLine: "always", prev: ["function", "class", "interface", "type"], next: "*" },
36
+ {
37
+ blankLine: "always",
38
+ prev: "*",
39
+ next: ["multiline-const", "multiline-let", "multiline-var", "multiline-using"],
40
+ },
41
+ {
42
+ blankLine: "always",
43
+ prev: ["multiline-const", "multiline-let", "multiline-var", "multiline-using"],
44
+ next: "*",
29
45
  },
30
- next: {
31
- selector:
32
- ':matches(TSDeclareFunction, FunctionDeclaration, ExportNamedDeclaration[declaration.type="TSDeclareFunction"], ExportNamedDeclaration[declaration.type="FunctionDeclaration"])',
46
+ { blankLine: "always", prev: "*", next: ["return", "if", "switch", "try", "for", "while", "do"] },
47
+ { blankLine: "always", prev: "block-like", next: "*" },
48
+ { blankLine: "any", prev: "import", next: "import" },
49
+ // Consecutive single-line bindings form one group at any scope, including
50
+ // module scope. A binding with its own leading comment starts a new group.
51
+ { blankLine: "any", prev: "singleline-binding", next: "undocumented-singleline-binding" },
52
+ {
53
+ blankLine: "any",
54
+ prev: {
55
+ selector:
56
+ ':matches(TSDeclareFunction, ExportNamedDeclaration[declaration.type="TSDeclareFunction"])',
57
+ },
58
+ next: {
59
+ selector:
60
+ ':matches(TSDeclareFunction, FunctionDeclaration, ExportNamedDeclaration[declaration.type="TSDeclareFunction"], ExportNamedDeclaration[declaration.type="FunctionDeclaration"])',
61
+ },
33
62
  },
63
+ ],
64
+ {
65
+ "singleline-binding": singlelineBinding,
66
+ "undocumented-singleline-binding": undocumentedSinglelineBinding,
34
67
  },
35
- ]);
68
+ );
36
69
 
37
- /** Restore structural blank lines with whitespace-only fixes; keep local short bindings and overloads grouped. */
70
+ /** Restore structural blank lines with whitespace-only fixes; keep single-line binding groups and overloads grouped. */
38
71
  export const requireReadableSpacingRule: CreateRule = {
39
72
  ...paddingRule,
40
73
  meta: {
@@ -15,7 +15,7 @@ The rule is MIT-licensed. Keep `LICENSE` with every redistributed copy, includin
15
15
  - Replace upstream type aliases with Oxlint's ESTree, context, token/comment, and rule types. Upstream's token type includes comments; Oxlint exposes those separately.
16
16
  - Replace `AST_NODE_TYPES` enum members with identical string literals.
17
17
  - Guard indexed reads for consuming repositories with `noUncheckedIndexedAccess`. Impossible missing AST/configuration entries raise explicit invariant errors rather than introducing new non-null assertions.
18
- - Replace the repository-specific `createRule` factory with `createPaddingLineRule(options)`. The anti-slop wrapper supplies typed options directly; it exposes no user configuration options.
18
+ - Replace the repository-specific `createRule` factory with `createPaddingLineRule(options, statementTypes?)`. The anti-slop wrapper supplies typed options directly; it exposes no user configuration options. The optional second argument registers caller-defined statement types, consulted before the upstream table, so a policy can match syntax the upstream matchers do not distinguish (exported bindings, comment ownership) without editing the vendored tables.
19
19
  - Implement the small required AST helper surface in `padding-line-ast.ts` using Oxlint's public source-code/token API. `isParenthesized` only needs the one-pair check used to exclude parenthesized directive strings, not the upstream general-purpose overloads.
20
20
  - Retain the upstream statement matchers, scope tracking, comment-aware insertion/removal, selector support, and diagnostic text. Upstream naming and non-null assumptions remain localized here to keep future diffs reviewable; the file is not a model for new application code.
21
21
 
@@ -3,9 +3,9 @@ import type { ESTree, Context as RuleContext, SourceCode, Token as SyntaxToken,
3
3
  type ASTNode = ESTree.Node
4
4
  type Token = SyntaxToken | Comment
5
5
  import type {
6
- RuleOptions,
6
+ PaddingType,
7
7
  SelectorOption,
8
- StatementOption,
8
+ StatementMatcher,
9
9
  } from './padding-line-options.d.ts'
10
10
  import {
11
11
  isClosingBraceToken,
@@ -42,17 +42,32 @@ type NodeTest = (
42
42
  sourceCode: SourceCode,
43
43
  ) => boolean
44
44
 
45
- interface NodeTestObject {
45
+ export interface NodeTestObject {
46
46
  test: NodeTest
47
47
  }
48
48
 
49
+ /**
50
+ * A policy entry may name a caller-defined statement type (registered through
51
+ * `createPaddingLineRule`) anywhere the upstream statement types are accepted.
52
+ */
53
+ export type PolicyStatementOption<Extra extends string = never> =
54
+ | StatementMatcher
55
+ | Extra
56
+ | [StatementMatcher | Extra, ...(StatementMatcher | Extra)[]]
57
+
58
+ export type PolicyOptions<Extra extends string = never> = {
59
+ blankLine: PaddingType
60
+ prev: PolicyStatementOption<Extra>
61
+ next: PolicyStatementOption<Extra>
62
+ }[]
63
+
49
64
  const LT = `[${Array.from(LINEBREAKS).join('')}]`
50
65
  const PADDING_LINE_SEQUENCE = new RegExp(
51
66
  String.raw`^(\s*?${LT})\s*${LT}(\s*;?)$`,
52
67
  'u',
53
68
  )
54
69
 
55
- function isSelectorOption(option: StatementOption): option is SelectorOption {
70
+ function isSelectorOption(option: PolicyStatementOption<string>): option is SelectorOption {
56
71
  return typeof option === 'object' && !Array.isArray(option)
57
72
  }
58
73
 
@@ -589,8 +604,16 @@ const StatementTypes: Record<string, NodeTestObject> = {
589
604
  ),
590
605
  }
591
606
 
592
- /** Build the vendored padding rule with caller-owned, typed policy options. */
593
- export default function createPaddingLineRule(options: RuleOptions): CreateRule {
607
+ /**
608
+ * Build the vendored padding rule with caller-owned, typed policy options.
609
+ * `statementTypes` registers caller-defined statement types, looked up before
610
+ * the upstream table, for syntax the upstream matchers do not distinguish.
611
+ */
612
+ export default function createPaddingLineRule<Extra extends string = never>(
613
+ options: PolicyOptions<NoInfer<Extra>>,
614
+ statementTypes?: Record<Extra, NodeTestObject>,
615
+ ): CreateRule {
616
+ const customStatementTypes: Partial<Record<string, NodeTestObject>> = statementTypes ?? {}
594
617
  return {
595
618
  meta: {
596
619
  type: 'layout',
@@ -667,7 +690,7 @@ return {
667
690
  const selectorMatchedNodes = new Map<string, Set<ASTNode>>()
668
691
  const pendingPairs: { prevNode: ASTNode, nextNode: ASTNode }[] = []
669
692
 
670
- function collectSelectorOption(option: StatementOption): void {
693
+ function collectSelectorOption(option: PolicyStatementOption<Extra>): void {
671
694
  if (Array.isArray(option)) {
672
695
  for (const item of option)
673
696
  collectSelectorOption(item)
@@ -722,7 +745,7 @@ return {
722
745
  * @returns `true` if the statement node matched the type.
723
746
  * @private
724
747
  */
725
- function match(node: ASTNode, type: StatementOption): boolean {
748
+ function match(node: ASTNode, type: PolicyStatementOption<Extra>): boolean {
726
749
  let innerStatementNode = node
727
750
 
728
751
  while (innerStatementNode.type === 'LabeledStatement')
@@ -746,7 +769,7 @@ return {
746
769
  return true
747
770
  }
748
771
  else {
749
- const statementType = StatementTypes[type]
772
+ const statementType = customStatementTypes[type] ?? StatementTypes[type]
750
773
  if (statementType === undefined)
751
774
  throw new Error(`Padding rule invariant: unsupported statement type ${type}`)
752
775
  return statementType.test(innerStatementNode, sourceCode)