oxlint-plugin-arch 0.2.1 → 0.2.2

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 +8 -0
  2. package/README.md +1 -1
  3. package/dist/index.js +1984 -0
  4. package/package.json +7 -8
  5. package/src/index.ts +0 -57
  6. package/src/rules/export-file-prefix.ts +0 -70
  7. package/src/rules/export-name-pattern.ts +0 -71
  8. package/src/rules/filename-export-name.ts +0 -86
  9. package/src/rules/filename-match.ts +0 -47
  10. package/src/rules/folder-prefix.ts +0 -73
  11. package/src/rules/no-extra-exports.ts +0 -60
  12. package/src/rules/no-extra-factory-keys.ts +0 -58
  13. package/src/rules/no-file-level-helpers.ts +0 -58
  14. package/src/rules/no-imported-type-alias.ts +0 -83
  15. package/src/rules/no-inline-schema-elements.ts +0 -70
  16. package/src/rules/no-local-schema-construction.ts +0 -162
  17. package/src/rules/no-rederive-schema.ts +0 -66
  18. package/src/rules/no-restricted-files.ts +0 -40
  19. package/src/rules/no-restricted-token.ts +0 -71
  20. package/src/rules/no-runtime-in-types.ts +0 -72
  21. package/src/rules/no-single-use-scalar-schema.ts +0 -164
  22. package/src/rules/no-top-level-functions.ts +0 -77
  23. package/src/rules/no-trivial-functions.ts +0 -56
  24. package/src/rules/no-unescaped-like.ts +0 -67
  25. package/src/rules/only-export-components.ts +0 -78
  26. package/src/rules/require-file-factory.ts +0 -67
  27. package/src/rules/require-object-params.ts +0 -32
  28. package/src/rules/require-orpc-output.ts +0 -64
  29. package/src/rules/require-paired-call.ts +0 -71
  30. package/src/rules/route-surface.ts +0 -62
  31. package/src/utils/helpers/ast.ts +0 -101
  32. package/src/utils/helpers/declarations.ts +0 -203
  33. package/src/utils/helpers/exports.ts +0 -158
  34. package/src/utils/helpers/factories.ts +0 -67
  35. package/src/utils/helpers/index.ts +0 -11
  36. package/src/utils/helpers/naming.ts +0 -89
  37. package/src/utils/helpers/options.ts +0 -5
  38. package/src/utils/helpers/orpc.ts +0 -31
  39. package/src/utils/helpers/react-components.ts +0 -146
  40. package/src/utils/helpers/routes.ts +0 -31
  41. package/src/utils/helpers/sanitizers.ts +0 -33
  42. package/src/utils/helpers/schemas.ts +0 -82
  43. package/src/utils/index.ts +0 -1
