oxlint-plugin-arch 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +101 -0
  4. package/package.json +64 -0
  5. package/src/index.ts +57 -0
  6. package/src/rules/export-file-prefix.ts +70 -0
  7. package/src/rules/export-name-pattern.ts +71 -0
  8. package/src/rules/filename-export-name.ts +86 -0
  9. package/src/rules/filename-match.ts +47 -0
  10. package/src/rules/folder-prefix.ts +73 -0
  11. package/src/rules/no-extra-exports.ts +60 -0
  12. package/src/rules/no-extra-factory-keys.ts +58 -0
  13. package/src/rules/no-file-level-helpers.ts +58 -0
  14. package/src/rules/no-imported-type-alias.ts +83 -0
  15. package/src/rules/no-inline-schema-elements.ts +70 -0
  16. package/src/rules/no-local-schema-construction.ts +162 -0
  17. package/src/rules/no-rederive-schema.ts +66 -0
  18. package/src/rules/no-restricted-files.ts +40 -0
  19. package/src/rules/no-restricted-token.ts +71 -0
  20. package/src/rules/no-runtime-in-types.ts +72 -0
  21. package/src/rules/no-single-use-scalar-schema.ts +164 -0
  22. package/src/rules/no-top-level-functions.ts +77 -0
  23. package/src/rules/no-trivial-functions.ts +56 -0
  24. package/src/rules/no-unescaped-like.ts +67 -0
  25. package/src/rules/only-export-components.ts +78 -0
  26. package/src/rules/require-file-factory.ts +67 -0
  27. package/src/rules/require-object-params.ts +32 -0
  28. package/src/rules/require-orpc-output.ts +64 -0
  29. package/src/rules/require-paired-call.ts +71 -0
  30. package/src/rules/route-surface.ts +62 -0
  31. package/src/utils/helpers/ast.ts +101 -0
  32. package/src/utils/helpers/declarations.ts +203 -0
  33. package/src/utils/helpers/exports.ts +158 -0
  34. package/src/utils/helpers/factories.ts +67 -0
  35. package/src/utils/helpers/index.ts +11 -0
  36. package/src/utils/helpers/naming.ts +89 -0
  37. package/src/utils/helpers/options.ts +5 -0
  38. package/src/utils/helpers/orpc.ts +31 -0
  39. package/src/utils/helpers/react-components.ts +146 -0
  40. package/src/utils/helpers/routes.ts +31 -0
  41. package/src/utils/helpers/sanitizers.ts +33 -0
  42. package/src/utils/helpers/schemas.ts +82 -0
  43. package/src/utils/index.ts +1 -0
