oxlint-plugin-arch 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.5
4
+
5
+ - Add `no-inline-types` to require named object and function types in function signatures, with separate parameter, return, and function-type options.
6
+ - Add `only-export-constants` to restrict exports to local `const` bindings, with opt-ins for function values, type exports, and re-exports.
7
+ - Add `no-member-comments` to remove comments on object/type members while preserving directives and optional `Why:` explanations.
8
+ - Document the new rule options and syntax-only limits, extend the full configuration example to all 30 rules, and sync the optional vendored skill assets.
9
+
3
10
  ## 0.2.4
4
11
 
5
12
  - Add `trailingRoles` and `roleSeparators` so `export-file-prefix`, `declaration-name`, and `no-extra-exports` can treat `onchain-utils.ts` like `onchain.utils.ts`.
package/README.md CHANGED
@@ -41,7 +41,7 @@ Then point `jsPlugins[].specifier` at the copied entry file instead of the packa
41
41
  ## Configuration examples
42
42
 
43
43
  - [`examples/minimal.oxlint.config.ts`](examples/minimal.oxlint.config.ts) — three small rules showing plugin registration, glob ownership, and options.
44
- - [`examples/full.oxlint.config.ts`](examples/full.oxlint.config.ts) — all 27 rules across representative component, action, service, route, API, schema, and database scopes.
44
+ - [`examples/full.oxlint.config.ts`](examples/full.oxlint.config.ts) — all 30 rules across representative component, action, service, constant, route, API, schema, and database scopes.
45
45
 
46
46
  Copy the shapes that match your repository; do not copy globs or naming policy blindly.
47
47
 
@@ -58,6 +58,7 @@ Copy the shapes that match your repository; do not copy globs or naming policy b
58
58
  - `no-file-level-helpers` — keep unapproved helpers out of module scope.
59
59
  - `no-imported-type-alias` — reject exported aliases that only rename imported types.
60
60
  - `no-inline-schema-elements` — require named schemas inside configured combinators.