package/package.json CHANGED
@@ -1,25 +1,24 @@
1
1
  {
2
2
  "name": "oxlint-plugin-arch",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Configurable architecture rules for Oxlint JavaScript plugins.",
5
5
  "type": "module",
6
- "main": "./src/index.ts",
6
+ "main": "./dist/index.js",
7
7
  "exports": {
8
- ".": "./src/index.ts"
8
+ ".": "./dist/index.js"
9
9
  },
10
10
  "files": [
11
11
  "CHANGELOG.md",
12
- "src/index.ts",
13
- "src/rules",
14
- "src/utils"
12
+ "dist"
15
13
  ],
16
14
  "scripts": {
17
- "check": "bun run lint && bun run test && bun run typecheck && bun run check:examples && bun run check:skill-assets",
15
+ "build": "bun build src/index.ts --outfile dist/index.js --target node --format esm --external @oxlint/plugins",
16
+ "check": "bun run lint && bun run test && bun run typecheck && bun run build && bun run check:examples && bun run check:skill-assets",
18
17
  "lint": "oxlint src examples scripts skills/install-oxlint-arch/scripts",
19
18
  "test": "vitest run src/tests",
20
19
  "typecheck": "tsc --noEmit",
21
20
  "sync:skill-assets": "bun scripts/sync-skill-assets.ts",
22
- "check:examples": "oxlint examples/minimal.oxlint.config.ts -c examples/minimal.oxlint.config.ts --disable-nested-config && oxlint examples/full.oxlint.config.ts -c examples/full.oxlint.config.ts --disable-nested-config",
21
+ "check:examples": "oxlint examples/minimal.oxlint.config.ts -c examples/minimal.oxlint.config.ts --disable-nested-config && oxlint examples/full.oxlint.config.ts -c examples/full.oxlint.config.ts --disable-nested-config && oxlint examples/dist.oxlint.config.ts -c examples/dist.oxlint.config.ts --disable-nested-config",
23
22
  "check:skill-assets": "bun scripts/sync-skill-assets.ts --check",
24
23
  "prepublishOnly": "bun run check",
25
24
  "release": "bash scripts/release.sh",
package/src/index.ts DELETED
@@ -1,57 +0,0 @@
1
- import { eslintCompatPlugin } from '@oxlint/plugins'
2
- import { exportFilePrefix } from './rules/export-file-prefix.ts'
3
- import { exportNamePattern } from './rules/export-name-pattern.ts'
4
- import { filenameExportName } from './rules/filename-export-name.ts'
5
- import { filenameMatch } from './rules/filename-match.ts'
6
- import { folderPrefix } from './rules/folder-prefix.ts'
7
- import { noExtraExports } from './rules/no-extra-exports.ts'
8
- import { noExtraFactoryKeys } from './rules/no-extra-factory-keys.ts'
9
- import { noFileLevelHelpers } from './rules/no-file-level-helpers.ts'
10
- import { noImportedTypeAlias } from './rules/no-imported-type-alias.ts'
11
- import { noInlineSchemaElements } from './rules/no-inline-schema-elements.ts'
12
- import { noLocalSchemaConstruction } from './rules/no-local-schema-construction.ts'
13
- import { noRederiveSchema } from './rules/no-rederive-schema.ts'
14
- import { noRestrictedFiles } from './rules/no-restricted-files.ts'
15
- import { noRestrictedToken } from './rules/no-restricted-token.ts'
16
- import { noRuntimeInTypes } from './rules/no-runtime-in-types.ts'
17
- import { noSingleUseScalarSchema } from './rules/no-single-use-scalar-schema.ts'
18
- import { noTopLevelFunctions } from './rules/no-top-level-functions.ts'
19
- import { noTrivialFunctions } from './rules/no-trivial-functions.ts'
20
- import { noUnescapedLike } from './rules/no-unescaped-like.ts'
21
- import { onlyExportComponents } from './rules/only-export-components.ts'
22
- import { requireFileFactory } from './rules/require-file-factory.ts'
23
- import { requireObjectParams } from './rules/require-object-params.ts'
24
- import { requireOrpcOutput } from './rules/require-orpc-output.ts'
25
- import { requirePairedCall } from './rules/require-paired-call.ts'
26
- import { routeSurface } from './rules/route-surface.ts'
27
-
28
- export default eslintCompatPlugin({
29
- meta: { name: 'arch' },
30
- rules: {
31
- 'export-file-prefix': exportFilePrefix,
32
- 'export-name-pattern': exportNamePattern,
33
- 'filename-export-name': filenameExportName,
34
- 'filename-match': filenameMatch,
35
- 'folder-prefix': folderPrefix,
36
- 'no-extra-exports': noExtraExports,
37
- 'no-extra-factory-keys': noExtraFactoryKeys,
38
- 'no-file-level-helpers': noFileLevelHelpers,
39
- 'no-imported-type-alias': noImportedTypeAlias,
40
- 'no-inline-schema-elements': noInlineSchemaElements,
41
- 'no-local-schema-construction': noLocalSchemaConstruction,
42
- 'no-rederive-schema': noRederiveSchema,
43
- 'no-restricted-files': noRestrictedFiles,
44
- 'no-restricted-token': noRestrictedToken,
45
- 'no-runtime-in-types': noRuntimeInTypes,
46
- 'no-single-use-scalar-schema': noSingleUseScalarSchema,
47
- 'no-top-level-functions': noTopLevelFunctions,
48
- 'no-trivial-functions': noTrivialFunctions,
49
- 'no-unescaped-like': noUnescapedLike,
50
- 'only-export-components': onlyExportComponents,
51
- 'require-file-factory': requireFileFactory,
52
- 'require-object-params': requireObjectParams,
53
- 'require-orpc-output': requireOrpcOutput,
54
- 'require-paired-call': requirePairedCall,
55
- 'route-surface': routeSurface,
56
- },
57
- })
@@ -1,70 +0,0 @@
1
- import { defineRule } from '@oxlint/plugins'
2
- import { declarationsCollectNamed, exportsCollect, namingFileBasename, optionsFirst } from '../utils/index.ts'
3
-
4
- interface ExportFilePrefixOptions {
5
- stem?: 'before-first-dot' | 'full-basename'
6
- normalize?: 'remove-separators' | 'none'
7
- allDeclarations?: boolean
8
- allowPattern?: string
9
- }
10
-
11
- /**
12
- * Checks that exported names start with a prefix taken from the filename, and can also check every function and type in the file.
13
- *
14
- * Example: In `foo-chart.tsx`, `fooChartPreview` and `interface FooChartPreviewProps` pass; `preview` fails when `allDeclarations` is on.
15
- */
16
- export const exportFilePrefix = defineRule({
17
- meta: {
18
- type: 'problem',
19
- schema: [{
20
- type: 'object',
21
- additionalProperties: false,
22
- properties: {
23
- stem: { type: 'string', enum: ['before-first-dot', 'full-basename'] },
24
- normalize: { type: 'string', enum: ['remove-separators', 'none'] },
25
- allDeclarations: { type: 'boolean' },
26
- allowPattern: { type: 'string' },
27
- },
28
- }],
29
- messages: {
30
- prefix: "'{{name}}' must start with file prefix '{{prefix}}'.",
31
- },
32
- },
33
- createOnce(context) {
34
- return {
35
- Program(program) {
36
- const options = optionsFirst<ExportFilePrefixOptions>(context, {
37
- stem: 'before-first-dot',
38
- normalize: 'remove-separators',
39
- })
40
- const basename = namingFileBasename(context.filename).replace(/\.(tsx?|jsx?)$/, '')
41
- const stem = options.stem === 'full-basename' ? basename : (basename.split('.')[0] ?? '')
42
- const prefix = options.normalize === 'none' ? stem : stem.replaceAll(/[-_]/g, '').toLowerCase()
43
- const allowed = options.allowPattern ? new RegExp(options.allowPattern) : null
44
- const names = options.allDeclarations
45
- ? declarationsCollectNamed(program)
46
- : exportsCollect(program).map((binding) => ({
47
- name: binding.localName ?? binding.exportedName,
48
- node: binding.node,
49
- }))
50
- const seen = new Set<string>()
51
-
52
- for (const item of names) {
53
- const key = `${item.name}:${item.node.start}:${item.node.end}`
54
- const comparableName = options.normalize === 'none' ? item.name : item.name.replaceAll('_', '').toLowerCase()
55
-
56
- if (seen.has(key) || allowed?.test(item.name) || comparableName.startsWith(prefix)) {
57
- continue
58
- }
59
-
60
- seen.add(key)
61
- context.report({
62
- node: item.node,
63
- messageId: 'prefix',
64
- data: { name: item.name, prefix },
65
- })
66
- }
67
- },
68
- }
69
- },
70
- })
@@ -1,71 +0,0 @@
1
- import { defineRule } from '@oxlint/plugins'
2
- import { declarationsCollectNamed, exportsCollect, optionsFirst } from '../utils/index.ts'
3
-
4
- interface ExportNamePatternOptions {
5
- pattern: string
6
- flags?: string
7
- ignoreTypeExports?: boolean
8
- allDeclarations?: boolean
9
- allowPattern?: string
10
- }
11
-
12
- /**
13
- * Checks exported names, or every function and type, against a regular expression supplied in the rule options.
14
- *
15
- * Example: With `^user[A-Z]`, `userGet` passes while `getUser` fails.
16
- */
17
- export const exportNamePattern = defineRule({
18
- meta: {
19
- type: 'problem',
20
- schema: [{
21
- type: 'object',
22
- additionalProperties: false,
23
- properties: {
24
- pattern: { type: 'string' },
25
- flags: { type: 'string' },
26
- ignoreTypeExports: { type: 'boolean' },
27
- allDeclarations: { type: 'boolean' },
28
- allowPattern: { type: 'string' },
29
- },
30
- required: ['pattern'],
31
- }],
32
- messages: {
33
- pattern: "'{{name}}' must match {{pattern}}.",
34
- },
35
- },
36
- createOnce(context) {
37
- return {
38
- Program(program) {
39
- const {
40
- pattern,
41
- flags,
42
- ignoreTypeExports = false,
43
- allDeclarations = false,
44
- allowPattern,
45
- } = optionsFirst<ExportNamePatternOptions>(context)
46
- const allowed = allowPattern ? new RegExp(allowPattern) : null
47
- const names = allDeclarations
48
- ? declarationsCollectNamed(program)
49
- : exportsCollect(program).flatMap((binding) => ignoreTypeExports && binding.typeOnly
50
- ? []
51
- : [{ name: binding.localName ?? binding.exportedName, node: binding.node }])
52
- const seen = new Set<string>()
53
-
54
- for (const item of names) {
55
- const key = `${item.name}:${item.node.start}:${item.node.end}`
56
-
57
- if (seen.has(key) || allowed?.test(item.name) || new RegExp(pattern, flags).test(item.name)) {
58
- continue
59
- }
60
-
61
- seen.add(key)
62
- context.report({
63
- node: item.node,
64
- messageId: 'pattern',
65
- data: { name: item.name, pattern },
66
- })
67
- }
68
- },
69
- }
70
- },
71
- })
@@ -1,86 +0,0 @@
1
- import { defineRule } from '@oxlint/plugins'
2
- import {
3
- declarationsCollectFunctions,
4
- exportsCollectFunctions,
5
- factoriesExpandTemplate,
6
- namingFileBasename,
7
- namingMatchTemplate,
8
- optionsFirst,
9
- } from '../utils/index.ts'
10
-
11
- interface FilenameExportNameOptions {
12
- file: string
13
- export: string
14
- mode?: 'all' | 'some'
15
- placeholderPattern?: string
16
- allDeclarations?: boolean
17
- }
18
-
19
- /**
20
- * Builds an expected function name from filename placeholders, then checks exported or all functions against it.
21
- *
22
- * Example: `user-action.create.ts` can require `makeUserActionCreate`; `mode: some` allows additional exports.
23
- */
24
- export const filenameExportName = defineRule({
25
- meta: {
26
- type: 'problem',
27
- schema: [{
28
- type: 'object',
29
- additionalProperties: false,
30
- properties: {
31
- file: { type: 'string' },
32
- export: { type: 'string' },
33
- mode: { type: 'string', enum: ['all', 'some'] },
34
- placeholderPattern: { type: 'string' },
35
- allDeclarations: { type: 'boolean' },
36
- },
37
- required: ['file', 'export'],
38
- }],
39
- messages: {
40
- mismatch: "Function '{{actual}}' must match '{{expected}}'.",
41
- },
42
- },
43
- createOnce(context) {
44
- return {
45
- Program(program) {
46
- const {
47
- file,
48
- export: exportTemplate,
49
- mode = 'all',
50
- placeholderPattern,
51
- allDeclarations = false,
52
- } = optionsFirst<FilenameExportNameOptions>(context)
53
- const groups = namingMatchTemplate(namingFileBasename(context.filename), file, placeholderPattern)
54
-
55
- if (!groups) {
56
- return
57
- }
58
-
59
- const expected = factoriesExpandTemplate(exportTemplate, groups)
60
- const functions = allDeclarations ? declarationsCollectFunctions(program) : exportsCollectFunctions(program)
61
-
62
- if (mode === 'some') {
63
- if (!functions.some((item) => item.name === expected)) {
64
- const first = functions.at(0)
65
- context.report({
66
- node: first?.node ?? program,
67
- messageId: 'mismatch',
68
- data: { actual: first?.name ?? '<none>', expected },
69
- })
70
- }
71
- return
72
- }
73
-
74
- for (const item of functions) {
75
- if (item.name !== expected) {
76
- context.report({
77
- node: item.node,
78
- messageId: 'mismatch',
79
- data: { actual: item.name, expected },
80
- })
81
- }
82
- }
83
- },
84
- }
85
- },
86
- })
@@ -1,47 +0,0 @@
1
- import { defineRule } from '@oxlint/plugins'
2
- import { namingFileBasename, optionsFirst } from '../utils/index.ts'
3
-
4
- interface FilenameMatchOptions {
5
- pattern: string
6
- message: string
7
- flags?: string
8
- }
9
-
10
- /**
11
- * Checks the current filename against a configurable regular expression and reports the configured message.
12
- *
13
- * Example: A pattern ending in `.spec.ts` accepts `login.spec.ts` and rejects `login.test.ts`.
14
- */
15
- export const filenameMatch = defineRule({
16
- meta: {
17
- type: 'problem',
18
- schema: [{
19
- type: 'object',
20
- additionalProperties: false,
21
- properties: {
22
- pattern: { type: 'string' },
23
- message: { type: 'string' },
24
- flags: { type: 'string' },
25
- },
26
- required: ['pattern', 'message'],
27
- }],
28
- messages: {
29
- mismatch: '{{message}}',
30
- },
31
- },
32
- createOnce(context) {
33
- return {
34
- Program(program) {
35
- const { pattern, message, flags } = optionsFirst<FilenameMatchOptions>(context)
36
-
37
- if (!new RegExp(pattern, flags).test(namingFileBasename(context.filename))) {
38
- context.report({
39
- node: program,
40
- messageId: 'mismatch',
41
- data: { message },
42
- })
43
- }
44
- },
45
- }
46
- },
47
- })
@@ -1,73 +0,0 @@
1
- import { defineRule } from '@oxlint/plugins'
2
- import {
3
- namingDirSegments,
4
- namingFileBasename,
5
- namingFolderPrefixes,
6
- namingPosixPath,
7
- namingSegmentsAfter,
8
- optionsFirst,
9
- } from '../utils/index.ts'
10
-
11
- interface FolderPrefixOptions {
12
- singularize?: 'none' | 'trailing-s'
13
- separators?: string[]
14
- after?: string
15
- }
16
-
17
- /**
18
- * Checks that a filename equals or starts with its parent folder name, or with the folders after a configured root.
19
- *
20
- * Example: Inside `users/`, `user-card.tsx` passes. With `after: 'components'`, `components/admin/insights/admin-insights-foo.tsx` passes.
21
- */
22
- export const folderPrefix = defineRule({
23
- meta: {
24
- type: 'problem',
25
- schema: [{
26
- type: 'object',
27
- additionalProperties: false,
28
- properties: {
29
- singularize: { type: 'string', enum: ['none', 'trailing-s'] },
30
- separators: { type: 'array', items: { type: 'string' } },
31
- after: { type: 'string' },
32
- },
33
- }],
34
- messages: {
35
- prefix: "File '{{file}}' must start with folder name '{{folder}}' or an allowed singular form.",
36
- },
37
- },
38
- createOnce(context) {
39
- return {
40
- Program(program) {
41
- const {
42
- singularize = 'trailing-s',
43
- separators = ['-'],
44
- after,
45
- } = optionsFirst<FolderPrefixOptions>(context, {})
46
- const filename = namingPosixPath(context.filename)
47
- const folders = after
48
- ? namingSegmentsAfter(filename, after)
49
- : [namingDirSegments(filename).at(-1) ?? '']
50
-
51
- if (!folders) {
52
- return
53
- }
54
-
55
- const resolved = folders.length > 0 ? folders : [namingDirSegments(filename).at(-1) ?? '']
56
- const separator = separators[0] ?? '-'
57
- const prefixes = namingFolderPrefixes(resolved, separator, singularize)
58
- const file = namingFileBasename(filename)
59
- const stem = file.replace(/\.(tsx?|jsx?)$/, '')
60
- const valid = prefixes.some((prefix) => stem === prefix
61
- || separators.some((item) => stem.startsWith(`${prefix}${item}`)))
62
-
63
- if (!valid) {
64
- context.report({
65
- node: program,
66
- messageId: 'prefix',
67
- data: { file, folder: prefixes[0] ?? '' },
68
- })
69
- }
70
- },
71
- }
72
- },
73
- })
@@ -1,60 +0,0 @@
1
- import { defineRule } from '@oxlint/plugins'
2
- import { exportsCollect, namingCamelCase, namingFileBasename, namingPascalCase, optionsFirst } from '../utils/index.ts'
3
-
4
- interface NoExtraExportsOptions {
5
- names: string[]
6
- domainStem?: 'before-first-dot' | 'full-basename'
7
- allowTypeExports?: boolean
8
- }
9
-
10
- /**
11
- * Allows only the configured export-name templates, with placeholders derived from the filename.
12
- *
13
- * Example: `billing.service.ts` may allow `makeBillingService`; an exported `helper` then fails.
14
- */
15
- export const noExtraExports = defineRule({
16
- meta: {
17
- type: 'problem',
18
- schema: [{
19
- type: 'object',
20
- additionalProperties: false,
21
- properties: {
22
- names: { type: 'array', items: { type: 'string' } },
23
- domainStem: { type: 'string', enum: ['before-first-dot', 'full-basename'] },
24
- allowTypeExports: { type: 'boolean' },
25
- },
26
- required: ['names'],
27
- }],
28
- messages: {
29
- extraExport: "Export '{{name}}' is not allowed; expected one of: {{allowed}}.",
30
- },
31
- },
32
- createOnce(context) {
33
- return {
34
- Program(program) {
35
- const options = optionsFirst<NoExtraExportsOptions>(context)
36
- const basename = namingFileBasename(context.filename).replace(/\.(tsx?|jsx?)$/, '')
37
- const domain = options.domainStem === 'full-basename' ? basename : (basename.split('.')[0] ?? '')
38
- const allowed = options.names.map((template) => template
39
- .replaceAll('{Domain}', namingPascalCase(domain))
40
- .replaceAll('{domain}', namingCamelCase(domain)))
41
-
42
- for (const binding of exportsCollect(program)) {
43
- if (options.allowTypeExports && binding.typeOnly) {
44
- continue
45
- }
46
-
47
- const name = binding.localName ?? binding.exportedName
48
-
49
- if (!allowed.includes(name)) {
50
- context.report({
51
- node: binding.node,
52
- messageId: 'extraExport',
53
- data: { name, allowed: allowed.join(', ') },
54
- })
55
- }
56
- }
57
- },
58
- }
59
- },
60
- })
@@ -1,58 +0,0 @@
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
- })
@@ -1,58 +0,0 @@
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
- })