@@ -0,0 +1,58 @@
1
+ import { defineRule } from '@oxlint/plugins'
2
+ import {
3
+ factoriesCollectTopLevel,
4
+ factoriesDirectlyReturnedObjects,
5
+ factoriesObjectPropertyName,
6
+ optionsFirst,
7
+ } from '../utils/index.ts'
8
+
9
+ interface NoExtraFactoryKeysOptions {
10
+ keys: string[]
11
+ factoryPattern?: string
12
+ }
13
+
14
+ /**
15
+ * Checks direct object returns from selected top-level factories and rejects keys outside the configured allow-list.
16
+ *
17
+ * Example: With `keys: [run]`, `return { run }` passes while `return { run, preview }` fails.
18
+ */
19
+ export const noExtraFactoryKeys = defineRule({
20
+ meta: {
21
+ type: 'problem',
22
+ schema: [{
23
+ type: 'object',
24
+ additionalProperties: false,
25
+ properties: {
26
+ keys: { type: 'array', items: { type: 'string' } },
27
+ factoryPattern: { type: 'string' },
28
+ },
29
+ required: ['keys'],
30
+ }],
31
+ messages: {
32
+ extraKey: "Factory '{{factory}}' must not return key '{{key}}'; allowed keys: {{allowed}}.",
33
+ },
34
+ },
35
+ createOnce(context) {
36
+ return {
37
+ Program(program) {
38
+ const { keys, factoryPattern = '^make[A-Z]' } = optionsFirst<NoExtraFactoryKeysOptions>(context)
39
+
40
+ for (const factory of factoriesCollectTopLevel(program, new RegExp(factoryPattern))) {
41
+ for (const object of factoriesDirectlyReturnedObjects(factory)) {
42
+ for (const property of object.properties) {
43
+ const key = factoriesObjectPropertyName(property)
44
+
45
+ if (!key || !keys.includes(key)) {
46
+ context.report({
47
+ node: property,
48
+ messageId: 'extraKey',
49
+ data: { factory: factory.name, key: key ?? '<computed or spread>', allowed: keys.join(', ') },
50
+ })
51
+ }
52
+ }
53
+ }
54
+ }
55
+ },
56
+ }
57
+ },
58
+ })
@@ -0,0 +1,58 @@
1
+ import { defineRule } from '@oxlint/plugins'
2
+ import { declarationsFileLevelFunctionCandidates, optionsFirst, reactComponentsIsLike } from '../utils/index.ts'
3
+
4
+ interface NoFileLevelHelpersOptions {
5
+ allowPattern?: string
6
+ detectComponents?: boolean
7
+ hookPattern?: string
8
+ }
9
+
10
+ /**
11
+ * Rejects top-level functions unless they are recognized components, hooks, or names allowed by configuration.
12
+ *
13
+ * Example: `useDialog` can pass as a hook, while a file-level `buildPayload` helper fails.
14
+ */
15
+ export const noFileLevelHelpers = defineRule({
16
+ meta: {
17
+ type: 'problem',
18
+ schema: [{
19
+ type: 'object',
20
+ additionalProperties: false,
21
+ properties: {
22
+ allowPattern: { type: 'string' },
23
+ detectComponents: { type: 'boolean' },
24
+ hookPattern: { type: 'string' },
25
+ },
26
+ }],
27
+ messages: {
28
+ helper: 'File-level helper {{name}} must move inside its owning component or hook.',
29
+ },
30
+ },
31
+ createOnce(context) {
32
+ return {
33
+ Program(program) {
34
+ const {
35
+ allowPattern,
36
+ detectComponents = true,
37
+ hookPattern = '^use[A-Z]',
38
+ } = optionsFirst<NoFileLevelHelpersOptions>(context, {})
39
+ const allowed = allowPattern ? new RegExp(allowPattern) : null
40
+ const hooks = new RegExp(hookPattern)
41
+
42
+ for (const candidate of declarationsFileLevelFunctionCandidates(program)) {
43
+ if ((detectComponents && reactComponentsIsLike(program, candidate))
44
+ || hooks.test(candidate.name)
45
+ || allowed?.test(candidate.name)) {
46
+ continue
47
+ }
48
+
49
+ context.report({
50
+ node: candidate.node,
51
+ messageId: 'helper',
52
+ data: { name: candidate.name },
53
+ })
54
+ }
55
+ },
56
+ }
57
+ },
58
+ })
@@ -0,0 +1,83 @@
1
+ import { defineRule } from '@oxlint/plugins'
2
+ import type { ESTree } from '@oxlint/plugins'
3
+
4
+ function importedAliasTarget(declaration: ESTree.Declaration | null, imports: Set<string>): string | undefined {
5
+ if (declaration?.type !== 'TSTypeAliasDeclaration'
6
+ || declaration.typeAnnotation.type !== 'TSTypeReference'
7
+ || declaration.typeAnnotation.typeName.type !== 'Identifier'
8
+ || !imports.has(declaration.typeAnnotation.typeName.name)) {
9
+ return undefined
10
+ }
11
+
12
+ return declaration.typeAnnotation.typeName.name
13
+ }
14
+
15
+ function specifierLocalName(specifier: ESTree.ExportSpecifier): string {
16
+ return specifier.local.type === 'Identifier' ? specifier.local.name : specifier.local.value
17
+ }
18
+
19
+ /**
20
+ * Rejects exported type aliases that merely rename an imported type without adding information.
21
+ *
22
+ * Example: `export type User = UserDto` fails when `UserDto` is imported; intersections and unions pass.
23
+ */
24
+ export const noImportedTypeAlias = defineRule({
25
+ meta: {
26
+ type: 'problem',
27
+ schema: [],
28
+ messages: {
29
+ alias: "Exported type '{{alias}}' must not re-alias imported type '{{name}}'.",
30
+ },
31
+ },
32
+ createOnce(context) {
33
+ return {
34
+ Program(program) {
35
+ const imports = new Set(program.body.flatMap((statement) => statement.type === 'ImportDeclaration'
36
+ ? statement.specifiers.map((specifier) => specifier.local.name)
37
+ : []))
38
+ const aliases = new Map<string, string>()
39
+
40
+ for (const statement of program.body) {
41
+ const declaration = statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration'
42
+ ? statement.declaration
43
+ : statement
44
+ const imported = declaration && declaration.type.endsWith('Declaration')
45
+ ? importedAliasTarget(declaration as ESTree.Declaration, imports)
46
+ : undefined
47
+
48
+ if (declaration?.type === 'TSTypeAliasDeclaration' && imported) {
49
+ aliases.set(declaration.id.name, imported)
50
+ }
51
+ }
52
+
53
+ for (const statement of program.body) {
54
+ if (statement.type !== 'ExportNamedDeclaration') {
55
+ continue
56
+ }
57
+
58
+ const inline = importedAliasTarget(statement.declaration, imports)
59
+ if (inline && statement.declaration?.type === 'TSTypeAliasDeclaration') {
60
+ context.report({
61
+ node: statement.declaration,
62
+ messageId: 'alias',
63
+ data: { alias: statement.declaration.id.name, name: inline },
64
+ })
65
+ }
66
+
67
+ for (const specifier of statement.specifiers) {
68
+ const local = specifierLocalName(specifier)
69
+ const imported = aliases.get(local)
70
+
71
+ if (imported) {
72
+ context.report({
73
+ node: specifier,
74
+ messageId: 'alias',
75
+ data: { alias: local, name: imported },
76
+ })
77
+ }
78
+ }
79
+ }
80
+ },
81
+ }
82
+ },
83
+ })
@@ -0,0 +1,70 @@
1
+ import { defineRule } from '@oxlint/plugins'
2
+ import {
3
+ astStaticMemberName,
4
+ optionsFirst,
5
+ schemasElements,
6
+ schemasIsAllowedElement,
7
+ schemasIsCall,
8
+ } from '../utils/index.ts'
9
+
10
+ interface NoInlineSchemaElementsOptions {
11
+ namespaces: string[]
12
+ methods: string[]
13
+ structuralMethods: string[]
14
+ allowZodScalars: boolean
15
+ }
16
+
17
+ /**
18
+ * Requires configured schema combinators to reference named schemas instead of defining structural schemas inline.
19
+ *
20
+ * Example: `z.array(userSchema)` passes; `z.array(z.object({ id: z.string() }))` fails.
21
+ */
22
+ export const noInlineSchemaElements = defineRule({
23
+ meta: {
24
+ type: 'problem',
25
+ schema: [{
26
+ type: 'object',
27
+ additionalProperties: false,
28
+ properties: {
29
+ namespaces: { type: 'array', items: { type: 'string' } },
30
+ methods: { type: 'array', items: { type: 'string' } },
31
+ structuralMethods: { type: 'array', items: { type: 'string' } },
32
+ allowZodScalars: { type: 'boolean' },
33
+ },
34
+ }],
35
+ defaultOptions: [{
36
+ namespaces: ['z'],
37
+ methods: ['array', 'union', 'record', 'tuple'],
38
+ structuralMethods: ['array', 'discriminatedUnion', 'intersection', 'lazy', 'object', 'record', 'tuple', 'union'],
39
+ allowZodScalars: true,
40
+ }],
41
+ messages: { inline: 'z.{{method}}() elements must use named schemas.' },
42
+ },
43
+ createOnce(context) {
44
+ return {
45
+ CallExpression(node) {
46
+ if (node.callee.type !== 'MemberExpression') {
47
+ return
48
+ }
49
+
50
+ const options = optionsFirst<NoInlineSchemaElementsOptions>(context)
51
+ const namespaces = new Set(options.namespaces)
52
+ const method = astStaticMemberName(node.callee)
53
+
54
+ if (!schemasIsCall(node, namespaces) || !method || !options.methods.includes(method)) {
55
+ return
56
+ }
57
+
58
+ for (const element of schemasElements(node)) {
59
+ if (!schemasIsAllowedElement(element, options.allowZodScalars, namespaces, new Set(options.structuralMethods))) {
60
+ context.report({
61
+ node: element,
62
+ messageId: 'inline',
63
+ data: { method },
64
+ })
65
+ }
66
+ }
67
+ },
68
+ }
69
+ },
70
+ })
@@ -0,0 +1,162 @@
1
+ import { defineRule } from '@oxlint/plugins'
2
+ import {
3
+ namingPosixPath,
4
+ optionsFirst,
5
+ schemasIsConstruction,
6
+ schemasIsRuntimeImport,
7
+ schemasIsRuntimeReexport,
8
+ schemasRuntimeImportLocals,
9
+ } from '../utils/index.ts'
10
+
11
+ interface NoLocalSchemaConstructionOptions {
12
+ packages?: string[]
13
+ namespaces?: string[]
14
+ allowIn?: string[]
15
+ allowPathPatterns?: string[]
16
+ message?: string
17
+ runtimeImportMessage?: string
18
+ constructionMessage?: string
19
+ }
20
+
21
+ const defaultRuntimeImportMessage = 'Do not import runtime schema constructors from {{source}}; import a named schema instead.'
22
+ const defaultConstructionMessage = 'Do not construct schemas locally; import a named schema instead.'
23
+
24
+ function formatMessage(template: string, data: Record<string, string>): string {
25
+ return Object.entries(data).reduce((result, [key, value]) => result.replaceAll(`{{${key}}}`, value), template)
26
+ }
27
+
28
+ /**
29
+ * Rejects runtime schema-library imports and local schema construction, while allowing imported named schemas.
30
+ *
31
+ * Example: `schemaResolver(userSchema)` passes after importing `userSchema`; `import { z } from 'zod'` and `z.object({})` fail.
32
+ */
33
+ export const noLocalSchemaConstruction = defineRule({
34
+ meta: {
35
+ type: 'problem',
36
+ schema: [{
37
+ type: 'object',
38
+ additionalProperties: false,
39
+ properties: {
40
+ packages: { type: 'array', items: { type: 'string' } },
41
+ namespaces: { type: 'array', items: { type: 'string' } },
42
+ allowIn: { type: 'array', items: { type: 'string' } },
43
+ allowPathPatterns: { type: 'array', items: { type: 'string' } },
44
+ message: { type: 'string' },
45
+ runtimeImportMessage: { type: 'string' },
46
+ constructionMessage: { type: 'string' },
47
+ },
48
+ }],
49
+ defaultOptions: [{
50
+ packages: ['zod'],
51
+ namespaces: ['z'],
52
+ }],
53
+ messages: {
54
+ runtimeImport: '{{message}}',
55
+ construction: '{{message}}',
56
+ },
57
+ },
58
+ createOnce(context) {
59
+ let constructors = new Set<string>()
60
+
61
+ return {
62
+ before() {
63
+ const {
64
+ namespaces = ['z'],
65
+ allowIn = [],
66
+ allowPathPatterns = [],
67
+ } = optionsFirst<NoLocalSchemaConstructionOptions>(context, {})
68
+ const filename = namingPosixPath(context.filename)
69
+ constructors = new Set(namespaces)
70
+
71
+ if (allowIn.some((suffix) => filename.endsWith(suffix))
72
+ || allowPathPatterns.some((pattern) => new RegExp(pattern).test(filename))) {
73
+ return false
74
+ }
75
+ },
76
+ ImportDeclaration(node) {
77
+ const {
78
+ packages = ['zod'],
79
+ message,
80
+ runtimeImportMessage,
81
+ } = optionsFirst<NoLocalSchemaConstructionOptions>(context, {})
82
+
83
+ if (!schemasIsRuntimeImport(node, new Set(packages))) {
84
+ return
85
+ }
86
+
87
+ for (const name of schemasRuntimeImportLocals(node)) {
88
+ constructors.add(name)
89
+ }
90
+
91
+ context.report({
92
+ node,
93
+ messageId: 'runtimeImport',
94
+ data: {
95
+ message: formatMessage(
96
+ runtimeImportMessage ?? message ?? defaultRuntimeImportMessage,
97
+ { source: node.source.value },
98
+ ),
99
+ },
100
+ })
101
+ },
102
+ ExportNamedDeclaration(node) {
103
+ const {
104
+ packages = ['zod'],
105
+ message,
106
+ runtimeImportMessage,
107
+ } = optionsFirst<NoLocalSchemaConstructionOptions>(context, {})
108
+
109
+ if (!schemasIsRuntimeReexport(node, new Set(packages)) || !node.source) {
110
+ return
111
+ }
112
+
113
+ context.report({
114
+ node,
115
+ messageId: 'runtimeImport',
116
+ data: {
117
+ message: formatMessage(
118
+ runtimeImportMessage ?? message ?? defaultRuntimeImportMessage,
119
+ { source: node.source.value },
120
+ ),
121
+ },
122
+ })
123
+ },
124
+ ExportAllDeclaration(node) {
125
+ const {
126
+ packages = ['zod'],
127
+ message,
128
+ runtimeImportMessage,
129
+ } = optionsFirst<NoLocalSchemaConstructionOptions>(context, {})
130
+
131
+ if (!schemasIsRuntimeReexport(node, new Set(packages))) {
132
+ return
133
+ }
134
+
135
+ context.report({
136
+ node,
137
+ messageId: 'runtimeImport',
138
+ data: {
139
+ message: formatMessage(
140
+ runtimeImportMessage ?? message ?? defaultRuntimeImportMessage,
141
+ { source: node.source.value },
142
+ ),
143
+ },
144
+ })
145
+ },
146
+ CallExpression(node) {
147
+ if (!schemasIsConstruction(node, constructors)) {
148
+ return
149
+ }
150
+
151
+ const { message, constructionMessage } = optionsFirst<NoLocalSchemaConstructionOptions>(context, {})
152
+ context.report({
153
+ node,
154
+ messageId: 'construction',
155
+ data: {
156
+ message: formatMessage(constructionMessage ?? message ?? defaultConstructionMessage, {}),
157
+ },
158
+ })
159
+ },
160
+ }
161
+ },
162
+ })
@@ -0,0 +1,66 @@
1
+ import { defineRule } from '@oxlint/plugins'
2
+ import { optionsFirst, schemasTypeOperator } from '../utils/index.ts'
3
+
4
+ interface NoRederiveSchemaOptions {
5
+ from: string[]
6
+ namespaces?: string[]
7
+ operators?: string[]
8
+ }
9
+
10
+ /**
11
+ * Prevents deriving types from configured imported schemas when the package should export and consume a named type.
12
+ *
13
+ * Example: `z.infer<typeof importedSchema>` fails; deriving from a same-file schema passes.
14
+ */
15
+ export const noRederiveSchema = defineRule({
16
+ meta: {
17
+ type: 'problem',
18
+ schema: [{
19
+ type: 'object',
20
+ additionalProperties: false,
21
+ properties: {
22
+ from: { type: 'array', items: { type: 'string' } },
23
+ namespaces: { type: 'array', items: { type: 'string' } },
24
+ operators: { type: 'array', items: { type: 'string' } },
25
+ },
26
+ required: ['from'],
27
+ }],
28
+ messages: {
29
+ rederive: 'Do not derive {{operator}} from imported schema {{schema}}; import its DTO type.',
30
+ },
31
+ },
32
+ createOnce(context) {
33
+ let imported = new Set<string>()
34
+
35
+ return {
36
+ Program(program) {
37
+ const { from } = optionsFirst<NoRederiveSchemaOptions>(context)
38
+ imported = new Set(program.body.flatMap((statement) => statement.type === 'ImportDeclaration'
39
+ && from.includes(statement.source.value)
40
+ ? statement.specifiers.map((specifier) => specifier.local.name)
41
+ : []))
42
+ },
43
+ TSTypeReference(node) {
44
+ const {
45
+ namespaces = ['z'],
46
+ operators = ['infer', 'input'],
47
+ } = optionsFirst<NoRederiveSchemaOptions>(context)
48
+ const operator = schemasTypeOperator(node, new Set(namespaces), new Set(operators))
49
+ const parameter = node.typeArguments?.params[0]
50
+
51
+ if (!operator
52
+ || parameter?.type !== 'TSTypeQuery'
53
+ || parameter.exprName.type !== 'Identifier'
54
+ || !imported.has(parameter.exprName.name)) {
55
+ return
56
+ }
57
+
58
+ context.report({
59
+ node,
60
+ messageId: 'rederive',
61
+ data: { operator, schema: parameter.exprName.name },
62
+ })
63
+ },
64
+ }
65
+ },
66
+ })
@@ -0,0 +1,40 @@
1
+ import { defineRule } from '@oxlint/plugins'
2
+ import { optionsFirst } from '../utils/index.ts'
3
+
4
+ interface NoRestrictedFilesOptions {
5
+ message: string
6
+ }
7
+
8
+ /**
9
+ * Reports every file matched by the consumer glob, making forbidden file locations or extensions lintable.
10
+ *
11
+ * Example: Enable it for hook TSX files to require hooks to use `.ts` files.
12
+ */
13
+ export const noRestrictedFiles = defineRule({
14
+ meta: {
15
+ type: 'problem',
16
+ schema: [{
17
+ type: 'object',
18
+ additionalProperties: false,
19
+ properties: {
20
+ message: { type: 'string' },
21
+ },
22
+ required: ['message'],
23
+ }],
24
+ messages: {
25
+ restricted: '{{message}}',
26
+ },
27
+ },
28
+ createOnce(context) {
29
+ return {
30
+ Program(program) {
31
+ const { message } = optionsFirst<NoRestrictedFilesOptions>(context)
32
+ context.report({
33
+ node: program,
34
+ messageId: 'restricted',
35
+ data: { message },
36
+ })
37
+ },
38
+ }
39
+ },
40
+ })
@@ -0,0 +1,71 @@
1
+ import { defineRule } from '@oxlint/plugins'
2
+ import { namingPosixPath, optionsFirst } from '../utils/index.ts'
3
+ import type { ESTree } from '@oxlint/plugins'
4
+
5
+ interface NoRestrictedTokenOptions {
6
+ token: string
7
+ allowIn: string[]
8
+ allowPathPatterns?: string[]
9
+ }
10
+
11
+ function tokenIsTypeOnlyImport(node: ESTree.Node): boolean {
12
+ const parent = node.parent
13
+
14
+ if (parent?.type === 'ImportSpecifier') {
15
+ return parent.importKind === 'type'
16
+ || (parent.parent.type === 'ImportDeclaration' && parent.parent.importKind === 'type')
17
+ }
18
+
19
+ return (parent?.type === 'ImportDefaultSpecifier' || parent?.type === 'ImportNamespaceSpecifier')
20
+ && parent.parent.type === 'ImportDeclaration'
21
+ && parent.parent.importKind === 'type'
22
+ }
23
+
24
+ /**
25
+ * Restricts one identifier to configured owner paths while leaving path selection to the consumer.
26
+ *
27
+ * Example: `RouterClient` can be allowed in `rpc.client.ts` and rejected everywhere else.
28
+ */
29
+ export const noRestrictedToken = defineRule({
30
+ meta: {
31
+ type: 'problem',
32
+ schema: [{
33
+ type: 'object',
34
+ additionalProperties: false,
35
+ properties: {
36
+ token: { type: 'string' },
37
+ allowIn: { type: 'array', items: { type: 'string' } },
38
+ allowPathPatterns: { type: 'array', items: { type: 'string' } },
39
+ },
40
+ required: ['token', 'allowIn'],
41
+ }],
42
+ messages: {
43
+ restricted: "'{{token}}' may only appear in configured owner paths.",
44
+ },
45
+ },
46
+ createOnce(context) {
47
+ return {
48
+ before() {
49
+ const options = optionsFirst<NoRestrictedTokenOptions>(context)
50
+ const filename = namingPosixPath(context.filename)
51
+ const allowed = options.allowIn.some((suffix) => filename.endsWith(suffix))
52
+ || (options.allowPathPatterns ?? []).some((pattern) => new RegExp(pattern).test(filename))
53
+
54
+ if (allowed) {
55
+ return false
56
+ }
57
+ },
58
+ Identifier(node) {
59
+ const { token } = optionsFirst<NoRestrictedTokenOptions>(context)
60
+
61
+ if (node.name === token && !tokenIsTypeOnlyImport(node)) {
62
+ context.report({
63
+ node,
64
+ messageId: 'restricted',
65
+ data: { token },
66
+ })
67
+ }
68
+ },
69
+ }
70
+ },
71
+ })
@@ -0,0 +1,72 @@
1
+ import { defineRule } from '@oxlint/plugins'
2
+ import {
3
+ declarationsIsRuntime,
4
+ declarationsRuntimeKind,
5
+ declarationsRuntimeName,
6
+ optionsFirst,
7
+ } from '../utils/index.ts'
8
+
9
+ interface NoRuntimeInTypesOptions {
10
+ allow?: ('class' | 'default' | 'enum' | 'function' | 'variable')[]
11
+ }
12
+
13
+ /**
14
+ * Keeps consumer-selected type modules free of runtime declarations, with optional declaration-category exceptions.
15
+ *
16
+ * Example: An interface passes; `export const value = 1` fails unless variables are allowed.
17
+ */
18
+ export const noRuntimeInTypes = defineRule({
19
+ meta: {
20
+ type: 'problem',
21
+ schema: [{
22
+ type: 'object',
23
+ additionalProperties: false,
24
+ properties: {
25
+ allow: {
26
+ type: 'array',
27
+ items: { type: 'string', enum: ['class', 'default', 'enum', 'function', 'variable'] },
28
+ },
29
+ },
30
+ }],
31
+ messages: {
32
+ runtimeValue: 'Types files must not contain runtime value {{name}}.',
33
+ },
34
+ },
35
+ createOnce(context) {
36
+ return {
37
+ Program(program) {
38
+ const { allow = [] } = optionsFirst<NoRuntimeInTypesOptions>(context, {})
39
+
40
+ for (const statement of program.body) {
41
+ const declaration = statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration'
42
+ ? statement.declaration
43
+ : statement
44
+
45
+ if (!declaration) {
46
+ continue
47
+ }
48
+
49
+ if (declarationsIsRuntime(declaration)) {
50
+ if (!declaration.declare && !allow.includes(declarationsRuntimeKind(declaration))) {
51
+ context.report({
52
+ node: declaration,
53
+ messageId: 'runtimeValue',
54
+ data: { name: declarationsRuntimeName(declaration) },
55
+ })
56
+ }
57
+ continue
58
+ }
59
+
60
+ if (!allow.includes('default') && statement.type === 'ExportDefaultDeclaration'
61
+ && declaration.type !== 'TSInterfaceDeclaration' && declaration.type !== 'TSTypeAliasDeclaration') {
62
+ context.report({
63
+ node: statement,
64
+ messageId: 'runtimeValue',
65
+ data: { name: 'default' },
66
+ })
67
+ }
68
+ }
69
+ },
70
+ }
71
+ },
72
+ })