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.
- package/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/dist/index.js +1984 -0
- package/package.json +7 -8
- package/src/index.ts +0 -57
- package/src/rules/export-file-prefix.ts +0 -70
- package/src/rules/export-name-pattern.ts +0 -71
- package/src/rules/filename-export-name.ts +0 -86
- package/src/rules/filename-match.ts +0 -47
- package/src/rules/folder-prefix.ts +0 -73
- package/src/rules/no-extra-exports.ts +0 -60
- package/src/rules/no-extra-factory-keys.ts +0 -58
- package/src/rules/no-file-level-helpers.ts +0 -58
- package/src/rules/no-imported-type-alias.ts +0 -83
- package/src/rules/no-inline-schema-elements.ts +0 -70
- package/src/rules/no-local-schema-construction.ts +0 -162
- package/src/rules/no-rederive-schema.ts +0 -66
- package/src/rules/no-restricted-files.ts +0 -40
- package/src/rules/no-restricted-token.ts +0 -71
- package/src/rules/no-runtime-in-types.ts +0 -72
- package/src/rules/no-single-use-scalar-schema.ts +0 -164
- package/src/rules/no-top-level-functions.ts +0 -77
- package/src/rules/no-trivial-functions.ts +0 -56
- package/src/rules/no-unescaped-like.ts +0 -67
- package/src/rules/only-export-components.ts +0 -78
- package/src/rules/require-file-factory.ts +0 -67
- package/src/rules/require-object-params.ts +0 -32
- package/src/rules/require-orpc-output.ts +0 -64
- package/src/rules/require-paired-call.ts +0 -71
- package/src/rules/route-surface.ts +0 -62
- package/src/utils/helpers/ast.ts +0 -101
- package/src/utils/helpers/declarations.ts +0 -203
- package/src/utils/helpers/exports.ts +0 -158
- package/src/utils/helpers/factories.ts +0 -67
- package/src/utils/helpers/index.ts +0 -11
- package/src/utils/helpers/naming.ts +0 -89
- package/src/utils/helpers/options.ts +0 -5
- package/src/utils/helpers/orpc.ts +0 -31
- package/src/utils/helpers/react-components.ts +0 -146
- package/src/utils/helpers/routes.ts +0 -31
- package/src/utils/helpers/sanitizers.ts +0 -33
- package/src/utils/helpers/schemas.ts +0 -82
- 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.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Configurable architecture rules for Oxlint JavaScript plugins.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
7
|
"exports": {
|
|
8
|
-
".": "./
|
|
8
|
+
".": "./dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"CHANGELOG.md",
|
|
12
|
-
"
|
|
13
|
-
"src/rules",
|
|
14
|
-
"src/utils"
|
|
12
|
+
"dist"
|
|
15
13
|
],
|
|
16
14
|
"scripts": {
|
|
17
|
-
"
|
|
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
|
-
})
|