@timmo001/oxlint-rules 0.6.2 → 0.6.4

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
@@ -7,19 +7,19 @@ locally owned generic rules under `timmo` and Effect rules under
7
7
 
8
8
  This package targets the latest stable Oxlint toolchain. Oxlint's
9
9
  [JavaScript plugin API is alpha](https://oxc.rs/docs/guide/usage/linter/config.html)
10
- and is outside its semver guarantees, so each rules release declares the exact
11
- `oxlint` and `@oxlint/plugins` versions it has validated. Older toolchains and
12
- untested future versions are not part of the support contract.
10
+ and is outside its semver guarantees. Each rules release is validated against
11
+ the Oxlint version pinned in its development dependencies, and declares a caret
12
+ peer range from that version so consumers can take newer Oxlint releases
13
+ without waiting for a rules release. Older toolchains are not supported.
13
14
 
14
15
  ## Install
15
16
 
16
- Check the published rules package's `peerDependencies` against the official npm
17
- `latest` versions of `oxlint` and `@oxlint/plugins`. If they differ, a validated
18
- rules release is needed before upgrading. Do not bypass peer checks or override
19
- Vite Plus's bundled dependencies to make an unsupported version fit.
17
+ Check that the official npm `latest` versions of `oxlint` and `@oxlint/plugins`
18
+ satisfy the published rules package's `peerDependencies`. Do not bypass peer
19
+ checks or override Vite Plus's bundled dependencies to make an unsupported
20
+ version fit.
20
21
 
21
- Once the versions match, install from npm with the package manager already used
22
- by the repository:
22
+ Install from npm with the package manager already used by the repository:
23
23
 
24
24
  ```sh
25
25
  bun add --dev --exact @timmo001/oxlint-rules oxlint @oxlint/plugins
@@ -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`
@@ -181,24 +182,25 @@ the surrounding test code is linted with the full rule set.
181
182
  isolated consumer using the exact toolchain pins from `package.json` and strict
182
183
  peer checks. It verifies both recommended configs, diagnostics from all four
183
184
  shared namespaces, and valid and invalid type-aware examples. CI runs this
184
- through the existing package workflow's build step. It also requires matching,
185
- exact Oxlint development and peer versions.
185
+ through the existing package workflow's build step. It also requires exact
186
+ Oxlint development versions that satisfy the caret peer ranges.
186
187
 
187
188
  ### Toolchain updates and releases
188
189
 
189
- Renovate's shared preset groups Oxlint, `@oxlint/plugins` and `oxlint-tsgolint`.
190
- The local rule keeps both development and peer dependencies pinned instead of
191
- widening peer ranges. For each update:
190
+ Renovate's shared preset groups Oxlint, `@oxlint/plugins` and `oxlint-tsgolint`
191
+ and widens peer ranges. The local rule keeps the development dependencies
192
+ pinned. For each update:
192
193
 
193
194
  1. Verify the official npm `latest` versions and Oxlint's tsgolint peer
194
195
  requirement. Keep Oxlint and plugins on the same latest version and regenerate
195
196
  the lockfile with the selected tsgolint version.
196
197
  2. Run the checks and package dry-runs above. Fix any API incompatibility before
197
198
  accepting the new pins; do not disable rules or bypass peer failures.
198
- 3. Review and merge the validated update. Consumers need a separately approved
199
- version bump and release to receive the new peer contract. Publishing a
200
- GitHub release triggers the existing npm and JSR workflows; both must succeed
201
- before the update is available to consumers.
199
+ 3. Review and merge the validated update. Consumers already in the peer range
200
+ don't need a rules release; raise the peer floor and release only when the
201
+ rules need a newer Oxlint. Publishing a GitHub release triggers the existing
202
+ npm and JSR workflows; both must succeed before the update is available to
203
+ consumers.
202
204
 
203
205
  Advance `vendor/anti-slop` only by updating its Git submodule commit. Keep its
204
206
  source and MIT licence unchanged, then run the full package checks so both
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.4",
4
4
  "description": "Shared Oxlint plugins and configs with optional Effect rules.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -49,14 +49,14 @@
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.91.0",
53
+ "oxlint": "^1.91.0"
54
54
  },
