@timmo001/oxlint-rules 0.6.2 → 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
@@ -139,8 +139,9 @@ they only run when the consumer enables `typeAware`.
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
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", ...])`. Use the
143
- package's typed operations, and add one to effect-gh when a command has none.
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: {
@@ -2669,7 +2669,8 @@ var StatementTypes = {
2669
2669
  ]
2670
2670
  ]))
2671
2671
  };
2672
- function createPaddingLineRule(options) {
2672
+ function createPaddingLineRule(options, statementTypes) {
2673
+ const customStatementTypes = statementTypes ?? {};
2673
2674
  return {
2674
2675
  meta: {
2675
2676
  type: "layout",
@@ -2785,7 +2786,7 @@ function createPaddingLineRule(options) {
2785
2786
  return !isSingleLine(innerStatementNode);
2786
2787
  return true;
2787
2788
  } else {
2788
- const statementType = StatementTypes[type];
2789
+ const statementType = customStatementTypes[type] ?? StatementTypes[type];
2789
2790
  if (statementType === undefined)
2790
2791
  throw new Error(`Padding rule invariant: unsupported statement type ${type}`);
2791
2792
  return statementType.test(innerStatementNode, sourceCode);
@@ -2884,6 +2885,20 @@ function createPaddingLineRule(options) {
2884
2885
  }
2885
2886
 
2886
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
+ };
2887
2902
  var paddingRule = createPaddingLineRule([
2888
2903
  { blankLine: "always", prev: "import", next: "*" },
2889
2904
  { blankLine: "always", prev: "*", next: { selector: "Program > :not(ImportDeclaration)" } },
@@ -2903,6 +2918,7 @@ var paddingRule = createPaddingLineRule([
2903
2918
  { blankLine: "always", prev: "*", next: ["return", "if", "switch", "try", "for", "while", "do"] },
2904
2919
  { blankLine: "always", prev: "block-like", next: "*" },
2905
2920
  { blankLine: "any", prev: "import", next: "import" },
2921
+ { blankLine: "any", prev: "singleline-binding", next: "undocumented-singleline-binding" },
2906
2922
  {
2907
2923
  blankLine: "any",
2908
2924
  prev: {
@@ -2912,7 +2928,10 @@ var paddingRule = createPaddingLineRule([
2912
2928
  selector: ':matches(TSDeclareFunction, FunctionDeclaration, ExportNamedDeclaration[declaration.type="TSDeclareFunction"], ExportNamedDeclaration[declaration.type="FunctionDeclaration"])'
2913
2929
  }
2914
2930
  }
2915
- ]);
2931
+ ], {
2932
+ "singleline-binding": singlelineBinding,
2933
+ "undocumented-singleline-binding": undocumentedSinglelineBinding
2934
+ });
2916
2935
  var requireReadableSpacingRule = {
2917
2936
  ...paddingRule,
2918
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: {
@@ -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.2",
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",
@@ -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)