61
+ - `no-inline-types` — require named object and function types in parameter and return annotations. See [options and scope](#no-inline-types).
61
62
  - `no-local-schema-construction` — reject runtime schema-library imports and local schema construction, with consumer-chosen severity, messages, and path exceptions.
62
63
  - `no-rederive-schema` — reject type derivation from configured imported schemas.
63
64
  - `no-restricted-files` — reject files selected by a consumer-owned forbidden glob.
@@ -67,8 +68,10 @@ Copy the shapes that match your repository; do not copy globs or naming policy b
67
68
  - `no-top-level-functions` — reject top-level functions and optional re-exports.
68
69
  - `no-trivial-functions` — reject empty or passthrough top-level functions.
69
70
  - `no-type-declarations` — reject type aliases and interfaces in matched files.
71
+ - `no-member-comments` — remove leading and same-line trailing comments on object-type members: interface properties and methods, index/call/construct signatures, type-literal members (including inline function parameter types), object literal properties, and object spreads. Autofix removes the comment and any blank line that followed it, consumes the spacing in front of a trailing comment so no line is left with trailing whitespace, and never removes the member. A contiguous run of comments is reported once and removed by a single fix, so `--fix` converges in one pass. `allowWhy` defaults to `true`: comments beginning with `Why: ` (after whitespace/JSDoc decoration), including contiguous `//` continuation lines, are preserved. Set `{ allowWhy: false }` to ban those too. The rule checks the prefix, not whether the rationale is meaningful. Directive comments are always preserved, whatever `allowWhy` says: `@ts-ignore`, `@ts-expect-error`, `@ts-nocheck`, `eslint-`/`oxlint-disable`/`enable`, `prettier-ignore`, `biome-ignore`, `c8`/`v8`/`istanbul ignore`, and webpack magic comments. Declaration, function-body, class-field, enum-member, union-member, and plain function-parameter comments are out of scope, as are comments inside destructuring patterns and array literals. Enable with `'arch/no-member-comments': ['error', { allowWhy: true }]` in a `**/*.{ts,tsx}` override.
70
72
  - `no-unescaped-like` — require configured sanitizers for configured query methods.
71
73
  - `only-export-components` — allow only React component and type exports.
74
+ - `only-export-constants` — allow only local `const` exports, with opt-ins for function values, types, and re-exports. See [options and scope](#only-export-constants).
72
75
  - `require-file-factory` — derive and require a filename-based factory function.
73
76
  - `require-object-params` — require exported functions to use object-shaped parameters.
74
77
  - `require-orpc-output` — require named oRPC output schemas.
@@ -77,6 +80,62 @@ Copy the shapes that match your repository; do not copy globs or naming policy b
77
80
 
78
81
  Every rule file includes plain-English behavior and examples. Rule tests under `src/tests/` show complete option shapes.
79
82
 
83
+ ### `no-inline-types`
84
+
85
+ Require named types in function signatures, including local functions, callbacks, methods, constructors, and overloads. Checks explicit annotations only; inferred types, variable annotations, generic constraints, and named type/interface definitions are not changed or banned. Use `no-type-declarations` separately to control where named types may live.
86
+
87
+ | Option | Default | Behavior |
88
+ | --- | --- | --- |
89
+ | `parameters` | `true` | Check parameter annotations, including destructuring, defaults, rest parameters, and constructor parameter properties. |
90
+ | `returns` | `true` | Check explicit return annotations. |
91
+ | `functionTypes` | `true` | Also reject inline function and constructor types. When `false`, object types nested inside those annotations are still checked. |
92
+
93
+ Object/function types nested in arrays, tuples, unions, intersections, or generic arguments are checked too (`Promise<{ id: string }>` fails). A rejected outer type is reported once, not again for each nested member.
94
+
95
+ ```ts
96
+ // In the consumer's override for service files:
97
+ 'arch/no-inline-types': ['error', {
98
+ parameters: true,
99
+ returns: true,
100
+ functionTypes: true,
101
+ }]
102
+
103
+ // Pass
104
+ function save(input: SaveInput): SaveResult { return persist(input) }
105
+ // Fail
106
+ function save(input: { id: string }): { ok: boolean } { return persist(input) }
107
+ ```
108
+
109
+ ### `only-export-constants`
110
+
111
+ Require every runtime export to reference a local `const` binding. Supports direct exports, destructuring, renamed exports, and default exports of local constants. Non-exported declarations are unrestricted.
112
+
113
+ | Option | Default | Behavior |
114
+ | --- | --- | --- |
115
+ | `allowFunctionValues` | `false` | Allow function-valued `const` bindings, not exported function declarations. |
116
+ | `allowTypeExports` | `false` | Allow type aliases, interfaces, and type-only exports, including type-only re-exports. |
117
+ | `allowReExports` | `false` | Allow runtime re-exports (`export ... from`, `export *`) and exports of imported bindings. Their declaration kind cannot be verified locally. |
118
+
119
+ `let`, `var`, function/class/enum/namespace declarations, class-valued constants, bare default expressions, and TypeScript `export =` / `export as namespace` are rejected. Name the default value with a local `const` first. Type-only re-exports use `allowTypeExports`, independently of `allowReExports`.
120
+
121
+ This is a syntax rule, not a deep-immutability or runtime-type check. It recognizes direct function/class values, TypeScript expression wrappers, and local identifier aliases. It does not infer values returned by calls, imported values, member accesses, destructuring, or object contents. Objects and arrays do not need `as const` or `Object.freeze()`.
122
+
123
+ ```ts
124
+ // In the consumer's override for constant files:
125
+ 'arch/only-export-constants': ['error', {
126
+ allowFunctionValues: false,
127
+ allowTypeExports: false,
128
+ allowReExports: false,
129
+ }]
130
+
131
+ // Pass
132
+ const retryLimit = 3
133
+ export { retryLimit }
134
+ // Fail
135
+ export let retryCount = 0
136
+ export const retry = () => run()
137
+ ```
138
+
80
139
  ## Development
81
140
 
82
141
  ```bash
package/dist/index.js CHANGED
@@ -181,7 +181,7 @@ function declarationsIsTrivialFunction(node) {
181
181
  if (!node.body || node.body.type !== "BlockStatement") {
182
182
  return false;
183
183
  }
184
- const statements = node.body.body.filter((statement2) => statement2.type !== "EmptyStatement" && !(statement2.type === "ExpressionStatement" && statement2.directive));
184
+ const statements = node.body.body.filter((statement) => statement.type !== "EmptyStatement" && !(statement.type === "ExpressionStatement" && statement.directive));
185
185
  if (statements.length === 0) {
186
186
  return true;
187
187
  }
@@ -743,19 +743,19 @@ var declarationName = defineRule({
743
743
  createOnce(context) {
744
744
  return {
745
745
  Program(program) {
746
- const options2 = optionsFirst(context, {
746
+ const options = optionsFirst(context, {
747
747
  stem: "before-first-dot",
748
748
  normalize: "remove-separators"
749
749
  });
750
- const allowed = options2.allowPattern ? new RegExp(options2.allowPattern) : null;
751
- const expected = options2.pattern ? new RegExp(options2.pattern, options2.flags) : null;
750
+ const allowed = options.allowPattern ? new RegExp(options.allowPattern) : null;
751
+ const expected = options.pattern ? new RegExp(options.pattern, options.flags) : null;
752
752
  const basename = namingFileBasename(context.filename).replace(/\.(tsx?|jsx?)$/, "");
753
- const stem = namingFileStem(basename, options2.stem, options2.trailingRoles, options2.roleSeparators);
754
- const prefixes = namingFilePrefixes(stem, options2.normalize ?? "remove-separators", options2.singularize ?? "none");
753
+ const stem = namingFileStem(basename, options.stem, options.trailingRoles, options.roleSeparators);
754
+ const prefixes = namingFilePrefixes(stem, options.normalize ?? "remove-separators", options.singularize ?? "none");
755
755
  const seen = new Set;
756
- for (const item of declarationsCollectNamed(program, options2.kinds)) {
756
+ for (const item of declarationsCollectNamed(program, options.kinds)) {
757
757
  const key = `${item.name}:${item.node.start}:${item.node.end}`;
758
- const comparableName = options2.normalize === "none" ? item.name : item.name.replaceAll("_", "").toLowerCase();
758
+ const comparableName = options.normalize === "none" ? item.name : item.name.replaceAll("_", "").toLowerCase();
759
759
  const matches = expected ? expected.test(item.name) : prefixes.some((prefix) => comparableName.startsWith(prefix));
760
760
  if (seen.has(key) || allowed?.test(item.name) || matches) {
761
761
  continue;
@@ -764,7 +764,7 @@ var declarationName = defineRule({
764
764
  context.report({
765
765
  node: item.node,
766
766
  messageId: expected ? "pattern" : "prefix",
767
- data: expected ? { name: item.name, pattern: options2.pattern ?? "" } : { name: item.name, prefix: prefixes.join(" or ") }
767
+ data: expected ? { name: item.name, pattern: options.pattern ?? "" } : { name: item.name, prefix: prefixes.join(" or ") }
768
768
  });
769
769
  }
770
770
  }
@@ -797,22 +797,22 @@ var exportFilePrefix = defineRule2({
797
797
  createOnce(context) {
798
798
  return {
799
799
  Program(program) {
800
- const options2 = optionsFirst(context, {
800
+ const options = optionsFirst(context, {
801
801
  stem: "before-first-dot",
802
802
  normalize: "remove-separators"
803
803
  });
804
804
  const basename = namingFileBasename(context.filename).replace(/\.(tsx?|jsx?)$/, "");
805
- const stem = namingFileStem(basename, options2.stem, options2.trailingRoles, options2.roleSeparators);
806
- const prefixes = namingFilePrefixes(stem, options2.normalize ?? "remove-separators", options2.singularize ?? "none");
807
- const allowed = options2.allowPattern ? new RegExp(options2.allowPattern) : null;
808
- const names = options2.allDeclarations ? declarationsCollectNamed(program) : exportsCollect(program).map((binding) => ({
805
+ const stem = namingFileStem(basename, options.stem, options.trailingRoles, options.roleSeparators);
806
+ const prefixes = namingFilePrefixes(stem, options.normalize ?? "remove-separators", options.singularize ?? "none");
807
+ const allowed = options.allowPattern ? new RegExp(options.allowPattern) : null;
808
+ const names = options.allDeclarations ? declarationsCollectNamed(program) : exportsCollect(program).map((binding) => ({
809
809
  name: binding.localName ?? binding.exportedName,
810
810
  node: binding.node
811
811
  }));
812
812
  const seen = new Set;
813
813
  for (const item of names) {
814
814
  const key = `${item.name}:${item.node.start}:${item.node.end}`;
815
- const comparableName = options2.normalize === "none" ? item.name : item.name.replaceAll("_", "").toLowerCase();
815
+ const comparableName = options.normalize === "none" ? item.name : item.name.replaceAll("_", "").toLowerCase();
816
816
  if (seen.has(key) || allowed?.test(item.name) || prefixes.some((prefix) => comparableName.startsWith(prefix))) {
817
817
  continue;
818
818
  }
@@ -1049,12 +1049,12 @@ var noExtraExports = defineRule7({
1049
1049
  createOnce(context) {
1050
1050
  return {
1051
1051
  Program(program) {
1052
- const options2 = optionsFirst(context);
1052
+ const options = optionsFirst(context);
1053
1053
  const basename = namingFileBasename(context.filename).replace(/\.(tsx?|jsx?)$/, "");
1054
- const domain = namingFileStem(basename, options2.domainStem, options2.trailingRoles, options2.roleSeparators);
1055
- const allowed = options2.names.map((template) => template.replaceAll("{Domain}", namingPascalCase(domain)).replaceAll("{domain}", namingCamelCase(domain)));
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
1056
  for (const binding of exportsCollect(program)) {
1057
- if (options2.allowTypeExports && binding.typeOnly) {
1057
+ if (options.allowTypeExports && binding.typeOnly) {
1058
1058
  continue;
1059
1059
  }
1060
1060
  const name = binding.localName ?? binding.exportedName;
@@ -1244,14 +1244,14 @@ var noInlineSchemaElements = defineRule11({
1244
1244
  if (node.callee.type !== "MemberExpression") {
1245
1245
  return;
1246
1246
  }
1247
- const options2 = optionsFirst(context);
1248
- const namespaces = new Set(options2.namespaces);
1247
+ const options = optionsFirst(context);
1248
+ const namespaces = new Set(options.namespaces);
1249
1249
  const method = astStaticMemberName(node.callee);
1250
- if (!schemasIsCall(node, namespaces) || !method || !options2.methods.includes(method)) {
1250
+ if (!schemasIsCall(node, namespaces) || !method || !options.methods.includes(method)) {
1251
1251
  return;
1252
1252
  }
1253
1253
  for (const element of schemasElements(node)) {
1254
- if (!schemasIsAllowedElement(element, options2.allowZodScalars, namespaces, new Set(options2.structuralMethods))) {
1254
+ if (!schemasIsAllowedElement(element, options.allowZodScalars, namespaces, new Set(options.structuralMethods))) {
1255
1255
  context.report({
1256
1256
  node: element,
1257
1257
  messageId: "inline",
@@ -1264,14 +1264,76 @@ var noInlineSchemaElements = defineRule11({
1264
1264
  }
1265
1265
  });
1266
1266
 
1267
- // src/rules/no-local-schema-construction.ts
1267
+ // src/rules/no-inline-types.ts
1268
1268
  import { defineRule as defineRule12 } from "@oxlint/plugins";
1269
+ function parameterAnnotation(parameter) {
1270
+ if (parameter.type === "TSParameterProperty") {
1271
+ return parameterAnnotation(parameter.parameter);
1272
+ }
1273
+ if (parameter.type === "AssignmentPattern") {
1274
+ return parameterAnnotation(parameter.left);
1275
+ }
1276
+ return parameter.typeAnnotation;
1277
+ }
1278
+ var noInlineTypes = defineRule12({
1279
+ meta: {
1280
+ type: "problem",
1281
+ schema: [{
1282
+ type: "object",
1283
+ additionalProperties: false,
1284
+ properties: {
1285
+ parameters: { type: "boolean" },
1286
+ returns: { type: "boolean" },
1287
+ functionTypes: { type: "boolean" }
1288
+ }
1289
+ }],
1290
+ messages: {
1291
+ inlineType: "Use a named type instead of an inline {{kind}} type in this {{position}} annotation."
1292
+ }
1293
+ },
1294
+ createOnce(context) {
1295
+ 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
+ });
1309
+ });
1310
+ }
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
+ }
1325
+ };
1326
+ }
1327
+ });
1328
+
1329
+ // src/rules/no-local-schema-construction.ts
1330
+ import { defineRule as defineRule13 } from "@oxlint/plugins";
1269
1331
  var defaultRuntimeImportMessage = "Do not import runtime schema constructors from {{source}}; import a named schema instead.";
1270
1332
  var defaultConstructionMessage = "Do not construct schemas locally; import a named schema instead.";
1271
1333
  function formatMessage(template, data) {
1272
1334
  return Object.entries(data).reduce((result, [key, value]) => result.replaceAll(`{{${key}}}`, value), template);
1273
1335
  }
1274
- var noLocalSchemaConstruction = defineRule12({
1336
+ var noLocalSchemaConstruction = defineRule13({
1275
1337
  meta: {
1276
1338
  type: "problem",
1277
1339
  schema: [{
@@ -1382,9 +1444,105 @@ var noLocalSchemaConstruction = defineRule12({
1382
1444
  }
1383
1445
  });
1384
1446
 
1447
+ // 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({
1451
+ meta: {
1452
+ type: "suggestion",
1453
+ fixable: "code",
1454
+ schema: [{
1455
+ type: "object",
1456
+ additionalProperties: false,
1457
+ properties: {
1458
+ allowWhy: { type: "boolean", default: true }
1459
+ }
1460
+ }],
1461
+ messages: {
1462
+ memberComment: "Remove this comment from the type member."
1463
+ }
1464
+ },
1465
+ createOnce(context) {
1466
+ const reported = new Set;
1467
+ function checkMember(node) {
1468
+ const parent = node.parent;
1469
+ if (parent.type !== "TSInterfaceBody" && parent.type !== "TSTypeLiteral" && parent.type !== "ObjectExpression")
1470
+ return;
1471
+ const source = context.sourceCode;
1472
+ const { allowWhy = true } = optionsFirst(context, {});
1473
+ const previousToken = source.getTokenBefore(node);
1474
+ const comments = [
1475
+ ...source.getCommentsBefore(node).filter((comment) => !previousToken || previousToken.range[0] === parent.range[0] || previousToken.loc.end.line !== comment.loc.start.line),
1476
+ ...source.getCommentsAfter(node).filter((comment) => comment.loc.start.line === node.loc.end.line)
1477
+ ].filter((comment) => comment.range[0] > parent.range[0] && comment.range[1] < parent.range[1]);
1478
+ let whyContinuation = false;
1479
+ let previousLine = -1;
1480
+ const removable = [];
1481
+ 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;
1484
+ whyContinuation = comment.type === "Line" && isWhy;
1485
+ previousLine = comment.loc.end.line;
1486
+ if (reported.has(comment.range[0]))
1487
+ continue;
1488
+ reported.add(comment.range[0]);
1489
+ if (allowWhy && isWhy)
1490
+ continue;
1491
+ if (DIRECTIVE.test(value))
1492
+ continue;
1493
+ removable.push(comment);
1494
+ }
1495
+ for (let index = 0;index < removable.length; ) {
1496
+ let last = index;
1497
+ while (last + 1 < removable.length && removable[last + 1].loc.start.line === removable[last].loc.end.line + 1)
1498
+ last++;
1499
+ const first = removable[index];
1500
+ const final = removable[last];
1501
+ index = last + 1;
1502
+ context.report({
1503
+ loc: { start: first.loc.start, end: final.loc.end },
1504
+ messageId: "memberComment",
1505
+ fix(fixer) {
1506
+ const text = source.text;
1507
+ const lineStart = text.lastIndexOf(`
1508
+ `, first.range[0] - 1) + 1;
1509
+ const before = text.slice(lineStart, first.range[0]);
1510
+ const start = first.range[0] - (/[\t ]*$/.exec(before)?.[0].length ?? 0);
1511
+ const after = text.slice(final.range[1]);
1512
+ const trailingLines = after.match(/^[\t ]*\r?\n(?:[\t ]*\r?\n)*/);
1513
+ if (trailingLines) {
1514
+ const end = final.range[1] + trailingLines[0].length;
1515
+ if (/^[\t ]*$/.test(before))
1516
+ return fixer.removeRange([lineStart, end]);
1517
+ const newline = trailingLines[0].includes(`\r
1518
+ `) ? `\r
1519
+ ` : `
1520
+ `;
1521
+ return fixer.replaceTextRange([start, end], newline);
1522
+ }
1523
+ return fixer.removeRange([start, final.range[1]]);
1524
+ }
1525
+ });
1526
+ }
1527
+ }
1528
+ return {
1529
+ Program() {
1530
+ reported.clear();
1531
+ },
1532
+ TSPropertySignature: checkMember,
1533
+ TSMethodSignature: checkMember,
1534
+ TSIndexSignature: checkMember,
1535
+ TSCallSignatureDeclaration: checkMember,
1536
+ TSConstructSignatureDeclaration: checkMember,
1537
+ Property: checkMember,
1538
+ SpreadElement: checkMember
1539
+ };
1540
+ }
1541
+ });
1542
+
1385
1543
  // src/rules/no-rederive-schema.ts
1386
- import { defineRule as defineRule13 } from "@oxlint/plugins";
1387
- var noRederiveSchema = defineRule13({
1544
+ import { defineRule as defineRule15 } from "@oxlint/plugins";
1545
+ var noRederiveSchema = defineRule15({
1388
1546
  meta: {
1389
1547
  type: "problem",
1390
1548
  schema: [{
@@ -1429,8 +1587,8 @@ var noRederiveSchema = defineRule13({
1429
1587
  });
1430
1588
 
1431
1589
  // src/rules/no-restricted-files.ts
1432
- import { defineRule as defineRule14 } from "@oxlint/plugins";
1433
- var noRestrictedFiles = defineRule14({
1590
+ import { defineRule as defineRule16 } from "@oxlint/plugins";
1591
+ var noRestrictedFiles = defineRule16({
1434
1592
  meta: {
1435
1593
  type: "problem",
1436
1594
  schema: [{
@@ -1460,7 +1618,7 @@ var noRestrictedFiles = defineRule14({
1460
1618
  });
1461
1619
 
1462
1620
  // src/rules/no-restricted-token.ts
1463
- import { defineRule as defineRule15 } from "@oxlint/plugins";
1621
+ import { defineRule as defineRule17 } from "@oxlint/plugins";
1464
1622
  function tokenIsTypeOnlyImport(node) {
1465
1623
  const parent = node.parent;
1466
1624
  if (parent?.type === "ImportSpecifier") {
@@ -1468,7 +1626,7 @@ function tokenIsTypeOnlyImport(node) {
1468
1626
  }
1469
1627
  return (parent?.type === "ImportDefaultSpecifier" || parent?.type === "ImportNamespaceSpecifier") && parent.parent.type === "ImportDeclaration" && parent.parent.importKind === "type";
1470
1628
  }
1471
- var noRestrictedToken = defineRule15({
1629
+ var noRestrictedToken = defineRule17({
1472
1630
  meta: {
1473
1631
  type: "problem",
1474
1632
  schema: [{
@@ -1488,9 +1646,9 @@ var noRestrictedToken = defineRule15({
1488
1646
  createOnce(context) {
1489
1647
  return {
1490
1648
  before() {
1491
- const options2 = optionsFirst(context);
1649
+ const options = optionsFirst(context);
1492
1650
  const filename = namingPosixPath(context.filename);
1493
- const allowed = options2.allowIn.some((suffix) => filename.endsWith(suffix)) || (options2.allowPathPatterns ?? []).some((pattern) => new RegExp(pattern).test(filename));
1651
+ const allowed = options.allowIn.some((suffix) => filename.endsWith(suffix)) || (options.allowPathPatterns ?? []).some((pattern) => new RegExp(pattern).test(filename));
1494
1652
  if (allowed) {
1495
1653
  return false;
1496
1654
  }
@@ -1510,8 +1668,8 @@ var noRestrictedToken = defineRule15({
1510
1668
  });
1511
1669
 
1512
1670
  // src/rules/no-runtime-in-types.ts
1513
- import { defineRule as defineRule16 } from "@oxlint/plugins";
1514
- var noRuntimeInTypes = defineRule16({
1671
+ import { defineRule as defineRule18 } from "@oxlint/plugins";
1672
+ var noRuntimeInTypes = defineRule18({
1515
1673
  meta: {
1516
1674
  type: "problem",
1517
1675
  schema: [{
@@ -1561,7 +1719,7 @@ var noRuntimeInTypes = defineRule16({
1561
1719
  });
1562
1720
 
1563
1721
  // src/rules/no-single-use-scalar-schema.ts
1564
- import { defineRule as defineRule17 } from "@oxlint/plugins";
1722
+ import { defineRule as defineRule19 } from "@oxlint/plugins";
1565
1723
  function isScalarSchema(node, namespaces, structuralMethods) {
1566
1724
  if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") {
1567
1725
  return false;
@@ -1583,8 +1741,8 @@ function isDirectObjectField(identifier, ancestors, namespaces, structuralMethod
1583
1741
  const method = call.callee.type === "MemberExpression" ? astStaticMemberName(call.callee) : undefined;
1584
1742
  return Boolean(method === "object" && structuralMethods.has(method) && call.arguments.includes(object));
1585
1743
  }
1586
- function isAllowedStructuralElement(identifier, initializer, ancestors, options2, namespaces, structuralMethods) {
1587
- if (!options2.allowZodScalars || !schemasIsAllowedElement(initializer, true, namespaces, structuralMethods)) {
1744
+ function isAllowedStructuralElement(identifier, initializer, ancestors, options, namespaces, structuralMethods) {
1745
+ if (!options.allowZodScalars || !schemasIsAllowedElement(initializer, true, namespaces, structuralMethods)) {
1588
1746
  return false;
1589
1747
  }
1590
1748
  const parent = ancestors.at(-1);
@@ -1593,9 +1751,9 @@ function isAllowedStructuralElement(identifier, initializer, ancestors, options2
1593
1751
  return false;
1594
1752
  }
1595
1753
  const method = call.callee.type === "MemberExpression" ? astStaticMemberName(call.callee) : undefined;
1596
- return Boolean(method && options2.methods.includes(method) && schemasElements(call).includes(identifier));
1754
+ return Boolean(method && options.methods.includes(method) && schemasElements(call).includes(identifier));
1597
1755
  }
1598
- var noSingleUseScalarSchema = defineRule17({
1756
+ var noSingleUseScalarSchema = defineRule19({
1599
1757
  meta: {
1600
1758
  type: "suggestion",
1601
1759
  schema: [{
@@ -1630,9 +1788,9 @@ var noSingleUseScalarSchema = defineRule17({
1630
1788
  }
1631
1789
  },
1632
1790
  "Program:exit"(program) {
1633
- const options2 = optionsFirst(context);
1634
- const namespaces = new Set(options2.namespaces);
1635
- const structuralMethods = new Set(options2.structuralMethods);
1791
+ const options = optionsFirst(context);
1792
+ const namespaces = new Set(options.namespaces);
1793
+ const structuralMethods = new Set(options.structuralMethods);
1636
1794
  const exportedNames = new Set(exportsCollect(program).flatMap((binding) => binding.localName ? [binding.localName] : []));
1637
1795
  for (const declarator of candidates) {
1638
1796
  const name = declarator.id.type === "Identifier" ? declarator.id.name : undefined;
@@ -1641,14 +1799,14 @@ var noSingleUseScalarSchema = defineRule17({
1641
1799
  continue;
1642
1800
  }
1643
1801
  const variable = context.sourceCode.getDeclaredVariables(declarator).find((candidate) => candidate.name === name);
1644
- const references = variable?.references.filter((reference2) => reference2.isRead()) ?? [];
1802
+ const references = variable?.references.filter((reference) => reference.isRead()) ?? [];
1645
1803
  if (references.length !== 1) {
1646
1804
  continue;
1647
1805
  }
1648
1806
  const reference = references[0];
1649
1807
  const identifier = reference.identifier;
1650
1808
  const ancestors = context.sourceCode.getAncestors(identifier);
1651
- const inlineable = isDirectObjectField(identifier, ancestors, namespaces, structuralMethods) || isAllowedStructuralElement(identifier, initializer, ancestors, options2, namespaces, structuralMethods);
1809
+ const inlineable = isDirectObjectField(identifier, ancestors, namespaces, structuralMethods) || isAllowedStructuralElement(identifier, initializer, ancestors, options, namespaces, structuralMethods);
1652
1810
  if (inlineable) {
1653
1811
  context.report({
1654
1812
  node: declarator.id,
@@ -1663,8 +1821,8 @@ var noSingleUseScalarSchema = defineRule17({
1663
1821
  });
1664
1822
 
1665
1823
  // src/rules/no-top-level-functions.ts
1666
- import { defineRule as defineRule18 } from "@oxlint/plugins";
1667
- var noTopLevelFunctions = defineRule18({
1824
+ import { defineRule as defineRule20 } from "@oxlint/plugins";
1825
+ var noTopLevelFunctions = defineRule20({
1668
1826
  meta: {
1669
1827
  type: "problem",
1670
1828
  schema: [{
@@ -1721,8 +1879,8 @@ var noTopLevelFunctions = defineRule18({
1721
1879
  });
1722
1880
 
1723
1881
  // src/rules/no-trivial-functions.ts
1724
- import { defineRule as defineRule19 } from "@oxlint/plugins";
1725
- var noTrivialFunctions = defineRule19({
1882
+ import { defineRule as defineRule21 } from "@oxlint/plugins";
1883
+ var noTrivialFunctions = defineRule21({
1726
1884
  meta: {
1727
1885
  type: "problem",
1728
1886
  schema: [{
@@ -1760,8 +1918,8 @@ var noTrivialFunctions = defineRule19({
1760
1918
  });
1761
1919
 
1762
1920
  // src/rules/no-type-declarations.ts
1763
- import { defineRule as defineRule20 } from "@oxlint/plugins";
1764
- var noTypeDeclarations = defineRule20({
1921
+ import { defineRule as defineRule22 } from "@oxlint/plugins";
1922
+ var noTypeDeclarations = defineRule22({
1765
1923
  meta: {
1766
1924
  type: "problem",
1767
1925
  schema: [{
@@ -1796,8 +1954,8 @@ var noTypeDeclarations = defineRule20({
1796
1954
  });
1797
1955
 
1798
1956
  // src/rules/no-unescaped-like.ts
1799
- import { defineRule as defineRule21 } from "@oxlint/plugins";
1800
- var noUnescapedLike = defineRule21({
1957
+ import { defineRule as defineRule23 } from "@oxlint/plugins";
1958
+ var noUnescapedLike = defineRule23({
1801
1959
  meta: {
1802
1960
  type: "problem",
1803
1961
  schema: [{
@@ -1816,21 +1974,21 @@ var noUnescapedLike = defineRule21({
1816
1974
  let sanitized = new Set;
1817
1975
  return {
1818
1976
  Program(program) {
1819
- const { sanitizers: sanitizers2, allowSanitizedBindings = true } = optionsFirst(context);
1820
- sanitized = allowSanitizedBindings ? sanitizersConfiguredBindings(program, new Set(sanitizers2)) : new Set;
1977
+ const { sanitizers, allowSanitizedBindings = true } = optionsFirst(context);
1978
+ sanitized = allowSanitizedBindings ? sanitizersConfiguredBindings(program, new Set(sanitizers)) : new Set;
1821
1979
  },
1822
1980
  CallExpression(node) {
1823
1981
  if (node.callee.type !== "MemberExpression") {
1824
1982
  return;
1825
1983
  }
1826
- const { methods, sanitizers: sanitizers2 } = optionsFirst(context);
1984
+ const { methods, sanitizers } = optionsFirst(context);
1827
1985
  const method = astStaticMemberName(node.callee);
1828
1986
  if (!method || !methods.includes(method)) {
1829
1987
  return;
1830
1988
  }
1831
1989
  const value = node.arguments.at(-1);
1832
1990
  const allowedIdentifier = value?.type === "Identifier" && sanitized.has(value.name);
1833
- if (!sanitizersCallsConfigured(value, new Set(sanitizers2)) && !allowedIdentifier) {
1991
+ if (!sanitizersCallsConfigured(value, new Set(sanitizers)) && !allowedIdentifier) {
1834
1992
  context.report({
1835
1993
  node: value ?? node,
1836
1994
  messageId: "unescaped",
@@ -1843,8 +2001,8 @@ var noUnescapedLike = defineRule21({
1843
2001
  });
1844
2002
 
1845
2003
  // src/rules/only-export-components.ts
1846
- import { defineRule as defineRule22 } from "@oxlint/plugins";
1847
- var onlyExportComponents = defineRule22({
2004
+ import { defineRule as defineRule24 } from "@oxlint/plugins";
2005
+ var onlyExportComponents = defineRule24({
1848
2006
  meta: {
1849
2007
  type: "problem",
1850
2008
  schema: [{
@@ -1899,9 +2057,146 @@ var onlyExportComponents = defineRule22({
1899
2057
  }
1900
2058
  });
1901
2059
 
2060
+ // src/rules/only-export-constants.ts
2061
+ import { defineRule as defineRule25 } from "@oxlint/plugins";
2062
+ function bindingNames(pattern) {
2063
+ switch (pattern.type) {
2064
+ case "Identifier":
2065
+ return [pattern.name];
2066
+ case "AssignmentPattern":
2067
+ return bindingNames(pattern.left);
2068
+ case "RestElement":
2069
+ return bindingNames(pattern.argument);
2070
+ case "ArrayPattern":
2071
+ return pattern.elements.flatMap((element) => element ? bindingNames(element) : []);
2072
+ case "ObjectPattern":
2073
+ return pattern.properties.flatMap((property) => bindingNames(property.type === "RestElement" ? property : property.value));
2074
+ }
2075
+ }
2076
+ function moduleName(node) {
2077
+ return node.type === "Identifier" ? node.name : node.value;
2078
+ }
2079
+ function unwrap(expression) {
2080
+ if (expression.type === "ParenthesizedExpression" || expression.type === "TSAsExpression" || expression.type === "TSSatisfiesExpression" || expression.type === "TSTypeAssertion" || expression.type === "TSNonNullExpression" || expression.type === "TSInstantiationExpression") {
2081
+ return unwrap(expression.expression);
2082
+ }
2083
+ return expression;
2084
+ }
2085
+ function collectBindings(program) {
2086
+ const bindings = new Map;
2087
+ for (const statement of program.body) {
2088
+ if (statement.type === "ImportDeclaration") {
2089
+ for (const specifier of statement.specifiers) {
2090
+ bindings.set(specifier.local.name, {
2091
+ kind: statement.importKind === "type" || specifier.type === "ImportSpecifier" && specifier.importKind === "type" ? "type" : "import"
2092
+ });
2093
+ }
2094
+ continue;
2095
+ }
2096
+ const declaration = statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" ? statement.declaration : statement;
2097
+ if (declaration?.type === "TSImportEqualsDeclaration") {
2098
+ bindings.set(declaration.id.name, { kind: declaration.importKind === "type" ? "type" : "import" });
2099
+ } else if (declaration?.type === "VariableDeclaration") {
2100
+ for (const item of declaration.declarations) {
2101
+ for (const name of bindingNames(item.id)) {
2102
+ bindings.set(name, {
2103
+ kind: declaration.kind === "const" ? "const" : "other",
2104
+ value: item.id.type === "Identifier" ? item.init : undefined
2105
+ });
2106
+ }
2107
+ }
2108
+ } else if (declaration && "id" in declaration && declaration.id?.type === "Identifier") {
2109
+ bindings.set(declaration.id.name, {
2110
+ kind: declaration.type === "TSTypeAliasDeclaration" || declaration.type === "TSInterfaceDeclaration" ? "type" : declaration.type === "FunctionDeclaration" || declaration.type === "TSDeclareFunction" ? "function" : declaration.type === "ClassDeclaration" ? "class" : "other"
2111
+ });
2112
+ }
2113
+ }
2114
+ return bindings;
2115
+ }
2116
+ var onlyExportConstants = defineRule25({
2117
+ meta: {
2118
+ type: "problem",
2119
+ schema: [{
2120
+ type: "object",
2121
+ additionalProperties: false,
2122
+ properties: {
2123
+ allowFunctionValues: { type: "boolean" },
2124
+ allowTypeExports: { type: "boolean" },
2125
+ allowReExports: { type: "boolean" }
2126
+ }
2127
+ }],
2128
+ messages: {
2129
+ nonConstant: "Export '{{name}}' must be a local const binding with an allowed value; types and re-exports require explicit options."
2130
+ }
2131
+ },
2132
+ createOnce(context) {
2133
+ return {
2134
+ Program(program) {
2135
+ const { allowFunctionValues = false, allowTypeExports = false, allowReExports = false } = optionsFirst(context, {});
2136
+ const bindings = collectBindings(program);
2137
+ function hasForbiddenValue(binding, seen = new Set) {
2138
+ if (seen.has(binding) || binding.kind === "class") {
2139
+ return true;
2140
+ }
2141
+ if (binding.kind === "function") {
2142
+ return !allowFunctionValues;
2143
+ }
2144
+ if (!binding.value) {
2145
+ return false;
2146
+ }
2147
+ seen.add(binding);
2148
+ const value = unwrap(binding.value);
2149
+ if (value.type === "ArrowFunctionExpression" || value.type === "FunctionExpression") {
2150
+ return !allowFunctionValues;
2151
+ }
2152
+ if (value.type === "ClassExpression") {
2153
+ return true;
2154
+ }
2155
+ const target = value.type === "Identifier" ? bindings.get(value.name) : undefined;
2156
+ return target ? hasForbiddenValue(target, seen) : false;
2157
+ }
2158
+ function check(node, name, localName, typeOnly = false, reExport = false) {
2159
+ const binding = localName ? bindings.get(localName) : undefined;
2160
+ const allowed = typeOnly || !reExport && binding?.kind === "type" ? allowTypeExports : reExport || binding?.kind === "import" ? allowReExports : binding?.kind === "const" && !hasForbiddenValue(binding);
2161
+ if (!allowed) {
2162
+ context.report({ node, messageId: "nonConstant", data: { name } });
2163
+ }
2164
+ }
2165
+ for (const statement of program.body) {
2166
+ if (statement.type === "ExportAllDeclaration") {
2167
+ check(statement, "*", undefined, statement.exportKind === "type", true);
2168
+ } else if (statement.type === "ExportDefaultDeclaration") {
2169
+ const declaration = statement.declaration;
2170
+ check(statement, "default", declaration.type === "Identifier" ? declaration.name : undefined, declaration.type === "TSInterfaceDeclaration");
2171
+ } else if (statement.type === "TSExportAssignment") {
2172
+ check(statement, "export =");
2173
+ } else if (statement.type === "TSNamespaceExportDeclaration") {
2174
+ check(statement, statement.id.name);
2175
+ } else if (statement.type === "ExportNamedDeclaration") {
2176
+ const declaration = statement.declaration;
2177
+ if (declaration?.type === "VariableDeclaration") {
2178
+ for (const item of declaration.declarations) {
2179
+ for (const name of bindingNames(item.id)) {
2180
+ check(item, name, name);
2181
+ }
2182
+ }
2183
+ } else if (declaration) {
2184
+ const name = "id" in declaration && declaration.id?.type === "Identifier" ? declaration.id.name : "<anonymous>";
2185
+ check(declaration, name, name);
2186
+ }
2187
+ for (const specifier of statement.specifiers) {
2188
+ check(specifier, moduleName(specifier.exported), moduleName(specifier.local), statement.exportKind === "type" || specifier.exportKind === "type", Boolean(statement.source));
2189
+ }
2190
+ }
2191
+ }
2192
+ }
2193
+ };
2194
+ }
2195
+ });
2196
+
1902
2197
  // src/rules/require-file-factory.ts
1903
- import { defineRule as defineRule23 } from "@oxlint/plugins";
1904
- var requireFileFactory = defineRule23({
2198
+ import { defineRule as defineRule26 } from "@oxlint/plugins";
2199
+ var requireFileFactory = defineRule26({
1905
2200
  meta: {
1906
2201
  type: "problem",
1907
2202
  schema: [{
@@ -1947,8 +2242,8 @@ var requireFileFactory = defineRule23({
1947
2242
  });
1948
2243
 
1949
2244
  // src/rules/require-object-params.ts
1950
- import { defineRule as defineRule24 } from "@oxlint/plugins";
1951
- var requireObjectParams = defineRule24({
2245
+ import { defineRule as defineRule27 } from "@oxlint/plugins";
2246
+ var requireObjectParams = defineRule27({
1952
2247
  meta: {
1953
2248
  type: "problem",
1954
2249
  schema: [],
@@ -1974,8 +2269,8 @@ var requireObjectParams = defineRule24({
1974
2269
  });
1975
2270
 
1976
2271
  // src/rules/require-orpc-output.ts
1977
- import { defineRule as defineRule25 } from "@oxlint/plugins";
1978
- var requireOrpcOutput = defineRule25({
2272
+ import { defineRule as defineRule28 } from "@oxlint/plugins";
2273
+ var requireOrpcOutput = defineRule28({
1979
2274
  meta: {
1980
2275
  type: "problem",
1981
2276
  schema: [{
@@ -2017,8 +2312,8 @@ var requireOrpcOutput = defineRule25({
2017
2312
  });
2018
2313
 
2019
2314
  // src/rules/require-paired-call.ts
2020
- import { defineRule as defineRule26 } from "@oxlint/plugins";
2021
- var requirePairedCall = defineRule26({
2315
+ import { defineRule as defineRule29 } from "@oxlint/plugins";
2316
+ var requirePairedCall = defineRule29({
2022
2317
  meta: {
2023
2318
  type: "problem",
2024
2319
  schema: [{
@@ -2073,8 +2368,8 @@ var requirePairedCall = defineRule26({
2073
2368
  });
2074
2369
 
2075
2370
  // src/rules/route-surface.ts
2076
- import { defineRule as defineRule27 } from "@oxlint/plugins";
2077
- var routeSurface = defineRule27({
2371
+ import { defineRule as defineRule30 } from "@oxlint/plugins";
2372
+ var routeSurface = defineRule30({
2078
2373
  meta: {
2079
2374
  type: "problem",
2080
2375
  schema: [{
@@ -2130,7 +2425,9 @@ var src_default = eslintCompatPlugin({
2130
2425
  "no-file-level-helpers": noFileLevelHelpers,
2131
2426
  "no-imported-type-alias": noImportedTypeAlias,
2132
2427
  "no-inline-schema-elements": noInlineSchemaElements,
2428
+ "no-inline-types": noInlineTypes,
2133
2429
  "no-local-schema-construction": noLocalSchemaConstruction,
2430
+ "no-member-comments": noMemberComments,
2134
2431
  "no-rederive-schema": noRederiveSchema,
2135
2432
  "no-restricted-files": noRestrictedFiles,
2136
2433
  "no-restricted-token": noRestrictedToken,
@@ -2141,6 +2438,7 @@ var src_default = eslintCompatPlugin({
2141
2438
  "no-type-declarations": noTypeDeclarations,
2142
2439
  "no-unescaped-like": noUnescapedLike,
2143
2440
  "only-export-components": onlyExportComponents,
2441
+ "only-export-constants": onlyExportConstants,
2144
2442
  "require-file-factory": requireFileFactory,
2145
2443
  "require-object-params": requireObjectParams,
2146
2444
  "require-orpc-output": requireOrpcOutput,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-arch",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Configurable architecture rules for Oxlint JavaScript plugins.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",