@astrale-os/sdk 0.4.12 → 0.4.13
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/dist/cli/run.d.ts +8 -2
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +51 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/define/remote-function.d.ts +3 -1
- package/dist/define/remote-function.d.ts.map +1 -1
- package/dist/define/remote-function.js.map +1 -1
- package/dist/dispatch/dispatcher.d.ts +10 -20
- package/dist/dispatch/dispatcher.d.ts.map +1 -1
- package/dist/dispatch/dispatcher.js +55 -54
- package/dist/dispatch/dispatcher.js.map +1 -1
- package/dist/dispatch/execute.d.ts +2 -1
- package/dist/dispatch/execute.d.ts.map +1 -1
- package/dist/dispatch/execute.js +1 -0
- package/dist/dispatch/execute.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/linter/analyze.d.ts +6 -0
- package/dist/linter/analyze.d.ts.map +1 -0
- package/dist/linter/analyze.js +12 -0
- package/dist/linter/analyze.js.map +1 -0
- package/dist/linter/diagnostic.d.ts +32 -0
- package/dist/linter/diagnostic.d.ts.map +1 -0
- package/dist/linter/diagnostic.js +35 -0
- package/dist/linter/diagnostic.js.map +1 -0
- package/dist/linter/index.d.ts +10 -0
- package/dist/linter/index.d.ts.map +1 -0
- package/dist/linter/index.js +6 -0
- package/dist/linter/index.js.map +1 -0
- package/dist/linter/lint.d.ts +8 -0
- package/dist/linter/lint.d.ts.map +1 -0
- package/dist/linter/lint.js +26 -0
- package/dist/linter/lint.js.map +1 -0
- package/dist/linter/oxlint/config.d.ts +14 -0
- package/dist/linter/oxlint/config.d.ts.map +1 -0
- package/dist/linter/oxlint/config.js +18 -0
- package/dist/linter/oxlint/config.js.map +1 -0
- package/dist/linter/oxlint/plugin.d.ts +13 -0
- package/dist/linter/oxlint/plugin.d.ts.map +1 -0
- package/dist/linter/oxlint/plugin.js +14 -0
- package/dist/linter/oxlint/plugin.js.map +1 -0
- package/dist/linter/oxlint/run.d.ts +7 -0
- package/dist/linter/oxlint/run.d.ts.map +1 -0
- package/dist/linter/oxlint/run.js +122 -0
- package/dist/linter/oxlint/run.js.map +1 -0
- package/dist/linter/preflight.d.ts +2 -0
- package/dist/linter/preflight.d.ts.map +1 -0
- package/dist/linter/preflight.js +162 -0
- package/dist/linter/preflight.js.map +1 -0
- package/dist/linter/project.d.ts +12 -0
- package/dist/linter/project.d.ts.map +1 -0
- package/dist/linter/project.js +88 -0
- package/dist/linter/project.js.map +1 -0
- package/dist/linter/report.d.ts +4 -0
- package/dist/linter/report.d.ts.map +1 -0
- package/dist/linter/report.js +22 -0
- package/dist/linter/report.js.map +1 -0
- package/dist/linter/rules/catalog.d.ts +56 -0
- package/dist/linter/rules/catalog.d.ts.map +1 -0
- package/dist/linter/rules/catalog.js +53 -0
- package/dist/linter/rules/catalog.js.map +1 -0
- package/dist/linter/rules/core-has-no-async.d.ts +3 -0
- package/dist/linter/rules/core-has-no-async.d.ts.map +1 -0
- package/dist/linter/rules/core-has-no-async.js +43 -0
- package/dist/linter/rules/core-has-no-async.js.map +1 -0
- package/dist/linter/rules/core-is-pure.d.ts +3 -0
- package/dist/linter/rules/core-is-pure.d.ts.map +1 -0
- package/dist/linter/rules/core-is-pure.js +74 -0
- package/dist/linter/rules/core-is-pure.js.map +1 -0
- package/dist/linter/rules/no-nested-steps.d.ts +3 -0
- package/dist/linter/rules/no-nested-steps.d.ts.map +1 -0
- package/dist/linter/rules/no-nested-steps.js +32 -0
- package/dist/linter/rules/no-nested-steps.js.map +1 -0
- package/dist/linter/rules/no-read-inside-mutate.d.ts +3 -0
- package/dist/linter/rules/no-read-inside-mutate.d.ts.map +1 -0
- package/dist/linter/rules/no-read-inside-mutate.js +64 -0
- package/dist/linter/rules/no-read-inside-mutate.js.map +1 -0
- package/dist/linter/rules/one-remote-definition-per-file.d.ts +3 -0
- package/dist/linter/rules/one-remote-definition-per-file.d.ts.map +1 -0
- package/dist/linter/rules/one-remote-definition-per-file.js +23 -0
- package/dist/linter/rules/one-remote-definition-per-file.js.map +1 -0
- package/dist/linter/rules/oxlint-ast.d.ts +30 -0
- package/dist/linter/rules/oxlint-ast.d.ts.map +1 -0
- package/dist/linter/rules/oxlint-ast.js +104 -0
- package/dist/linter/rules/oxlint-ast.js.map +1 -0
- package/dist/linter/rules/oxlint-rule.d.ts +45 -0
- package/dist/linter/rules/oxlint-rule.d.ts.map +1 -0
- package/dist/linter/rules/oxlint-rule.js +2 -0
- package/dist/linter/rules/oxlint-rule.js.map +1 -0
- package/dist/linter/rules/project-rule.d.ts +10 -0
- package/dist/linter/rules/project-rule.d.ts.map +1 -0
- package/dist/linter/rules/project-rule.js +24 -0
- package/dist/linter/rules/project-rule.js.map +1 -0
- package/dist/linter/rules/thin-schema-composition-root.d.ts +3 -0
- package/dist/linter/rules/thin-schema-composition-root.d.ts.map +1 -0
- package/dist/linter/rules/thin-schema-composition-root.js +23 -0
- package/dist/linter/rules/thin-schema-composition-root.js.map +1 -0
- package/dist/linter/source.d.ts +24 -0
- package/dist/linter/source.d.ts.map +1 -0
- package/dist/linter/source.js +279 -0
- package/dist/linter/source.js.map +1 -0
- package/dist/linter/suppression.d.ts +3 -0
- package/dist/linter/suppression.d.ts.map +1 -0
- package/dist/linter/suppression.js +11 -0
- package/dist/linter/suppression.js.map +1 -0
- package/dist/method/context.d.ts +3 -1
- package/dist/method/context.d.ts.map +1 -1
- package/dist/server/auxiliary-routes.d.ts.map +1 -1
- package/dist/server/auxiliary-routes.js +4 -4
- package/dist/server/auxiliary-routes.js.map +1 -1
- package/dist/server/create.js +1 -1
- package/dist/server/create.js.map +1 -1
- package/dist/server/start.d.ts.map +1 -1
- package/dist/server/start.js.map +1 -1
- package/package.json +27 -7
- package/src/cli/run.ts +53 -2
- package/src/define/remote-function.ts +3 -1
- package/src/dispatch/dispatcher.ts +62 -69
- package/src/dispatch/execute.ts +3 -1
- package/src/index.ts +2 -2
- package/src/linter/analyze.ts +17 -0
- package/src/linter/diagnostic.ts +68 -0
- package/src/linter/docs/DIAGNOSTICS.md +47 -0
- package/src/linter/docs/JUDGMENT.md +81 -0
- package/src/linter/docs/PRINCIPLES.md +97 -0
- package/src/linter/docs/README.md +38 -0
- package/src/linter/docs/RULE-AUTHORING.md +144 -0
- package/src/linter/docs/RULES.md +153 -0
- package/src/linter/index.ts +14 -0
- package/src/linter/lint.ts +32 -0
- package/src/linter/oxlint/astrale-ox.d.ts +5 -0
- package/src/linter/oxlint/config.ts +19 -0
- package/src/linter/oxlint/plugin.js +16 -0
- package/src/linter/oxlint/run.ts +162 -0
- package/src/linter/preflight.ts +181 -0
- package/src/linter/project.ts +108 -0
- package/src/linter/report.ts +25 -0
- package/src/linter/rules/catalog.ts +67 -0
- package/src/linter/rules/core-has-no-async.js +43 -0
- package/src/linter/rules/core-is-pure.ts +103 -0
- package/src/linter/rules/no-nested-steps.js +37 -0
- package/src/linter/rules/no-read-inside-mutate.js +72 -0
- package/src/linter/rules/one-remote-definition-per-file.ts +29 -0
- package/src/linter/rules/oxlint-ast.js +110 -0
- package/src/linter/rules/oxlint-rule.ts +35 -0
- package/src/linter/rules/project-rule.ts +37 -0
- package/src/linter/rules/thin-schema-composition-root.ts +26 -0
- package/src/linter/source.ts +323 -0
- package/src/linter/suppression.ts +11 -0
- package/src/method/context.ts +3 -1
- package/src/server/auxiliary-routes.ts +9 -4
- package/src/server/create.ts +1 -1
- package/src/server/start.ts +2 -1
- package/dist/defer/index.d.ts +0 -24
- package/dist/defer/index.d.ts.map +0 -1
- package/dist/defer/index.js +0 -42
- package/dist/defer/index.js.map +0 -1
- package/src/defer/index.ts +0 -64
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { LintDiagnostic } from '../diagnostic.js'
|
|
2
|
+
import type { SourceFile, SourceImport } from '../source.js'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
isProductionCore,
|
|
6
|
+
isProductionSource,
|
|
7
|
+
resolveProjectImport,
|
|
8
|
+
type DomainProject,
|
|
9
|
+
} from '../project.js'
|
|
10
|
+
import { analyzerDiagnostic, type AnalyzerRule } from './project-rule.js'
|
|
11
|
+
|
|
12
|
+
const forbiddenPackages = new Set([
|
|
13
|
+
'@astrale-os/adapter-astrale',
|
|
14
|
+
'@astrale-os/adapter-cloudflare',
|
|
15
|
+
'@astrale-os/kernel-api',
|
|
16
|
+
'@astrale-os/kernel-client',
|
|
17
|
+
'@astrale-os/kernel-server',
|
|
18
|
+
'@astrale-os/kernel-test',
|
|
19
|
+
'@astrale-os/sdk',
|
|
20
|
+
'node:child_process',
|
|
21
|
+
'node:cluster',
|
|
22
|
+
'node:dgram',
|
|
23
|
+
'node:dns',
|
|
24
|
+
'node:fs',
|
|
25
|
+
'node:fs/promises',
|
|
26
|
+
'node:http',
|
|
27
|
+
'node:http2',
|
|
28
|
+
'node:https',
|
|
29
|
+
'node:net',
|
|
30
|
+
'node:tls',
|
|
31
|
+
'node:worker_threads',
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
const forbiddenRoots = new Set(['client', 'functions', 'integrations', 'runtime', 'simulation'])
|
|
35
|
+
const forbiddenRootFiles = new Set(['astrale.config.ts', 'deps.ts', 'domain.ts', 'env.ts'])
|
|
36
|
+
|
|
37
|
+
export const coreIsPure: AnalyzerRule = {
|
|
38
|
+
slug: 'core-is-pure',
|
|
39
|
+
analyze(project: DomainProject): readonly LintDiagnostic[] {
|
|
40
|
+
const diagnostics: LintDiagnostic[] = []
|
|
41
|
+
const visited = new Set<string>()
|
|
42
|
+
const pending = project.files.filter((file) => isProductionCore(file.relativePath))
|
|
43
|
+
|
|
44
|
+
while (pending.length > 0) {
|
|
45
|
+
const file = pending.pop()!
|
|
46
|
+
if (visited.has(file.path)) continue
|
|
47
|
+
visited.add(file.path)
|
|
48
|
+
|
|
49
|
+
for (const sourceImport of file.imports) {
|
|
50
|
+
const forbiddenPackage = forbiddenPackageName(sourceImport.specifier)
|
|
51
|
+
if (forbiddenPackage) {
|
|
52
|
+
pushDiagnostic(
|
|
53
|
+
diagnostics,
|
|
54
|
+
this.slug,
|
|
55
|
+
file,
|
|
56
|
+
sourceImport,
|
|
57
|
+
`Import ${forbiddenPackage} crosses the pure-core boundary.`,
|
|
58
|
+
)
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const dependency = resolveProjectImport(project, file, sourceImport.specifier)
|
|
63
|
+
if (!dependency || !isProductionSource(dependency.relativePath)) continue
|
|
64
|
+
if (isForbiddenProjectLayer(dependency.relativePath)) {
|
|
65
|
+
pushDiagnostic(
|
|
66
|
+
diagnostics,
|
|
67
|
+
this.slug,
|
|
68
|
+
file,
|
|
69
|
+
sourceImport,
|
|
70
|
+
`Import ${sourceImport.specifier} reaches ${dependency.relativePath}.`,
|
|
71
|
+
)
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
pending.push(dependency)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return diagnostics
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function forbiddenPackageName(specifier: string): string | undefined {
|
|
82
|
+
if (forbiddenPackages.has(specifier)) return specifier
|
|
83
|
+
for (const name of forbiddenPackages) {
|
|
84
|
+
if (specifier.startsWith(`${name}/`)) return name
|
|
85
|
+
}
|
|
86
|
+
return undefined
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isForbiddenProjectLayer(relativePath: string): boolean {
|
|
90
|
+
const [root] = relativePath.split('/')
|
|
91
|
+
return forbiddenRoots.has(root ?? '') || forbiddenRootFiles.has(relativePath)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function pushDiagnostic(
|
|
95
|
+
diagnostics: LintDiagnostic[],
|
|
96
|
+
slug: AnalyzerRule['slug'],
|
|
97
|
+
file: SourceFile,
|
|
98
|
+
sourceImport: SourceImport,
|
|
99
|
+
detail: string,
|
|
100
|
+
): void {
|
|
101
|
+
const diagnostic = analyzerDiagnostic(slug, file, sourceImport.token, detail)
|
|
102
|
+
if (diagnostic) diagnostics.push(diagnostic)
|
|
103
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
callbackCall,
|
|
5
|
+
isProductionFile,
|
|
6
|
+
isStepRun,
|
|
7
|
+
nearestFunction,
|
|
8
|
+
reportUnlessSuppressed,
|
|
9
|
+
} from './oxlint-ast.js'
|
|
10
|
+
|
|
11
|
+
const id = 'astrale/no-nested-steps'
|
|
12
|
+
|
|
13
|
+
/** @type {import('./oxlint-rule.js').OxlintRule} */
|
|
14
|
+
export const noNestedSteps = {
|
|
15
|
+
meta: {
|
|
16
|
+
type: 'problem',
|
|
17
|
+
docs: {
|
|
18
|
+
description: 'A durable step directly contains another durable step.',
|
|
19
|
+
url: 'https://docs.astrale.ai/linter/rules/no-nested-steps',
|
|
20
|
+
},
|
|
21
|
+
messages: {
|
|
22
|
+
nested: 'A step.run callback must not directly invoke another step.run.',
|
|
23
|
+
},
|
|
24
|
+
schema: [],
|
|
25
|
+
},
|
|
26
|
+
create(context) {
|
|
27
|
+
if (!isProductionFile(context)) return {}
|
|
28
|
+
return {
|
|
29
|
+
CallExpression(node) {
|
|
30
|
+
if (!isStepRun(node)) return
|
|
31
|
+
const containingFunction = nearestFunction(node.parent)
|
|
32
|
+
if (!containingFunction || !isStepRun(callbackCall(containingFunction))) return
|
|
33
|
+
reportUnlessSuppressed(context, node, id, 'nested')
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
callbackCall,
|
|
5
|
+
isFunctionNode,
|
|
6
|
+
isMutateCall,
|
|
7
|
+
isProductionFile,
|
|
8
|
+
nearestFunction,
|
|
9
|
+
reportUnlessSuppressed,
|
|
10
|
+
staticMember,
|
|
11
|
+
} from './oxlint-ast.js'
|
|
12
|
+
|
|
13
|
+
const id = 'astrale/no-read-inside-mutate'
|
|
14
|
+
const readMethods = new Set([
|
|
15
|
+
'call',
|
|
16
|
+
'check',
|
|
17
|
+
'children',
|
|
18
|
+
'get',
|
|
19
|
+
'getOrThrow',
|
|
20
|
+
'neighbors',
|
|
21
|
+
'query',
|
|
22
|
+
'refresh',
|
|
23
|
+
'whoami',
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
/** @type {import('./oxlint-rule.js').OxlintRule} */
|
|
27
|
+
export const noReadInsideMutate = {
|
|
28
|
+
meta: {
|
|
29
|
+
type: 'problem',
|
|
30
|
+
docs: {
|
|
31
|
+
description: 'A graph mutation builder performs a read or is asynchronous.',
|
|
32
|
+
url: 'https://docs.astrale.ai/linter/rules/no-read-inside-mutate',
|
|
33
|
+
},
|
|
34
|
+
messages: {
|
|
35
|
+
asyncCallback: 'A kernel.mutate callback must be synchronous.',
|
|
36
|
+
read: 'Read graph state before kernel.mutate; its callback only constructs a patch.',
|
|
37
|
+
},
|
|
38
|
+
schema: [],
|
|
39
|
+
},
|
|
40
|
+
create(context) {
|
|
41
|
+
if (!isProductionFile(context)) return {}
|
|
42
|
+
return {
|
|
43
|
+
CallExpression(node) {
|
|
44
|
+
if (isMutateCall(node)) {
|
|
45
|
+
const callback = node.arguments?.find(isFunctionNode)
|
|
46
|
+
if (callback?.async) {
|
|
47
|
+
reportUnlessSuppressed(context, callback, id, 'asyncCallback')
|
|
48
|
+
}
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const callback = nearestFunction(node.parent)
|
|
53
|
+
if (!callback || callback.async) return
|
|
54
|
+
const mutate = callbackCall(callback)
|
|
55
|
+
if (!isMutateCall(mutate)) return
|
|
56
|
+
|
|
57
|
+
const mutationMember = staticMember(mutate?.callee)
|
|
58
|
+
const readMember = staticMember(node.callee)
|
|
59
|
+
if (!mutationMember || !readMember || !readMethods.has(readMember.property)) return
|
|
60
|
+
if (!sameKernelRoot(context, mutationMember.object, readMember.object)) return
|
|
61
|
+
reportUnlessSuppressed(context, node, id, 'read')
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @param {import('./oxlint-rule.js').OxlintRuleContext} context @param {import('./oxlint-rule.js').AstNode} mutationRoot @param {import('./oxlint-rule.js').AstNode} readRoot */
|
|
68
|
+
function sameKernelRoot(context, mutationRoot, readRoot) {
|
|
69
|
+
const mutationText = context.sourceCode.getText(mutationRoot)
|
|
70
|
+
const readText = context.sourceCode.getText(readRoot)
|
|
71
|
+
return readText === mutationText || readText.startsWith(`${mutationText}.`)
|
|
72
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { LintDiagnostic } from '../diagnostic.js'
|
|
2
|
+
|
|
3
|
+
import { isProductionSource, type DomainProject } from '../project.js'
|
|
4
|
+
import { importedCalls } from '../source.js'
|
|
5
|
+
import { analyzerDiagnostic, type AnalyzerRule } from './project-rule.js'
|
|
6
|
+
|
|
7
|
+
const sdkModules = new Set(['@astrale-os/sdk'])
|
|
8
|
+
const remoteDefinitions = new Set(['defineRemoteFunction', 'remoteMethod'])
|
|
9
|
+
|
|
10
|
+
export const oneRemoteDefinitionPerFile: AnalyzerRule = {
|
|
11
|
+
slug: 'one-remote-definition-per-file',
|
|
12
|
+
analyze(project: DomainProject): readonly LintDiagnostic[] {
|
|
13
|
+
const diagnostics: LintDiagnostic[] = []
|
|
14
|
+
for (const file of project.files) {
|
|
15
|
+
if (!isProductionSource(file.relativePath)) continue
|
|
16
|
+
const definitions = importedCalls(file, sdkModules, remoteDefinitions)
|
|
17
|
+
for (const definition of definitions.slice(1)) {
|
|
18
|
+
const diagnostic = analyzerDiagnostic(
|
|
19
|
+
this.slug,
|
|
20
|
+
file,
|
|
21
|
+
definition,
|
|
22
|
+
`Found ${definitions.length} definitions.`,
|
|
23
|
+
)
|
|
24
|
+
if (diagnostic) diagnostics.push(diagnostic)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return diagnostics
|
|
28
|
+
},
|
|
29
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { relative, sep } from 'node:path'
|
|
4
|
+
|
|
5
|
+
/** @typedef {import('./oxlint-rule.js').AstNode} AstNode */
|
|
6
|
+
/** @typedef {import('./oxlint-rule.js').OxlintRuleContext} OxlintRuleContext */
|
|
7
|
+
/** @typedef {{ object: AstNode, property: string }} StaticMember */
|
|
8
|
+
|
|
9
|
+
/** @param {AstNode | undefined} node @returns {StaticMember | undefined} */
|
|
10
|
+
export function staticMember(node) {
|
|
11
|
+
if (!node || node.type !== 'MemberExpression' || node.computed) return undefined
|
|
12
|
+
if (!node.object || node.property?.type !== 'Identifier' || !node.property.name) return undefined
|
|
13
|
+
return { object: node.object, property: node.property.name }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** @param {AstNode | undefined} node */
|
|
17
|
+
export function isFunctionNode(node) {
|
|
18
|
+
return (
|
|
19
|
+
node?.type === 'ArrowFunctionExpression' ||
|
|
20
|
+
node?.type === 'FunctionDeclaration' ||
|
|
21
|
+
node?.type === 'FunctionExpression'
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** @param {AstNode | undefined} node @returns {AstNode | undefined} */
|
|
26
|
+
export function nearestFunction(node) {
|
|
27
|
+
let current = node
|
|
28
|
+
while (current) {
|
|
29
|
+
if (isFunctionNode(current)) return current
|
|
30
|
+
current = current.parent
|
|
31
|
+
}
|
|
32
|
+
return undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @param {AstNode} callback @returns {AstNode | undefined} */
|
|
36
|
+
export function callbackCall(callback) {
|
|
37
|
+
let current = callback
|
|
38
|
+
while (current.parent && isTransparentWrapper(current.parent)) current = current.parent
|
|
39
|
+
const parent = current.parent
|
|
40
|
+
if (parent?.type !== 'CallExpression' || !parent.arguments?.includes(current)) return undefined
|
|
41
|
+
return parent
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @param {AstNode} node */
|
|
45
|
+
function isTransparentWrapper(node) {
|
|
46
|
+
return (
|
|
47
|
+
node.type === 'ChainExpression' ||
|
|
48
|
+
node.type === 'ParenthesizedExpression' ||
|
|
49
|
+
node.type === 'TSAsExpression' ||
|
|
50
|
+
node.type === 'TSNonNullExpression' ||
|
|
51
|
+
node.type === 'TSSatisfiesExpression'
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @param {AstNode | undefined} call */
|
|
56
|
+
export function isStepRun(call) {
|
|
57
|
+
if (call?.type !== 'CallExpression') return false
|
|
58
|
+
const member = staticMember(call.callee)
|
|
59
|
+
if (!member || member.property !== 'run') return false
|
|
60
|
+
if (member.object.type === 'Identifier') return member.object.name === 'step'
|
|
61
|
+
return staticMember(member.object)?.property === 'step'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @param {AstNode | undefined} call */
|
|
65
|
+
export function isMutateCall(call) {
|
|
66
|
+
if (call?.type !== 'CallExpression') return false
|
|
67
|
+
const member = staticMember(call.callee)
|
|
68
|
+
if (!member || member.property !== 'mutate') return false
|
|
69
|
+
if (member.object.type === 'Identifier') return member.object.name === 'kernel'
|
|
70
|
+
return staticMember(member.object)?.property === 'kernel'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** @param {OxlintRuleContext} context */
|
|
74
|
+
export function relativeFilename(context) {
|
|
75
|
+
const path = relative(context.cwd, context.filename)
|
|
76
|
+
return sep === '/' ? path : path.split(sep).join('/')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @param {OxlintRuleContext} context */
|
|
80
|
+
export function isProductionFile(context) {
|
|
81
|
+
const path = relativeFilename(context)
|
|
82
|
+
return (
|
|
83
|
+
!path.split('/').includes('__tests__') &&
|
|
84
|
+
!/\.(?:test|spec|bench|perf)\.[cm]?[jt]sx?$/.test(path) &&
|
|
85
|
+
!path.includes('.gen.')
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** @param {OxlintRuleContext} context */
|
|
90
|
+
export function isProductionCoreFile(context) {
|
|
91
|
+
const path = relativeFilename(context)
|
|
92
|
+
return (path === 'core' || path.startsWith('core/')) && isProductionFile(context)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @param {OxlintRuleContext} context @param {AstNode} node @param {string} id @param {string} messageId */
|
|
96
|
+
export function reportUnlessSuppressed(context, node, id, messageId) {
|
|
97
|
+
if (isSuppressed(context, node, id)) return
|
|
98
|
+
context.report({ node, messageId })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** @param {OxlintRuleContext} context @param {AstNode} node @param {string} id */
|
|
102
|
+
function isSuppressed(context, node, id) {
|
|
103
|
+
const line = context.sourceCode.getLoc(node).start.line
|
|
104
|
+
if (line <= 1) return false
|
|
105
|
+
const previous = context.sourceCode.text.split(/\r?\n/)[line - 2]
|
|
106
|
+
const match = previous
|
|
107
|
+
? /^\s*\/\/\s*astrale-disable-next-line\s+(.+?)\s+--\s+\S.*$/.exec(previous)
|
|
108
|
+
: null
|
|
109
|
+
return match ? match[1].split(/[\s,]+/).includes(id) : false
|
|
110
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type AstNode = {
|
|
2
|
+
type: string
|
|
3
|
+
parent?: AstNode
|
|
4
|
+
range?: [number, number]
|
|
5
|
+
async?: boolean
|
|
6
|
+
await?: boolean
|
|
7
|
+
name?: string
|
|
8
|
+
computed?: boolean
|
|
9
|
+
object?: AstNode
|
|
10
|
+
property?: AstNode
|
|
11
|
+
callee?: AstNode
|
|
12
|
+
arguments?: AstNode[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type OxlintRuleContext = {
|
|
16
|
+
id: string
|
|
17
|
+
filename: string
|
|
18
|
+
cwd: string
|
|
19
|
+
sourceCode: {
|
|
20
|
+
text: string
|
|
21
|
+
getText(node?: AstNode | null): string
|
|
22
|
+
getLoc(node: AstNode): { start: { line: number; column: number } }
|
|
23
|
+
}
|
|
24
|
+
report(diagnostic: { node: AstNode; messageId: string }): void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type OxlintRule = {
|
|
28
|
+
meta: {
|
|
29
|
+
type: 'problem'
|
|
30
|
+
docs: { description: string; url: string }
|
|
31
|
+
messages: Record<string, string>
|
|
32
|
+
schema: readonly []
|
|
33
|
+
}
|
|
34
|
+
create(context: OxlintRuleContext): Record<string, ((node: AstNode) => void) | undefined>
|
|
35
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { LintDiagnostic } from '../diagnostic.js'
|
|
2
|
+
import type { DomainProject } from '../project.js'
|
|
3
|
+
import type { SourceFile, SourceToken } from '../source.js'
|
|
4
|
+
|
|
5
|
+
import { isSuppressed } from '../suppression.js'
|
|
6
|
+
import { ruleCatalog, ruleId, type ImplementedRuleSlug } from './catalog.js'
|
|
7
|
+
|
|
8
|
+
export type AnalyzerRule = {
|
|
9
|
+
slug: ImplementedRuleSlug
|
|
10
|
+
analyze(project: DomainProject): readonly LintDiagnostic[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function analyzerDiagnostic(
|
|
14
|
+
slug: ImplementedRuleSlug,
|
|
15
|
+
file: SourceFile,
|
|
16
|
+
token: SourceToken,
|
|
17
|
+
detail?: string,
|
|
18
|
+
): LintDiagnostic | undefined {
|
|
19
|
+
const info = ruleCatalog[slug]
|
|
20
|
+
const id = ruleId(slug)
|
|
21
|
+
if (isSuppressed(file, token.line, id)) return undefined
|
|
22
|
+
return {
|
|
23
|
+
id,
|
|
24
|
+
severity: 'error',
|
|
25
|
+
message: detail ? `${info.message} ${detail}` : info.message,
|
|
26
|
+
help: info.help,
|
|
27
|
+
url: info.url,
|
|
28
|
+
owner: info.owner,
|
|
29
|
+
location: {
|
|
30
|
+
path: file.relativePath,
|
|
31
|
+
line: token.line,
|
|
32
|
+
column: token.column,
|
|
33
|
+
offset: token.offset,
|
|
34
|
+
length: token.length,
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { LintDiagnostic } from '../diagnostic.js'
|
|
2
|
+
import type { DomainProject } from '../project.js'
|
|
3
|
+
|
|
4
|
+
import { importedCalls } from '../source.js'
|
|
5
|
+
import { analyzerDiagnostic, type AnalyzerRule } from './project-rule.js'
|
|
6
|
+
|
|
7
|
+
const schemaModules = new Set(['@astrale-os/kernel-core', '@astrale-os/kernel-dsl'])
|
|
8
|
+
const memberConstructors = new Set([
|
|
9
|
+
'edgeClass',
|
|
10
|
+
'edgeInterface',
|
|
11
|
+
'fn',
|
|
12
|
+
'func',
|
|
13
|
+
'nodeClass',
|
|
14
|
+
'nodeInterface',
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
export const thinSchemaCompositionRoot: AnalyzerRule = {
|
|
18
|
+
slug: 'thin-schema-composition-root',
|
|
19
|
+
analyze(project: DomainProject): readonly LintDiagnostic[] {
|
|
20
|
+
const root = project.files.find((file) => file.relativePath === 'schema/index.ts')
|
|
21
|
+
if (!root) return []
|
|
22
|
+
return importedCalls(root, schemaModules, memberConstructors)
|
|
23
|
+
.map((token) => analyzerDiagnostic(this.slug, root, token, `Found ${token.value}().`))
|
|
24
|
+
.filter((diagnostic): diagnostic is LintDiagnostic => diagnostic !== undefined)
|
|
25
|
+
},
|
|
26
|
+
}
|