55
55
  "devDependencies": {
56
- "@oxlint/plugins": "1.87.0",
56
+ "@oxlint/plugins": "1.91.0",
57
57
  "@types/bun": "^1.3.14",
58
- "oxlint": "1.87.0",
59
- "oxlint-tsgolint": "7.0.2004",
58
+ "oxlint": "1.91.0",
59
+ "oxlint-tsgolint": "7.0.2005",
60
60
  "prettier": "^3.9.5",
61
61
  "skills-ref": "0.1.5",
62
62
  "typescript": "^7.0.0"
@@ -15,11 +15,11 @@ description: >-
15
15
  the user names another target.
16
16
  3. Preserve existing ignores, overrides, plugins, and repository-owned rules.
17
17
  Verify the official npm `latest` versions of `oxlint` and `@oxlint/plugins`.
18
- They must match each other and the exact peers of the selected rules release.
19
- Support is latest-only: if the published peer contract has not caught up,
20
- report the pending rules release instead of widening peers or bypassing
21
- package-manager failures. Do not force upgrades of Vite Plus's transitive
22
- dependencies or assume its bundled plugins satisfy this contract.
18
+ They must match each other and satisfy the peer ranges of the selected rules
19
+ release. If they fall outside those ranges, report the pending rules release
20
+ instead of bypassing package-manager failures. Do not force upgrades of Vite
21
+ Plus's transitive dependencies or assume its bundled plugins satisfy this
22
+ contract.
23
23
 
24
24
  ## Package
25
25
 
@@ -18,12 +18,15 @@ description: >-
18
18
  minor, patch, or exact version. Ask only when the release level is missing.
19
19
  Do not add a `v` prefix when the repository's existing tags omit it.
20
20
  3. Verify official npm `latest` metadata for `oxlint`, `@oxlint/plugins` and
21
- `oxlint-tsgolint`. Support is latest-only: keep Oxlint and plugins on the same
22
- latest version, pin their development and peer dependencies exactly, and
23
- select the latest tsgolint satisfying Oxlint's peer requirement. Regenerate
24
- `bun.lock`. The shared Renovate preset groups this toolchain; the local rule
25
- keeps peers pinned. Resolve compatibility failures before releasing, without
26
- widening peers, disabling rules or forcing transitive dependency upgrades.
21
+ `oxlint-tsgolint`. Keep Oxlint and plugins on the same latest version, pinned
22
+ exactly in development dependencies, and select the latest tsgolint
23
+ satisfying Oxlint's peer requirement. Declare the peers as a caret range
24
+ (`^1.91.0`) so consumers can move to newer Oxlint releases without waiting
25
+ for a release here; raise its floor only when the rules need a newer Oxlint.
26
+ Regenerate `bun.lock`. The shared Renovate preset groups this toolchain and
27
+ widens peer ranges; the local rule pins only the development dependencies.
28
+ Resolve compatibility failures before releasing, without disabling rules or
29
+ forcing transitive dependency upgrades.
27
30
  4. Update the version in `package.json` and `jsr.json` only when a version bump
28
31
  is requested. Before committing, run `mise run check`, `mise run build`,
29
32
  `mise run test:package`, `npm pack --dry-run`, and
@@ -45,6 +48,4 @@ description: >-
45
48
  succeed.
46
49
  8. Report the release URL, version commit, validation results, and npm and JSR
47
50
  publication conclusions. Do not report the release as complete while either
48
- publication job is pending or failed. A merged toolchain update alone does
49
- not deliver its peer contract to consumers; preparation ends with publication
50
- pending until a version bump and release are approved.
51
+ publication job is pending or failed.
@@ -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)