@dokebi/cli 0.4.2 → 0.4.27
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/AI.md +279 -57
- package/README.md +35 -10
- package/catalog.json +2520 -258
- package/describe.schema.json +748 -0
- package/dist/api-app.d.ts +2 -0
- package/dist/api-app.js +35 -0
- package/dist/app-urls.js +3 -2
- package/dist/bin.js +638 -37
- package/dist/capability-guide.d.ts +30 -0
- package/dist/capability-guide.js +122 -0
- package/dist/catalog.d.ts +16 -2
- package/dist/catalog.js +19 -7
- package/dist/check/asset-rules.d.ts +6 -0
- package/dist/check/asset-rules.js +163 -0
- package/dist/check/browser-sources.d.ts +20 -0
- package/dist/check/browser-sources.js +89 -0
- package/dist/check/console-rules.d.ts +6 -0
- package/dist/check/console-rules.js +40 -0
- package/dist/check/error-rules.d.ts +21 -0
- package/dist/check/error-rules.js +212 -0
- package/dist/check/form-rules.d.ts +12 -0
- package/dist/check/form-rules.js +69 -6
- package/dist/check/formatter-rules.d.ts +2 -0
- package/dist/check/formatter-rules.js +71 -0
- package/dist/check/i18n-rules.d.ts +27 -0
- package/dist/check/i18n-rules.js +353 -0
- package/dist/check/index.d.ts +15 -1
- package/dist/check/index.js +89 -11
- package/dist/check/mail-rules.d.ts +4 -0
- package/dist/check/mail-rules.js +72 -0
- package/dist/check/module-rules.d.ts +13 -0
- package/dist/check/module-rules.js +104 -0
- package/dist/check/packages.d.ts +8 -0
- package/dist/check/packages.js +16 -0
- package/dist/check/pool-rules.d.ts +8 -0
- package/dist/check/pool-rules.js +164 -0
- package/dist/check/problems.d.ts +6 -1
- package/dist/check/problems.js +100 -2
- package/dist/check/project-rules.d.ts +23 -3
- package/dist/check/project-rules.js +265 -23
- package/dist/check/seal-rules.d.ts +6 -0
- package/dist/check/seal-rules.js +68 -0
- package/dist/check/security-rules.d.ts +10 -0
- package/dist/check/security-rules.js +450 -0
- package/dist/check/shared-state-rules.d.ts +15 -0
- package/dist/check/shared-state-rules.js +117 -0
- package/dist/check/source-rules.d.ts +26 -3
- package/dist/check/source-rules.js +249 -28
- package/dist/check/structure-rules.d.ts +23 -0
- package/dist/check/structure-rules.js +502 -0
- package/dist/check/timer-rules.d.ts +7 -0
- package/dist/check/timer-rules.js +543 -0
- package/dist/check/typescript.d.ts +35 -5
- package/dist/check/typescript.js +163 -17
- package/dist/check/vite-rules.d.ts +2 -0
- package/dist/check/vite-rules.js +136 -0
- package/dist/check/wait-rules.d.ts +16 -0
- package/dist/check/wait-rules.js +337 -0
- package/dist/commands.d.ts +7 -0
- package/dist/commands.js +381 -36
- package/dist/config-diff.d.ts +42 -0
- package/dist/config-diff.js +289 -0
- package/dist/config-edit.d.ts +35 -2
- package/dist/config-edit.js +145 -13
- package/dist/config-init.d.ts +15 -0
- package/dist/config-init.js +61 -0
- package/dist/config-paths.d.ts +22 -0
- package/dist/config-paths.js +82 -0
- package/dist/config-secrets.d.ts +46 -0
- package/dist/config-secrets.js +187 -0
- package/dist/config-show.d.ts +82 -0
- package/dist/config-show.js +563 -0
- package/dist/config-values.d.ts +8 -0
- package/dist/config-values.js +68 -0
- package/dist/configuration-scaffold.d.ts +42 -0
- package/dist/configuration-scaffold.js +258 -4
- package/dist/configuration.d.ts +63 -3
- package/dist/configuration.js +178 -4
- package/dist/deploy-scripts.d.ts +4 -0
- package/dist/deploy-scripts.js +361 -0
- package/dist/deploy-templates.d.ts +46 -0
- package/dist/deploy-templates.js +379 -0
- package/dist/deploy.d.ts +46 -0
- package/dist/deploy.js +592 -0
- package/dist/deployment.d.ts +2 -0
- package/dist/deployment.js +111 -0
- package/dist/describe-db.d.ts +22 -0
- package/dist/describe-db.js +176 -0
- package/dist/describe.d.ts +92 -0
- package/dist/describe.js +417 -0
- package/dist/dev-compose.d.ts +20 -0
- package/dist/dev-compose.js +154 -0
- package/dist/dev-guard.js +44 -0
- package/dist/dev-host.d.ts +13 -0
- package/dist/dev-host.js +55 -0
- package/dist/dev-infra.d.ts +72 -4
- package/dist/dev-infra.js +237 -17
- package/dist/dev-watch.d.ts +34 -0
- package/dist/dev-watch.js +192 -0
- package/dist/diagnostics.d.ts +10 -7
- package/dist/diagnostics.js +22 -4
- package/dist/drift.d.ts +50 -0
- package/dist/drift.js +218 -0
- package/dist/env-file.d.ts +25 -1
- package/dist/env-file.js +64 -6
- package/dist/errors.d.ts +7 -1
- package/dist/example-config.d.ts +2 -0
- package/dist/example-config.js +3 -0
- package/dist/first-admin.d.ts +43 -0
- package/dist/first-admin.js +175 -0
- package/dist/formatter-ignore.d.ts +37 -0
- package/dist/formatter-ignore.js +344 -0
- package/dist/generator.d.ts +45 -3
- package/dist/generator.js +205 -15
- package/dist/guide.d.ts +1 -1
- package/dist/guide.js +42 -21
- package/dist/install-module.d.ts +20 -2
- package/dist/install-module.js +45 -8
- package/dist/install-plan.js +5 -3
- package/dist/layout.d.ts +12 -1
- package/dist/layout.js +19 -6
- package/dist/log-writer-control.d.ts +2 -0
- package/dist/log-writer-control.js +17 -2
- package/dist/log-writer.js +43 -10
- package/dist/logging-config.d.ts +8 -3
- package/dist/logging-config.js +19 -5
- package/dist/manifest.d.ts +29 -1
- package/dist/manifest.js +84 -6
- package/dist/merge-install-defaults.d.ts +0 -1
- package/dist/merge-install-defaults.js +13 -47
- package/dist/npm-client.d.ts +40 -4
- package/dist/npm-client.js +152 -15
- package/dist/npm-paths.js +18 -4
- package/dist/openapi.d.ts +27 -0
- package/dist/openapi.js +649 -0
- package/dist/option-fixes.d.ts +39 -0
- package/dist/option-fixes.js +130 -0
- package/dist/output.d.ts +1 -1
- package/dist/output.js +5 -5
- package/dist/plugin-lifecycle.d.ts +34 -2
- package/dist/plugin-lifecycle.js +195 -16
- package/dist/plugin-options.d.ts +19 -4
- package/dist/plugin-options.js +138 -6
- package/dist/plugins-command.js +44 -7
- package/dist/process-group.d.ts +39 -0
- package/dist/process-group.js +132 -0
- package/dist/production-doctor.d.ts +3 -0
- package/dist/production-doctor.js +176 -0
- package/dist/project-guide.d.ts +19 -1
- package/dist/project-guide.js +119 -20
- package/dist/project-inspector.d.ts +15 -0
- package/dist/project-inspector.js +497 -33
- package/dist/project-state.d.ts +5 -0
- package/dist/project-state.js +8 -0
- package/dist/project.d.ts +203 -19
- package/dist/project.js +3006 -333
- package/dist/recipe-facts.d.ts +127 -0
- package/dist/recipe-facts.js +563 -0
- package/dist/recipe-login.d.ts +63 -0
- package/dist/recipe-login.js +464 -0
- package/dist/recipe-mail.d.ts +21 -0
- package/dist/recipe-mail.js +214 -0
- package/dist/recipe-members.d.ts +35 -0
- package/dist/recipe-members.js +178 -0
- package/dist/recipe-record.d.ts +82 -7
- package/dist/recipe-record.js +226 -22
- package/dist/recipe-reset.d.ts +2 -0
- package/dist/recipe-reset.js +117 -0
- package/dist/recipe-site.d.ts +48 -0
- package/dist/recipe-site.js +396 -0
- package/dist/recipe-step.d.ts +42 -0
- package/dist/recipe-step.js +89 -0
- package/dist/recipe.d.ts +56 -20
- package/dist/recipe.js +703 -131
- package/dist/registry.d.ts +98 -2
- package/dist/registry.js +171 -5
- package/dist/reloadable.d.ts +35 -0
- package/dist/reloadable.js +257 -0
- package/dist/remote-verify.d.ts +31 -0
- package/dist/remote-verify.js +204 -0
- package/dist/server.d.ts +43 -4
- package/dist/server.js +601 -52
- package/dist/settings.d.ts +48 -0
- package/dist/settings.js +168 -0
- package/dist/site-auth.d.ts +39 -0
- package/dist/site-auth.js +195 -0
- package/dist/template-fixes.d.ts +26 -0
- package/dist/template-fixes.js +285 -0
- package/dist/test-reporter.d.ts +49 -0
- package/dist/test-reporter.js +190 -0
- package/dist/transaction.d.ts +47 -0
- package/dist/transaction.js +241 -7
- package/dist/verify-tests.d.ts +98 -0
- package/dist/verify-tests.js +449 -0
- package/dist/verify.d.ts +103 -3
- package/dist/verify.js +272 -21
- package/dist/view-choice.d.ts +58 -0
- package/dist/view-choice.js +131 -0
- package/dist/vite-watch.js +4 -2
- package/package.json +8 -6
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// Source rules of `dokebi check` (D-0072): one walk over each file's TypeScript 7 AST. Each rule
|
|
2
2
|
// matches only shapes that are wrong whatever the surrounding code is (no heuristics on names):
|
|
3
3
|
// empty-catch `catch {}`, `.catch(() => {})`, `.then(ok, () => {})`, an empty
|
|
4
|
-
// `on('error', …)` listener
|
|
4
|
+
// `on('error', …)` listener, and the same handlers that only give back
|
|
5
|
+
// nothing (`() => undefined`, `() => null`, `() => void 0`) (D-0047)
|
|
6
|
+
// missing-cause a catch block that throws a new error and never reads the one it
|
|
7
|
+
// caught (`catch { throw new X() }`): the original error is lost
|
|
8
|
+
// (D-0047, D-0118)
|
|
5
9
|
// floating-promise an expression statement whose call returns a Promise (the checker's
|
|
6
10
|
// type) that is neither awaited, returned, `void`ed nor `.catch`ed;
|
|
7
11
|
// node:test's test()/describe() are exempt (their promise never rejects)
|
|
@@ -9,9 +13,27 @@
|
|
|
9
13
|
// values, parameter properties, decorators, `import x =`, `export =`, `<T>x`
|
|
10
14
|
// private-import an `@dokebi/*` path outside the package exports (packages.ts)
|
|
11
15
|
// missing-package an `@dokebi/*` package that is not installed, absent or unknown
|
|
16
|
+
// and, when their plugin is installed, the security rules of security-rules.ts (D-0076 §6), and
|
|
17
|
+
// when src/shared/errors.ts declares the app's codes, unknown-error-code (error-rules.ts, D-0077),
|
|
18
|
+
// and in server code background-timer (timer-rules.ts), console-output (console-rules.ts,
|
|
19
|
+
// D-0151) and the structure warnings
|
|
20
|
+
// responsibility-size and memory-filtered-page (structure-rules.ts, D-0105), and
|
|
21
|
+
// module-point-not-installed (module-rules.ts, D-0130).
|
|
22
|
+
// Browser code outside the program (.vue scripts, views/ .js; browser-sources.ts, D-0120) gets
|
|
23
|
+
// only empty-catch, missing-cause and floating-promise, typed by its own program.
|
|
12
24
|
import { relative, sep } from 'node:path';
|
|
13
|
-
import {
|
|
25
|
+
import { assetRouteRules } from './asset-rules.js';
|
|
26
|
+
import { consoleRules } from './console-rules.js';
|
|
27
|
+
import { ERRORS_FILE, errorCodeRules, isAppCode, readErrorList, } from './error-rules.js';
|
|
28
|
+
import { mailRules } from './mail-rules.js';
|
|
29
|
+
import { modulePoints } from './module-rules.js';
|
|
30
|
+
import { poolRules } from './pool-rules.js';
|
|
31
|
+
import { BROWSER_RULES, MARKABLE, WARNING_RULES } from './problems.js';
|
|
32
|
+
import { securityRules } from './security-rules.js';
|
|
33
|
+
import { layerOf, structureRules } from './structure-rules.js';
|
|
34
|
+
import { isServerCode, timerRules } from './timer-rules.js';
|
|
14
35
|
import { field, nodes, property, } from './typescript.js';
|
|
36
|
+
import { ownerCapRules, queueRules, waitRules } from './wait-rules.js';
|
|
15
37
|
const LISTEN = new Set([
|
|
16
38
|
'on',
|
|
17
39
|
'once',
|
|
@@ -22,7 +44,7 @@ const LISTEN = new Set([
|
|
|
22
44
|
]);
|
|
23
45
|
const SNIPPET = 60;
|
|
24
46
|
/** `// dokebi-check: allow <rule> <reason>` (also inside a block comment). */
|
|
25
|
-
const MARKER = /dokebi-check:\s*allow\s+([a-
|
|
47
|
+
const MARKER = /dokebi-check:\s*allow\s+([a-z0-9-]+)[ \t]*(?:[:\-—–][ \t]*)?([^\n]*?)[ \t]*(?:\*\/|$)/gm;
|
|
26
48
|
export function readMarkers(text) {
|
|
27
49
|
const markers = [];
|
|
28
50
|
let lineStart = 0;
|
|
@@ -43,16 +65,31 @@ export function readMarkers(text) {
|
|
|
43
65
|
return markers;
|
|
44
66
|
}
|
|
45
67
|
/** Walks one file: all syntactic problems, and the statements the checker must judge. */
|
|
46
|
-
export async function inspectFile(file, kinds, root, imports
|
|
47
|
-
|
|
68
|
+
export async function inspectFile(file, kinds, root, imports,
|
|
69
|
+
/** Installed plugin ids: the security rules run only for the plugin they protect. */
|
|
70
|
+
installed = new Set(),
|
|
71
|
+
/** The codes of src/shared/errors.ts (D-0077 D); unknown-error-code runs only with them. */
|
|
72
|
+
errorList,
|
|
73
|
+
/**
|
|
74
|
+
* D-0120: a virtual browser source — the file problems name, and the only rules that run
|
|
75
|
+
* (the per-plugin, server and import rules do not apply to browser code).
|
|
76
|
+
*/
|
|
77
|
+
browser) {
|
|
78
|
+
const path = browser?.path ?? relative(root, file.fileName.split('/').join(sep)).split(sep).join('/');
|
|
48
79
|
const text = file.text;
|
|
49
80
|
const markers = readMarkers(text);
|
|
50
81
|
const lineOf = (position) => file.getLineAndCharacterOfPosition(position).line + 1;
|
|
51
82
|
const problems = [];
|
|
52
83
|
const candidates = [];
|
|
84
|
+
const untypedImports = [];
|
|
53
85
|
const specifiers = [];
|
|
86
|
+
/** console-output (D-0151): a marker before the first statement covers the whole file. */
|
|
87
|
+
const firstStatement = nodes(file, 'statements')[0];
|
|
88
|
+
const headerEnd = firstStatement ? lineOf(firstStatement.getStart()) : Number.POSITIVE_INFINITY;
|
|
54
89
|
/** The problem, or nothing when a marker with a reason sits on or just above its lines. */
|
|
55
90
|
const report = (node, rule, message, fix) => {
|
|
91
|
+
if (browser && !browser.rules.has(rule))
|
|
92
|
+
return undefined;
|
|
56
93
|
const first = lineOf(node.getStart());
|
|
57
94
|
const last = lineOf(Math.max(node.getStart(), node.end - 1));
|
|
58
95
|
let note = '';
|
|
@@ -60,12 +97,21 @@ export async function inspectFile(file, kinds, root, imports) {
|
|
|
60
97
|
for (const marker of markers)
|
|
61
98
|
if (marker.rule === rule &&
|
|
62
99
|
marker.line <= last &&
|
|
63
|
-
(marker.line >= first ||
|
|
100
|
+
(marker.line >= first ||
|
|
101
|
+
(marker.line === first - 1 && marker.ownLine) ||
|
|
102
|
+
(rule === 'console-output' && marker.ownLine && marker.line < headerEnd))) {
|
|
64
103
|
if (marker.reason)
|
|
65
104
|
return undefined;
|
|
66
105
|
note = ' (the dokebi-check marker has no reason, so it does not count)';
|
|
67
106
|
}
|
|
68
|
-
const problem = {
|
|
107
|
+
const problem = {
|
|
108
|
+
rule,
|
|
109
|
+
...(WARNING_RULES.has(rule) ? { severity: 'warning' } : {}),
|
|
110
|
+
file: path,
|
|
111
|
+
line: first,
|
|
112
|
+
message: message + note,
|
|
113
|
+
fix,
|
|
114
|
+
};
|
|
69
115
|
return problem;
|
|
70
116
|
};
|
|
71
117
|
const push = (problem) => {
|
|
@@ -80,12 +126,57 @@ export async function inspectFile(file, kinds, root, imports) {
|
|
|
80
126
|
? String(property(node, 'text'))
|
|
81
127
|
: undefined;
|
|
82
128
|
const hasModifier = (node, kind) => nodes(node, 'modifiers').some((modifier) => modifier.kind === kind);
|
|
83
|
-
|
|
84
|
-
|
|
129
|
+
const unwrap = (node) => {
|
|
130
|
+
let current = node;
|
|
131
|
+
while (current?.kind === kinds.ParenthesizedExpression)
|
|
132
|
+
current = field(current, 'expression');
|
|
133
|
+
return current;
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* An expression that does nothing: `undefined`, `null`, or `void` of a name or a literal
|
|
137
|
+
* (`void 0`, `void error`). A call (`void log(error)`) or any other value is not one: a
|
|
138
|
+
* fallback value such as `false` is the handler's decision, not a drop.
|
|
139
|
+
*/
|
|
140
|
+
const nothing = (node) => {
|
|
141
|
+
const value = unwrap(node);
|
|
142
|
+
if (!value)
|
|
143
|
+
return false;
|
|
144
|
+
if (value.kind === kinds.NullKeyword)
|
|
145
|
+
return true;
|
|
146
|
+
if (value.kind === kinds.Identifier)
|
|
147
|
+
return property(value, 'text') === 'undefined';
|
|
148
|
+
if (value.kind !== kinds.VoidExpression)
|
|
149
|
+
return false;
|
|
150
|
+
const operand = unwrap(field(value, 'expression'));
|
|
151
|
+
return (operand !== undefined &&
|
|
152
|
+
(operand.kind === kinds.Identifier ||
|
|
153
|
+
operand.kind === kinds.NullKeyword ||
|
|
154
|
+
operand.kind === kinds.TrueKeyword ||
|
|
155
|
+
operand.kind === kinds.FalseKeyword ||
|
|
156
|
+
operand.kind === kinds.NumericLiteral ||
|
|
157
|
+
operand.kind === kinds.StringLiteral ||
|
|
158
|
+
operand.kind === kinds.NoSubstitutionTemplateLiteral));
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* An arrow or function expression that drops what it gets (D-0047): an empty block, a block
|
|
162
|
+
* that only returns nothing (`return;`, `return undefined;`, `return null;`), or an arrow whose
|
|
163
|
+
* body is nothing (`() => undefined`, `() => null`, `() => void 0`).
|
|
164
|
+
*/
|
|
165
|
+
const emptyFunction = (argument) => {
|
|
166
|
+
const node = unwrap(argument);
|
|
85
167
|
if (!node || (node.kind !== kinds.ArrowFunction && node.kind !== kinds.FunctionExpression))
|
|
86
168
|
return false;
|
|
87
169
|
const body = field(node, 'body');
|
|
88
|
-
|
|
170
|
+
if (body?.kind !== kinds.Block)
|
|
171
|
+
return nothing(body);
|
|
172
|
+
const statements = nodes(body, 'statements');
|
|
173
|
+
if (statements.length === 0)
|
|
174
|
+
return true;
|
|
175
|
+
const only = statements.length === 1 ? statements[0] : undefined;
|
|
176
|
+
if (only?.kind !== kinds.ReturnStatement)
|
|
177
|
+
return false;
|
|
178
|
+
const returned = field(only, 'expression');
|
|
179
|
+
return returned === undefined || nothing(returned);
|
|
89
180
|
};
|
|
90
181
|
/** The method name of `a.b(...)`, if the callee is a property access. */
|
|
91
182
|
const methodName = (call) => {
|
|
@@ -95,12 +186,6 @@ export async function inspectFile(file, kinds, root, imports) {
|
|
|
95
186
|
const name = field(callee, 'name');
|
|
96
187
|
return name?.kind === kinds.Identifier ? String(property(name, 'text')) : undefined;
|
|
97
188
|
};
|
|
98
|
-
const unwrap = (node) => {
|
|
99
|
-
let current = node;
|
|
100
|
-
while (current?.kind === kinds.ParenthesizedExpression)
|
|
101
|
-
current = field(current, 'expression');
|
|
102
|
-
return current;
|
|
103
|
-
};
|
|
104
189
|
/** Namespaces with only types (or `declare`) are erased; anything else makes a value. */
|
|
105
190
|
const instantiated = (body) => {
|
|
106
191
|
if (!body)
|
|
@@ -146,26 +231,95 @@ export async function inspectFile(file, kinds, root, imports) {
|
|
|
146
231
|
current = field(current, 'expression');
|
|
147
232
|
return current?.kind === kinds.Identifier ? String(property(current, 'text')) : undefined;
|
|
148
233
|
};
|
|
234
|
+
const functionKinds = new Set([
|
|
235
|
+
kinds.ArrowFunction,
|
|
236
|
+
kinds.FunctionExpression,
|
|
237
|
+
kinds.FunctionDeclaration,
|
|
238
|
+
kinds.MethodDeclaration,
|
|
239
|
+
kinds.Constructor,
|
|
240
|
+
kinds.GetAccessor,
|
|
241
|
+
kinds.SetAccessor,
|
|
242
|
+
kinds.ClassDeclaration,
|
|
243
|
+
kinds.ClassExpression,
|
|
244
|
+
]);
|
|
245
|
+
/**
|
|
246
|
+
* D-0118: a catch block that throws a new error (`throw new X(…)`, or `throw fail(…)` — a call
|
|
247
|
+
* makes a new error too, AI-M46; outside nested functions) and never reads the error it caught —
|
|
248
|
+
* no binding, or a plain name that is never used. A destructured binding counts as read.
|
|
249
|
+
*/
|
|
250
|
+
const losesCause = (clause, block) => {
|
|
251
|
+
const declaration = field(clause, 'variableDeclaration');
|
|
252
|
+
const binding = declaration && field(declaration, 'name');
|
|
253
|
+
if (binding && binding.kind !== kinds.Identifier)
|
|
254
|
+
return false;
|
|
255
|
+
const name = binding ? String(property(binding, 'text')) : undefined;
|
|
256
|
+
let throwsNew = false;
|
|
257
|
+
let reads = false;
|
|
258
|
+
const walk = (current, inFunction) => {
|
|
259
|
+
if (current.kind === kinds.ThrowStatement && !inFunction) {
|
|
260
|
+
const thrown = unwrap(field(current, 'expression'));
|
|
261
|
+
if (thrown?.kind === kinds.NewExpression || thrown?.kind === kinds.CallExpression)
|
|
262
|
+
throwsNew = true;
|
|
263
|
+
}
|
|
264
|
+
if (name !== undefined &&
|
|
265
|
+
current.kind === kinds.Identifier &&
|
|
266
|
+
property(current, 'text') === name)
|
|
267
|
+
reads = true;
|
|
268
|
+
const nested = inFunction || functionKinds.has(current.kind);
|
|
269
|
+
current.forEachChild((child) => walk(child, nested));
|
|
270
|
+
return undefined;
|
|
271
|
+
};
|
|
272
|
+
block.forEachChild((child) => walk(child, false));
|
|
273
|
+
return throwsNew && !reads;
|
|
274
|
+
};
|
|
275
|
+
/**
|
|
276
|
+
* The value names an import binds (not `import type`, not a .vue component: a component is
|
|
277
|
+
* never called, and a project without a shim types none). Judged after the walk.
|
|
278
|
+
*/
|
|
279
|
+
const untypedImport = (node, value) => {
|
|
280
|
+
const clause = field(node, 'importClause');
|
|
281
|
+
if (!clause || property(clause, 'isTypeOnly') === true)
|
|
282
|
+
return;
|
|
283
|
+
if (/\.(?:vue|css|scss|sass|less|svg|png|jpe?g|gif|webp|json)(?:\?.*)?$/.test(value))
|
|
284
|
+
return;
|
|
285
|
+
const bindings = field(clause, 'namedBindings');
|
|
286
|
+
const names = [
|
|
287
|
+
field(clause, 'name'),
|
|
288
|
+
bindings?.kind === kinds.NamespaceImport ? field(bindings, 'name') : undefined,
|
|
289
|
+
...(bindings && bindings.kind !== kinds.NamespaceImport
|
|
290
|
+
? nodes(bindings, 'elements')
|
|
291
|
+
.filter((element) => property(element, 'isTypeOnly') !== true)
|
|
292
|
+
.map((element) => field(element, 'name'))
|
|
293
|
+
: []),
|
|
294
|
+
].filter((name) => name?.kind === kinds.Identifier);
|
|
295
|
+
if (!names.length)
|
|
296
|
+
return;
|
|
297
|
+
const problem = report(node, 'untyped-browser-import', `'${value}' has no types in the browser program: floating-promise cannot judge the calls of what this file imports from it`, `make its types resolve as the bundler does (the package's types, a declaration file such as src/infra/inertia/page.d.ts, or a relative path that exists); when the module really has no types, write // dokebi-check: allow untyped-browser-import <reason>`);
|
|
298
|
+
if (problem)
|
|
299
|
+
untypedImports.push({ names, problem });
|
|
300
|
+
};
|
|
149
301
|
const visit = (node) => {
|
|
150
302
|
const kind = node.kind;
|
|
151
303
|
if (kind === kinds.CatchClause) {
|
|
152
304
|
const block = field(node, 'block');
|
|
153
305
|
if (block && nodes(block, 'statements').length === 0)
|
|
154
306
|
push(report(node, 'empty-catch', 'An empty catch block hides the error (D-0047: no silent catch)', 'rethrow it, turn it into a coded error for the caller, or log it at error level (fixed message, code, error); if ignoring is really right, write // dokebi-check: allow empty-catch <reason> in the block'));
|
|
307
|
+
else if (block && losesCause(node, block))
|
|
308
|
+
push(report(node, 'missing-cause', 'This catch throws a new error and drops the one it caught: the original message and stack are lost (D-0047)', "name the caught error and pass it on: catch (error) { throw new X(…, { cause: error }) } (or the error class's cause argument); if the original must not travel, log it at debug first, or write // dokebi-check: allow missing-cause <reason>"));
|
|
155
309
|
}
|
|
156
310
|
else if (kind === kinds.CallExpression) {
|
|
157
311
|
const args = nodes(node, 'arguments');
|
|
158
312
|
const method = methodName(node);
|
|
159
313
|
const callee = field(node, 'expression');
|
|
160
314
|
if (method === 'catch' && emptyFunction(args[0]))
|
|
161
|
-
push(report(node, 'empty-catch', `${snippet(node)} drops the rejection with
|
|
315
|
+
push(report(node, 'empty-catch', `${snippet(node)} drops the rejection with a handler that does nothing (D-0047)`, 'handle or log the error in the handler (error level, code, error); to keep only an unhandled-rejection guard on a promise you also return, write // dokebi-check: allow empty-catch <reason>'));
|
|
162
316
|
else if (method === 'then' && emptyFunction(args[1]))
|
|
163
|
-
push(report(node, 'empty-catch', `${snippet(node)} drops the rejection with an
|
|
317
|
+
push(report(node, 'empty-catch', `${snippet(node)} drops the rejection with an onRejected handler that does nothing (D-0047)`, 'handle or log the error in the second handler, or remove it and let the caller see the rejection'));
|
|
164
318
|
else if (method &&
|
|
165
319
|
LISTEN.has(method) &&
|
|
166
320
|
stringValue(args[0]) === 'error' &&
|
|
167
321
|
emptyFunction(args[1]))
|
|
168
|
-
push(report(node, 'empty-catch', `
|
|
322
|
+
push(report(node, 'empty-catch', `A '${method}('error', …)' listener that does nothing hides every error of that emitter (D-0047)`, 'log the error (error level, code, error) and close or replace the resource; write // dokebi-check: allow empty-catch <reason> only for a deliberate drop'));
|
|
169
323
|
if (callee?.kind === kinds.ImportKeyword) {
|
|
170
324
|
const value = stringValue(args[0]);
|
|
171
325
|
if (value !== undefined)
|
|
@@ -183,7 +337,11 @@ export async function inspectFile(file, kinds, root, imports) {
|
|
|
183
337
|
(method === 'then' && args.length >= 2) ||
|
|
184
338
|
(expression.kind === kinds.CallExpression && testNames.has(calleeRoot(expression) ?? ''));
|
|
185
339
|
if (!handled) {
|
|
186
|
-
const problem = report(node, 'floating-promise',
|
|
340
|
+
const problem = report(node, 'floating-promise', browser
|
|
341
|
+
? `${snippet(expression)} returns a Promise that nothing awaits: its rejection becomes an unhandled rejection in the browser that the page never handles or shows`
|
|
342
|
+
: `${snippet(expression)} returns a Promise that nothing awaits: its rejection becomes an unhandled rejection and stops the process`, browser
|
|
343
|
+
? 'await it (or return it to the caller); for deliberate background work end it with .catch(error => <show or report the error>)'
|
|
344
|
+
: 'await it (or return it to the caller); for deliberate background work end it with .catch(error => <log at error level>)');
|
|
187
345
|
if (problem)
|
|
188
346
|
candidates.push({ expression, problem });
|
|
189
347
|
}
|
|
@@ -231,6 +389,8 @@ export async function inspectFile(file, kinds, root, imports) {
|
|
|
231
389
|
const value = stringValue(specifier);
|
|
232
390
|
if (value !== undefined && specifier)
|
|
233
391
|
specifiers.push({ node: specifier, value });
|
|
392
|
+
if (browser && kind === kinds.ImportDeclaration && value !== undefined)
|
|
393
|
+
untypedImport(node, value);
|
|
234
394
|
}
|
|
235
395
|
else if (kind === kinds.ImportType) {
|
|
236
396
|
const argument = field(node, 'argument');
|
|
@@ -243,31 +403,92 @@ export async function inspectFile(file, kinds, root, imports) {
|
|
|
243
403
|
return undefined;
|
|
244
404
|
};
|
|
245
405
|
file.forEachChild(visit);
|
|
406
|
+
if (browser)
|
|
407
|
+
return { problems, candidates, imports: untypedImports };
|
|
408
|
+
if (installed.has('dokebi-mail'))
|
|
409
|
+
mailRules(file, kinds, (node, rule, message, fix) => push(report(node, rule, message, fix)));
|
|
410
|
+
// D-0105: periodic work on the app's own timers, and files that carry several responsibilities.
|
|
411
|
+
if (isServerCode(path)) {
|
|
412
|
+
timerRules(file, kinds, (node, message, fix, rule = 'background-timer') => push(report(node, rule, message, fix)));
|
|
413
|
+
consoleRules(file, kinds, (node, message, fix) => push(report(node, 'console-output', message, fix)));
|
|
414
|
+
waitRules(file, kinds, (node, message, fix) => push(report(node, 'wait-count-refusal', message, fix)));
|
|
415
|
+
queueRules(file, kinds, (node, message, fix) => push(report(node, 'in-process-queue', message, fix)));
|
|
416
|
+
ownerCapRules(file, kinds, (node, message, fix) => push(report(node, 'fixed-owner-cap', message, fix)));
|
|
417
|
+
poolRules(file, path, kinds, (node, rule, message, fix) => push(report(node, rule, message, fix)));
|
|
418
|
+
assetRouteRules(file, kinds, (node, message, fix) => push(report(node, 'asset-route', message, fix)));
|
|
419
|
+
}
|
|
420
|
+
const layer = layerOf(path);
|
|
421
|
+
if (layer)
|
|
422
|
+
structureRules(file, layer, kinds, (node, rule, message, fix) => push(report(node, rule, message, fix)));
|
|
423
|
+
const auth = installed.has('dokebi-auth');
|
|
424
|
+
const csrf = installed.has('dokebi-csrf');
|
|
425
|
+
if (auth || csrf)
|
|
426
|
+
securityRules(file, kinds, { auth, csrf }, (node, rule, message, fix) => push(report(node, rule, message, fix)));
|
|
427
|
+
if (errorList && isAppCode(path))
|
|
428
|
+
errorCodeRules(file, path, kinds, errorList, (node, message, fix) => push(report(node, 'unknown-error-code', message, fix)));
|
|
246
429
|
for (const { node, value } of specifiers) {
|
|
247
430
|
const verdict = await imports.verdict(file.fileName, value);
|
|
248
431
|
if (verdict)
|
|
249
432
|
push(report(node, verdict.rule, verdict.message, verdict.fix));
|
|
250
433
|
}
|
|
434
|
+
// D-0130: a module point whose plugin package is present but not installed in the project.
|
|
435
|
+
for (const point of modulePoints(file, kinds)) {
|
|
436
|
+
const plugin = await imports.pluginOf(file.fileName, point.specifier);
|
|
437
|
+
if (!plugin || installed.has(plugin.id))
|
|
438
|
+
continue;
|
|
439
|
+
push(report(point.node, 'module-point-not-installed', `Module ${point.module || '(unnamed)'} contributes to ${point.point} of ${plugin.package}, but this project has not installed ${plugin.id}: nothing registers the plugin that collects it, so the app drops the values (warn POINT_NOT_COLLECTED)`, `npx dokebi add ${plugin.package} --dry-run --json, then without --dry-run (read its AI.md first); or remove ${point.point} from contributes. When you register the plugin yourself, write // dokebi-check: allow module-point-not-installed <reason>`));
|
|
440
|
+
}
|
|
251
441
|
return { problems, candidates };
|
|
252
442
|
}
|
|
253
443
|
/** Every program file: syntactic problems, then one batched checker call for the candidates. */
|
|
254
|
-
export async function sourceProblems(typescript, root, imports, skip) {
|
|
444
|
+
export async function sourceProblems(typescript, root, imports, skip, installed = new Set()) {
|
|
255
445
|
const problems = [];
|
|
256
446
|
const candidates = [];
|
|
257
447
|
let files = 0;
|
|
448
|
+
const pathOf = (file) => relative(root, file.fileName.split('/').join(sep)).split(sep).join('/');
|
|
449
|
+
const errorsFile = typescript.files.find((file) => pathOf(file) === ERRORS_FILE);
|
|
450
|
+
const errorList = errorsFile ? readErrorList(errorsFile, typescript.kinds) : undefined;
|
|
258
451
|
for (const file of typescript.files) {
|
|
259
|
-
const path =
|
|
452
|
+
const path = pathOf(file);
|
|
260
453
|
if (skip(path))
|
|
261
454
|
continue;
|
|
262
455
|
files++;
|
|
263
|
-
const found = await inspectFile(file, typescript.kinds, root, imports);
|
|
456
|
+
const found = await inspectFile(file, typescript.kinds, root, imports, installed, errorList);
|
|
264
457
|
problems.push(...found.problems);
|
|
265
458
|
candidates.push(...found.candidates);
|
|
266
459
|
}
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
460
|
+
const browserCandidates = [];
|
|
461
|
+
const browserImports = [];
|
|
462
|
+
for (const { file, path } of typescript.browser) {
|
|
463
|
+
if (skip(path))
|
|
464
|
+
continue;
|
|
465
|
+
files++;
|
|
466
|
+
const found = await inspectFile(file, typescript.kinds, root, imports, installed, undefined, {
|
|
467
|
+
path,
|
|
468
|
+
rules: BROWSER_RULES,
|
|
469
|
+
});
|
|
470
|
+
problems.push(...found.problems);
|
|
471
|
+
browserCandidates.push(...found.candidates);
|
|
472
|
+
browserImports.push(...(found.imports ?? []));
|
|
473
|
+
}
|
|
474
|
+
// D-0120 §1.4: an import none of whose names has a type is reported, never passed silently.
|
|
475
|
+
const untyped = typescript.untyped(browserImports.flatMap((item) => item.names), true);
|
|
476
|
+
let at = 0;
|
|
477
|
+
for (const item of browserImports) {
|
|
478
|
+
const verdicts = untyped.slice(at, at + item.names.length);
|
|
479
|
+
at += item.names.length;
|
|
480
|
+
if (verdicts.every(Boolean))
|
|
481
|
+
problems.push(item.problem);
|
|
482
|
+
}
|
|
483
|
+
for (const [list, browser] of [
|
|
484
|
+
[candidates, false],
|
|
485
|
+
[browserCandidates, true],
|
|
486
|
+
]) {
|
|
487
|
+
const verdicts = typescript.promises(list.map((candidate) => candidate.expression), browser);
|
|
488
|
+
list.forEach((candidate, index) => {
|
|
489
|
+
if (verdicts[index])
|
|
490
|
+
problems.push(candidate.problem);
|
|
491
|
+
});
|
|
492
|
+
}
|
|
272
493
|
return { problems, files };
|
|
273
494
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Kinds, type TsNode, type TsSourceFile } from './typescript.js';
|
|
2
|
+
export type StructureRule = 'responsibility-size' | 'memory-filtered-page';
|
|
3
|
+
export type Report = (node: TsNode, rule: StructureRule, message: string, fix: string) => void;
|
|
4
|
+
/** D-0105 §3: the smallest measured model that carried several features had 486 code lines. */
|
|
5
|
+
export declare const MODEL_LINES = 400;
|
|
6
|
+
export declare const MODEL_TABLES = 8;
|
|
7
|
+
/**
|
|
8
|
+
* D-0105 §3: the controllers that took every route of their app had 22–34 handlers (176–467
|
|
9
|
+
* code lines); every other controller measured had at most 16 handlers and 214 code lines.
|
|
10
|
+
*/
|
|
11
|
+
export declare const CONTROLLER_HANDLERS = 20;
|
|
12
|
+
export declare const CONTROLLER_LINES = 300;
|
|
13
|
+
/** D-0105 §3: services that only forwarded had 10–15 such methods out of 15–21. */
|
|
14
|
+
export declare const SERVICE_FORWARDS = 5;
|
|
15
|
+
export declare const FORWARD_SHARE = 0.6;
|
|
16
|
+
export type Layer = 'model' | 'controller' | 'service';
|
|
17
|
+
/** The layer of an app file by its folder (D-0031, D-0032), or undefined outside them. */
|
|
18
|
+
export declare function layerOf(path: string): Layer | undefined;
|
|
19
|
+
/** Lines with code: comments and blank lines do not count (string contents do, SQL is code). */
|
|
20
|
+
export declare function codeLines(text: string): number;
|
|
21
|
+
/** Table names in one piece of SQL text (EXTRACT(… FROM x) and CTE names excluded). */
|
|
22
|
+
export declare function sqlTables(sql: string, into: Set<string>): void;
|
|
23
|
+
export declare function structureRules(file: TsSourceFile, layer: Layer, kinds: Kinds, report: Report): void;
|