@agjs/tsforge 0.1.0
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/bin/tsforge.js +2 -0
- package/package.json +35 -0
- package/src/agent/agent.constants.ts +382 -0
- package/src/agent/agent.types.ts +34 -0
- package/src/agent/index.ts +4 -0
- package/src/agent/model-agent.ts +297 -0
- package/src/agent/tool-repair.ts +194 -0
- package/src/agent/tools.ts +190 -0
- package/src/browser/checks.ts +96 -0
- package/src/browser/index.ts +8 -0
- package/src/browser/oracle.ts +303 -0
- package/src/classify.ts +48 -0
- package/src/cli.ts +1333 -0
- package/src/config/config.constants.ts +9 -0
- package/src/config/flags.ts +32 -0
- package/src/config/index.ts +8 -0
- package/src/config/tsforge-config.ts +301 -0
- package/src/constitution/baseline.ts +257 -0
- package/src/detect-gate.ts +498 -0
- package/src/eval/eval.types.ts +36 -0
- package/src/eval/index.ts +3 -0
- package/src/eval/judge.ts +62 -0
- package/src/eval/score.ts +39 -0
- package/src/files/create.ts +22 -0
- package/src/files/edit.ts +193 -0
- package/src/files/files.constants.ts +11 -0
- package/src/files/files.types.ts +81 -0
- package/src/files/hashline-format.ts +110 -0
- package/src/files/hashline.ts +689 -0
- package/src/files/index.ts +19 -0
- package/src/index.ts +8 -0
- package/src/inference/index.ts +6 -0
- package/src/inference/inference.constants.ts +34 -0
- package/src/inference/inference.types.ts +123 -0
- package/src/inference/openai-compatible.ts +113 -0
- package/src/inference/stream-guard.ts +161 -0
- package/src/inference/stream.ts +370 -0
- package/src/inference/transport.ts +78 -0
- package/src/inference/wire.ts +0 -0
- package/src/lib/fs/fs.ts +126 -0
- package/src/lib/fs/fs.types.ts +5 -0
- package/src/lib/fs/index.ts +3 -0
- package/src/lib/fs/process.ts +146 -0
- package/src/lib/guards/guards.ts +9 -0
- package/src/lib/guards/index.ts +1 -0
- package/src/lib/json/index.ts +1 -0
- package/src/lib/json/json.ts +12 -0
- package/src/lib/scope/index.ts +2 -0
- package/src/lib/scope/scope.constants.ts +3 -0
- package/src/lib/scope/scope.ts +40 -0
- package/src/loop/astgrep-fix.ts +228 -0
- package/src/loop/feedback/feedback.ts +138 -0
- package/src/loop/feedback/index.ts +8 -0
- package/src/loop/feedback/meta-rule-docs.ts +41 -0
- package/src/loop/feedback/meta-rule-feedback.ts +61 -0
- package/src/loop/feedback/rule-docs.generated.json +112 -0
- package/src/loop/feedback/rule-docs.ts +342 -0
- package/src/loop/index.ts +19 -0
- package/src/loop/loop.constants.ts +68 -0
- package/src/loop/loop.types.ts +99 -0
- package/src/loop/prompt/index.ts +2 -0
- package/src/loop/prompt/project-map.ts +69 -0
- package/src/loop/prompt/prompt.ts +107 -0
- package/src/loop/quality.ts +174 -0
- package/src/loop/rule-docs.generated.json +367 -0
- package/src/loop/run-spec.ts +88 -0
- package/src/loop/run.ts +400 -0
- package/src/loop/session.ts +1410 -0
- package/src/loop/tools/add-dependency.ts +71 -0
- package/src/loop/tools/condense.ts +498 -0
- package/src/loop/tools/edit-hashline.ts +80 -0
- package/src/loop/tools/execute-tool.ts +80 -0
- package/src/loop/tools/file-ops.ts +323 -0
- package/src/loop/tools/index.ts +2 -0
- package/src/loop/tools/lsp-ops.ts +222 -0
- package/src/loop/tools/scaffold-routes.ts +68 -0
- package/src/loop/tools/scaffold-ui.ts +62 -0
- package/src/loop/tools/scaffold-web.ts +35 -0
- package/src/loop/tools/tool-context.ts +126 -0
- package/src/loop/ttsr-defaults.ts +53 -0
- package/src/loop/ttsr.ts +322 -0
- package/src/loop/turn.ts +856 -0
- package/src/lsp/index.ts +2 -0
- package/src/lsp/lsp.types.ts +56 -0
- package/src/lsp/service.ts +500 -0
- package/src/meta-rules/context.ts +195 -0
- package/src/meta-rules/index.ts +9 -0
- package/src/meta-rules/meta-rules.types.ts +47 -0
- package/src/meta-rules/parsers/package-json-parser.ts +51 -0
- package/src/meta-rules/registry.ts +37 -0
- package/src/meta-rules/rules/ci/workflow-actions-pinned.ts +59 -0
- package/src/meta-rules/rules/ci/workflow-runner-pinned.ts +57 -0
- package/src/meta-rules/rules/ci/workflow-timeout-required.ts +114 -0
- package/src/meta-rules/rules/config/tsconfig-paths-exist.ts +117 -0
- package/src/meta-rules/rules/config/tsconfig-strict.ts +91 -0
- package/src/meta-rules/rules/source-text/no-eslint-disable-comments.ts +34 -0
- package/src/meta-rules/rules/source-text/no-ts-suppressions.ts +38 -0
- package/src/meta-rules/rules/supply-chain/no-overlapping-libs.ts +57 -0
- package/src/meta-rules/rules/supply-chain/package-exact-deps.ts +55 -0
- package/src/meta-rules/rules/testing/test-sibling-required.ts +110 -0
- package/src/meta-rules/runner.ts +64 -0
- package/src/models-config.ts +196 -0
- package/src/render/ansi.ts +289 -0
- package/src/render/banner.ts +113 -0
- package/src/render/box.ts +134 -0
- package/src/render/index.ts +7 -0
- package/src/render/markdown.ts +123 -0
- package/src/render/render.types.ts +21 -0
- package/src/render/stream-markdown.ts +128 -0
- package/src/render/style.ts +26 -0
- package/src/rule-packs/bullmq/index.ts +39 -0
- package/src/rule-packs/bullmq/rules/index.ts +7 -0
- package/src/rule-packs/bullmq/rules/job-name-must-be-constant.ts +141 -0
- package/src/rule-packs/bullmq/rules/job-options-must-set-attempts.ts +174 -0
- package/src/rule-packs/bullmq/rules/no-blocking-concurrency-zero.ts +103 -0
- package/src/rule-packs/bullmq/rules/queue-options-must-set-removeoncomplete.ts +130 -0
- package/src/rule-packs/bullmq/rules/queue-options-must-set-removeonfail.ts +130 -0
- package/src/rule-packs/bullmq/rules/worker-must-implement-close.ts +182 -0
- package/src/rule-packs/bullmq/rules/worker-must-listen-failed.ts +140 -0
- package/src/rule-packs/bullmq/utils.ts +334 -0
- package/src/rule-packs/code-flow/index.ts +25 -0
- package/src/rule-packs/code-flow/rules/index.ts +3 -0
- package/src/rule-packs/code-flow/rules/no-bare-date-now.ts +138 -0
- package/src/rule-packs/code-flow/rules/no-template-trim-empty-ternary.ts +87 -0
- package/src/rule-packs/code-flow/rules/prefer-early-return.ts +80 -0
- package/src/rule-packs/code-flow/utils/prefer-early-return.ts +132 -0
- package/src/rule-packs/comment-hygiene/index.ts +25 -0
- package/src/rule-packs/comment-hygiene/rules/index.ts +3 -0
- package/src/rule-packs/comment-hygiene/rules/no-historical-comments.ts +102 -0
- package/src/rule-packs/comment-hygiene/rules/no-narration-comments.ts +83 -0
- package/src/rule-packs/comment-hygiene/rules/no-pr-reference-comments.ts +90 -0
- package/src/rule-packs/create-rule.ts +9 -0
- package/src/rule-packs/drizzle/index.ts +41 -0
- package/src/rule-packs/drizzle/rules/account-scoped-tables-require-where.ts +371 -0
- package/src/rule-packs/drizzle/rules/index.ts +8 -0
- package/src/rule-packs/drizzle/rules/no-nested-db-transaction.ts +127 -0
- package/src/rule-packs/drizzle/rules/no-raw-sql-outside-allowlist.ts +100 -0
- package/src/rule-packs/drizzle/rules/relations-must-cover-fks.ts +209 -0
- package/src/rule-packs/drizzle/rules/schema-files-must-not-import-driver.ts +127 -0
- package/src/rule-packs/drizzle/rules/schema-files-must-only-export-schema.ts +149 -0
- package/src/rule-packs/drizzle/rules/tables-must-have-timestamps.ts +312 -0
- package/src/rule-packs/drizzle/rules/timestamp-must-specify-mode.ts +166 -0
- package/src/rule-packs/drizzle/utils.ts +115 -0
- package/src/rule-packs/elysia/index.ts +43 -0
- package/src/rule-packs/elysia/rules/consistent-status-via-set.ts +69 -0
- package/src/rule-packs/elysia/rules/no-decorate-state-collision.ts +276 -0
- package/src/rule-packs/elysia/rules/no-separate-model-interfaces.ts +144 -0
- package/src/rule-packs/elysia/rules/prefer-destructured-context.ts +155 -0
- package/src/rule-packs/elysia/rules/prefer-direct-return.ts +176 -0
- package/src/rule-packs/elysia/rules/prefer-static-services.ts +159 -0
- package/src/rule-packs/elysia/rules/prefer-throw-status.ts +151 -0
- package/src/rule-packs/elysia/rules/require-hooks-before-routes.ts +209 -0
- package/src/rule-packs/elysia/rules/require-plugin-name.ts +107 -0
- package/src/rule-packs/elysia/utils/elysiaChain.ts +306 -0
- package/src/rule-packs/env-access/index.ts +23 -0
- package/src/rule-packs/env-access/rules/index.ts +2 -0
- package/src/rule-packs/env-access/rules/no-direct-process-env.ts +133 -0
- package/src/rule-packs/env-access/rules/no-process-exit.ts +95 -0
- package/src/rule-packs/i18n-keys/index.ts +19 -0
- package/src/rule-packs/i18n-keys/rules/static-translation-key-exists.ts +173 -0
- package/src/rule-packs/index.ts +139 -0
- package/src/rule-packs/jwt-cookies/index.ts +25 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-httponly.ts +150 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-secure-in-prod.ts +149 -0
- package/src/rule-packs/jwt-cookies/rules/bcrypt-rounds-min.ts +195 -0
- package/src/rule-packs/jwt-cookies/utils.ts +188 -0
- package/src/rule-packs/oauth-security/index.ts +25 -0
- package/src/rule-packs/oauth-security/rules/pkce-required-for-oidc.ts +296 -0
- package/src/rule-packs/oauth-security/rules/state-must-be-redis-backed.ts +193 -0
- package/src/rule-packs/oauth-security/rules/state-ttl-bounded.ts +219 -0
- package/src/rule-packs/oauth-security/utils.ts +127 -0
- package/src/rule-packs/react-component-architecture/index.ts +35 -0
- package/src/rule-packs/react-component-architecture/rules/component-folder-structure.ts +123 -0
- package/src/rule-packs/react-component-architecture/rules/forwardref-display-name.ts +93 -0
- package/src/rule-packs/react-component-architecture/rules/index-must-reexport-default.ts +123 -0
- package/src/rule-packs/react-component-architecture/rules/max-hooks-per-file.ts +122 -0
- package/src/rule-packs/react-component-architecture/rules/no-cross-feature-imports.ts +170 -0
- package/src/rule-packs/react-component-architecture/rules/no-inline-jsx-functions.ts +66 -0
- package/src/rule-packs/react-component-architecture/utils.ts +47 -0
- package/src/rule-packs/rule-packs.types.ts +18 -0
- package/src/rule-packs/structured-logging/index.ts +26 -0
- package/src/rule-packs/structured-logging/rules/mask-pii-fields.ts +221 -0
- package/src/rule-packs/structured-logging/rules/no-error-stringify.ts +217 -0
- package/src/rule-packs/structured-logging/rules/require-event-field.ts +136 -0
- package/src/rule-packs/structured-logging/utils/logger.ts +104 -0
- package/src/rule-packs/tanstack-query/index.ts +20 -0
- package/src/rule-packs/tanstack-query/rules/prefix-query-key-must-use-set-queries-data.ts +321 -0
- package/src/rule-packs/test-conventions/index.ts +23 -0
- package/src/rule-packs/test-conventions/rules/index.ts +2 -0
- package/src/rule-packs/test-conventions/rules/no-focused-tests.ts +170 -0
- package/src/rule-packs/test-conventions/rules/test-file-mirrors-source.ts +127 -0
- package/src/rule-packs/utils.ts +142 -0
- package/src/session-store.ts +359 -0
- package/src/spec/generate-tests.ts +213 -0
- package/src/spec/index.ts +5 -0
- package/src/spec/parse.ts +152 -0
- package/src/spec/review-tests.ts +162 -0
- package/src/spec/spec.constants.ts +13 -0
- package/src/spec/spec.types.ts +79 -0
- package/src/stack-detection/detect.ts +246 -0
- package/src/stack-detection/index.ts +3 -0
- package/src/stack-detection/packs.ts +174 -0
- package/src/stack-detection/stack-detection.types.ts +47 -0
- package/src/validate/accept.ts +49 -0
- package/src/validate/errors.ts +35 -0
- package/src/validate/index.ts +12 -0
- package/src/validate/parse.ts +148 -0
- package/src/validate/run-tests.ts +59 -0
- package/src/validate/validate.ts +40 -0
- package/src/validate/validate.types.ts +52 -0
- package/src/web-components.ts +638 -0
- package/src/web-coverage.ts +89 -0
- package/src/web-routes.ts +151 -0
- package/src/web-templates.ts +1011 -0
- package/strict.eslint.config.mjs +84 -0
- package/strict.web.eslint.config.mjs +185 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { walkAll } from "../utils";
|
|
4
|
+
|
|
5
|
+
export { walkAll, walkSome } from "../utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Helper utilities for BullMQ rules.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface BullmqImports {
|
|
12
|
+
hasBullmqImport: boolean;
|
|
13
|
+
workerLocalNames: Set<string>;
|
|
14
|
+
queueLocalNames: Set<string>;
|
|
15
|
+
queueEventsLocalNames: Set<string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface QueueDefinition {
|
|
19
|
+
bindingKey: string;
|
|
20
|
+
defaultJobOptions?: TSESTree.ObjectExpression | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface WorkerDefinition {
|
|
24
|
+
bindingKey: string | null;
|
|
25
|
+
node: TSESTree.NewExpression;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function analyzeBullmqImports(program: TSESTree.Program): BullmqImports {
|
|
29
|
+
const result: BullmqImports = {
|
|
30
|
+
hasBullmqImport: false,
|
|
31
|
+
workerLocalNames: new Set(),
|
|
32
|
+
queueLocalNames: new Set(),
|
|
33
|
+
queueEventsLocalNames: new Set(),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
for (const stmt of program.body) {
|
|
37
|
+
if (stmt.type !== AST_NODE_TYPES.ImportDeclaration) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (stmt.source.value !== "bullmq") {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
result.hasBullmqImport = true;
|
|
46
|
+
|
|
47
|
+
for (const specifier of stmt.specifiers) {
|
|
48
|
+
recordImportSpecifier(specifier, result);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function recordImportSpecifier(
|
|
56
|
+
specifier: TSESTree.ImportClause,
|
|
57
|
+
result: BullmqImports
|
|
58
|
+
): void {
|
|
59
|
+
if (specifier.type !== AST_NODE_TYPES.ImportSpecifier) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (specifier.imported.type !== AST_NODE_TYPES.Identifier) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const target = {
|
|
68
|
+
Worker: result.workerLocalNames,
|
|
69
|
+
Queue: result.queueLocalNames,
|
|
70
|
+
QueueEvents: result.queueEventsLocalNames,
|
|
71
|
+
}[specifier.imported.name];
|
|
72
|
+
|
|
73
|
+
target?.add(specifier.local.name);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function extractDefaultJobOptions(
|
|
77
|
+
newExpr: TSESTree.NewExpression
|
|
78
|
+
): TSESTree.ObjectExpression | null {
|
|
79
|
+
const opts = getOptionsObjectArg(newExpr, 1);
|
|
80
|
+
|
|
81
|
+
if (!opts) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const property = findObjectProperty(opts, "defaultJobOptions");
|
|
86
|
+
|
|
87
|
+
if (!property) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (property.value.type === AST_NODE_TYPES.ObjectExpression) {
|
|
92
|
+
return property.value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isNewQueue(
|
|
99
|
+
node: TSESTree.Node,
|
|
100
|
+
imports: BullmqImports
|
|
101
|
+
): node is TSESTree.NewExpression {
|
|
102
|
+
if (node.type !== AST_NODE_TYPES.NewExpression) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (node.callee.type !== AST_NODE_TYPES.Identifier) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return imports.queueLocalNames.has(node.callee.name);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function collectQueueDefinitions(
|
|
114
|
+
program: TSESTree.Program,
|
|
115
|
+
imports: BullmqImports
|
|
116
|
+
): Map<string, QueueDefinition> {
|
|
117
|
+
const queues = new Map<string, QueueDefinition>();
|
|
118
|
+
|
|
119
|
+
walkAll(program, (node) => {
|
|
120
|
+
if (
|
|
121
|
+
node.type === AST_NODE_TYPES.VariableDeclarator &&
|
|
122
|
+
node.id.type === AST_NODE_TYPES.Identifier &&
|
|
123
|
+
node.init &&
|
|
124
|
+
isNewQueue(node.init, imports)
|
|
125
|
+
) {
|
|
126
|
+
const varName = node.id.name;
|
|
127
|
+
const firstArg = node.init.arguments[0];
|
|
128
|
+
|
|
129
|
+
if (firstArg?.type === AST_NODE_TYPES.Literal) {
|
|
130
|
+
const queueName = firstArg.value;
|
|
131
|
+
|
|
132
|
+
if (typeof queueName === "string") {
|
|
133
|
+
const defaultJobOptions = extractDefaultJobOptions(node.init);
|
|
134
|
+
|
|
135
|
+
queues.set(varName, {
|
|
136
|
+
bindingKey: varName,
|
|
137
|
+
defaultJobOptions,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
walkAll(program, (node) => {
|
|
145
|
+
if (!isNewQueue(node, imports)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const firstArg = node.arguments[0];
|
|
150
|
+
|
|
151
|
+
if (firstArg?.type !== AST_NODE_TYPES.Literal) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const queueName = firstArg.value;
|
|
156
|
+
|
|
157
|
+
if (typeof queueName !== "string") {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const defaultJobOptions = extractDefaultJobOptions(node);
|
|
162
|
+
const { parent } = node;
|
|
163
|
+
|
|
164
|
+
if (
|
|
165
|
+
parent.type !== AST_NODE_TYPES.VariableDeclarator ||
|
|
166
|
+
parent.id.type !== AST_NODE_TYPES.Identifier
|
|
167
|
+
) {
|
|
168
|
+
queues.set(queueName, {
|
|
169
|
+
bindingKey: queueName,
|
|
170
|
+
defaultJobOptions,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return queues;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function collectWorkerDefinitions(
|
|
179
|
+
program: TSESTree.Program,
|
|
180
|
+
imports: BullmqImports
|
|
181
|
+
): WorkerDefinition[] {
|
|
182
|
+
const workers: WorkerDefinition[] = [];
|
|
183
|
+
const varToWorkerMap = new Map<string, TSESTree.NewExpression>();
|
|
184
|
+
|
|
185
|
+
walkAll(program, (node) => {
|
|
186
|
+
if (
|
|
187
|
+
node.type === AST_NODE_TYPES.VariableDeclarator &&
|
|
188
|
+
node.id.type === AST_NODE_TYPES.Identifier &&
|
|
189
|
+
node.init &&
|
|
190
|
+
isNewWorker(node.init, imports)
|
|
191
|
+
) {
|
|
192
|
+
varToWorkerMap.set(node.id.name, node.init);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
walkAll(program, (node) => {
|
|
197
|
+
if (!isNewWorker(node, imports)) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const bindingKey = workerBindingKey(node.parent);
|
|
202
|
+
|
|
203
|
+
workers.push({
|
|
204
|
+
bindingKey,
|
|
205
|
+
node,
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return workers;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function workerBindingKey(parent: TSESTree.Node): string | null {
|
|
213
|
+
if (
|
|
214
|
+
parent.type === AST_NODE_TYPES.VariableDeclarator &&
|
|
215
|
+
parent.id.type === AST_NODE_TYPES.Identifier
|
|
216
|
+
) {
|
|
217
|
+
return parent.id.name;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (parent.type === AST_NODE_TYPES.PropertyDefinition) {
|
|
221
|
+
if (parent.key.type === AST_NODE_TYPES.Identifier && !parent.computed) {
|
|
222
|
+
return `this.${parent.key.name}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (
|
|
226
|
+
parent.key.type === AST_NODE_TYPES.Literal &&
|
|
227
|
+
typeof parent.key.value === "string"
|
|
228
|
+
) {
|
|
229
|
+
return `this.${parent.key.value}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (parent.type === AST_NODE_TYPES.AssignmentExpression) {
|
|
236
|
+
if (parent.left.type === AST_NODE_TYPES.Identifier) {
|
|
237
|
+
return parent.left.name;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (parent.left.type === AST_NODE_TYPES.MemberExpression) {
|
|
241
|
+
return getReceiverKey(parent.left);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function isNewWorker(
|
|
249
|
+
node: TSESTree.Node,
|
|
250
|
+
imports: BullmqImports
|
|
251
|
+
): node is TSESTree.NewExpression {
|
|
252
|
+
if (node.type !== AST_NODE_TYPES.NewExpression) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (node.callee.type !== AST_NODE_TYPES.Identifier) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return imports.workerLocalNames.has(node.callee.name);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function isQueueAddCall(node: TSESTree.CallExpression): boolean {
|
|
264
|
+
return (
|
|
265
|
+
node.callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
266
|
+
node.callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
267
|
+
node.callee.property.name === "add"
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function getCallReceiverKey(
|
|
272
|
+
node: TSESTree.CallExpression
|
|
273
|
+
): string | null {
|
|
274
|
+
return getReceiverKey(node.callee);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function getReceiverKey(callee: TSESTree.Node): string | null {
|
|
278
|
+
if (callee.type === AST_NODE_TYPES.MemberExpression) {
|
|
279
|
+
if (callee.object.type === AST_NODE_TYPES.Identifier) {
|
|
280
|
+
return callee.object.name;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (
|
|
284
|
+
callee.object.type === AST_NODE_TYPES.MemberExpression &&
|
|
285
|
+
callee.object.property.type === AST_NODE_TYPES.Identifier
|
|
286
|
+
) {
|
|
287
|
+
const base = getReceiverKey(callee.object);
|
|
288
|
+
|
|
289
|
+
if (base) {
|
|
290
|
+
return `${base}.${callee.object.property.name}`;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function isQueueLikeReceiverName(name: string): boolean {
|
|
299
|
+
return /[Qq]ueue/.test(name);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function getOptionsObjectArg(
|
|
303
|
+
node: TSESTree.NewExpression | TSESTree.CallExpression,
|
|
304
|
+
argIndex: number
|
|
305
|
+
): TSESTree.ObjectExpression | null {
|
|
306
|
+
const arg = node.arguments[argIndex];
|
|
307
|
+
|
|
308
|
+
if (arg?.type === AST_NODE_TYPES.ObjectExpression) {
|
|
309
|
+
return arg;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function findObjectProperty(
|
|
316
|
+
obj: TSESTree.ObjectExpression,
|
|
317
|
+
name: string
|
|
318
|
+
): TSESTree.Property | null {
|
|
319
|
+
for (const prop of obj.properties) {
|
|
320
|
+
if (prop.type !== AST_NODE_TYPES.Property) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (prop.key.type === AST_NODE_TYPES.Identifier && prop.key.name === name) {
|
|
325
|
+
return prop;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (prop.key.type === AST_NODE_TYPES.Literal && prop.key.value === name) {
|
|
329
|
+
return prop;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { TSESLint } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { noBareDateNowRule } from "./rules/no-bare-date-now";
|
|
4
|
+
import { noTemplateTrimEmptyTernaryRule } from "./rules/no-template-trim-empty-ternary";
|
|
5
|
+
import { preferEarlyReturnRule } from "./rules/prefer-early-return";
|
|
6
|
+
import type { IRulePack } from "../rule-packs.types";
|
|
7
|
+
|
|
8
|
+
const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
|
|
9
|
+
"no-bare-date-now": noBareDateNowRule,
|
|
10
|
+
"no-template-trim-empty-ternary": noTemplateTrimEmptyTernaryRule,
|
|
11
|
+
"prefer-early-return": preferEarlyReturnRule,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const codeFlowPack: IRulePack = {
|
|
15
|
+
id: "code-flow",
|
|
16
|
+
description: "Control flow clarity and early returns",
|
|
17
|
+
rules,
|
|
18
|
+
rulesConfig: {
|
|
19
|
+
"no-bare-date-now": "error",
|
|
20
|
+
"no-template-trim-empty-ternary": "error",
|
|
21
|
+
"prefer-early-return": "error",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default codeFlowPack;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { createRule } from "../../create-rule";
|
|
4
|
+
|
|
5
|
+
export const RULE_NAME = "no-bare-date-now";
|
|
6
|
+
|
|
7
|
+
type MessageIds =
|
|
8
|
+
| "bareDateNow"
|
|
9
|
+
| "bareNewDate"
|
|
10
|
+
| "bareMathRandom"
|
|
11
|
+
| "bareDateConstructor";
|
|
12
|
+
|
|
13
|
+
export interface INoBareDateNowOptions {
|
|
14
|
+
readonly allowedPaths?: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULTS: Required<INoBareDateNowOptions> = {
|
|
18
|
+
allowedPaths: [],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function fileMatchesAllowlist(
|
|
22
|
+
filename: string,
|
|
23
|
+
allowed: readonly string[]
|
|
24
|
+
): boolean {
|
|
25
|
+
if (allowed.length === 0) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const normalized = filename.replace(/\\/g, "/");
|
|
30
|
+
|
|
31
|
+
return allowed.some((segment) => normalized.includes(segment));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isDateNow(node: TSESTree.CallExpression): boolean {
|
|
35
|
+
const callee = node.callee;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
39
|
+
callee.object.type === AST_NODE_TYPES.Identifier &&
|
|
40
|
+
callee.object.name === "Date" &&
|
|
41
|
+
callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
42
|
+
callee.property.name === "now" &&
|
|
43
|
+
!callee.computed
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isMathRandom(node: TSESTree.CallExpression): boolean {
|
|
48
|
+
const callee = node.callee;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
52
|
+
callee.object.type === AST_NODE_TYPES.Identifier &&
|
|
53
|
+
callee.object.name === "Math" &&
|
|
54
|
+
callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
55
|
+
callee.property.name === "random" &&
|
|
56
|
+
!callee.computed
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isBareDate(
|
|
61
|
+
node: TSESTree.NewExpression | TSESTree.CallExpression
|
|
62
|
+
): boolean {
|
|
63
|
+
return (
|
|
64
|
+
node.callee.type === AST_NODE_TYPES.Identifier &&
|
|
65
|
+
node.callee.name === "Date" &&
|
|
66
|
+
node.arguments.length === 0
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const noBareDateNowRule = createRule<
|
|
71
|
+
[INoBareDateNowOptions],
|
|
72
|
+
MessageIds
|
|
73
|
+
>({
|
|
74
|
+
name: RULE_NAME,
|
|
75
|
+
meta: {
|
|
76
|
+
type: "problem",
|
|
77
|
+
docs: {
|
|
78
|
+
description:
|
|
79
|
+
"Disallow direct calls to non-deterministic time/random sources (`Date.now()`, `new Date()`, `Date()`, `Math.random()`) outside an allowlisted set of utility paths. Determinism is required for snapshot tests, workflow replays, and time-travel debugging — every consumer should route through a typed util that can be faked in tests.",
|
|
80
|
+
},
|
|
81
|
+
schema: [
|
|
82
|
+
{
|
|
83
|
+
type: "object",
|
|
84
|
+
properties: {
|
|
85
|
+
allowedPaths: {
|
|
86
|
+
type: "array",
|
|
87
|
+
items: { type: "string" },
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
additionalProperties: false,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
messages: {
|
|
94
|
+
bareDateNow:
|
|
95
|
+
"Direct `Date.now()` is non-deterministic. Import the project's `now()` util instead (or add the file to this rule's `allowedPaths` if it IS the util).",
|
|
96
|
+
bareNewDate:
|
|
97
|
+
"Direct `new Date()` (no args) is non-deterministic. Import the project's `now()` util and pass the millisecond timestamp explicitly, or add the file to `allowedPaths`.",
|
|
98
|
+
bareDateConstructor:
|
|
99
|
+
"Direct `Date()` (no args) is non-deterministic. Import the project's `now()` util and pass the millisecond timestamp explicitly, or add the file to `allowedPaths`.",
|
|
100
|
+
bareMathRandom:
|
|
101
|
+
"Direct `Math.random()` is non-deterministic. Import the project's random util (which can be seeded in tests) instead, or add the file to `allowedPaths`.",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
defaultOptions: [DEFAULTS],
|
|
105
|
+
create(context, optionsArg) {
|
|
106
|
+
const options = optionsArg[0] ?? DEFAULTS;
|
|
107
|
+
const allowed = options.allowedPaths ?? DEFAULTS.allowedPaths;
|
|
108
|
+
|
|
109
|
+
if (fileMatchesAllowlist(context.filename, allowed)) {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
CallExpression(node: TSESTree.CallExpression) {
|
|
115
|
+
if (isDateNow(node)) {
|
|
116
|
+
context.report({ node, messageId: "bareDateNow" });
|
|
117
|
+
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (isMathRandom(node)) {
|
|
122
|
+
context.report({ node, messageId: "bareMathRandom" });
|
|
123
|
+
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (isBareDate(node)) {
|
|
128
|
+
context.report({ node, messageId: "bareDateConstructor" });
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
NewExpression(node: TSESTree.NewExpression) {
|
|
132
|
+
if (isBareDate(node)) {
|
|
133
|
+
context.report({ node, messageId: "bareNewDate" });
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { createRule } from "../../create-rule";
|
|
4
|
+
|
|
5
|
+
export const RULE_NAME = "no-template-trim-empty-ternary";
|
|
6
|
+
|
|
7
|
+
type MessageIds = "extractToUtil";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Bans the "build a string from a template literal, trim it, ternary
|
|
11
|
+
* against empty" pattern:
|
|
12
|
+
*
|
|
13
|
+
* `${first} ${last}`.trim() === "" ? email : `${first} ${last}`.trim()
|
|
14
|
+
*
|
|
15
|
+
* Patterns like this are testability traps: the same expression is
|
|
16
|
+
* built twice in the call site, no name documents the intent, and tests
|
|
17
|
+
* have to duplicate the construction to verify behaviour. Extract to a
|
|
18
|
+
* named util (e.g. `buildDisplayName({ first, last, fallback }))`) and
|
|
19
|
+
* test it in one place.
|
|
20
|
+
*/
|
|
21
|
+
export const noTemplateTrimEmptyTernaryRule = createRule<[], MessageIds>({
|
|
22
|
+
name: RULE_NAME,
|
|
23
|
+
meta: {
|
|
24
|
+
type: "suggestion",
|
|
25
|
+
docs: {
|
|
26
|
+
description:
|
|
27
|
+
"Disallow inline `<template>.trim() === '' ? fallback : <template>.trim()` patterns. Extract to a named utility.",
|
|
28
|
+
},
|
|
29
|
+
schema: [],
|
|
30
|
+
messages: {
|
|
31
|
+
extractToUtil:
|
|
32
|
+
"Extract this `<template>.trim() === ''` fallback pattern to a named util (e.g. `buildDisplayName(...)`). Inline ternaries duplicate the expression and aren't unit-testable in one place.",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
defaultOptions: [],
|
|
36
|
+
create(context) {
|
|
37
|
+
return {
|
|
38
|
+
ConditionalExpression(node) {
|
|
39
|
+
if (matchesTemplateTrimEmptyTest(node.test)) {
|
|
40
|
+
context.report({ node, messageId: "extractToUtil" });
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
function matchesTemplateTrimEmptyTest(test: TSESTree.Expression): boolean {
|
|
48
|
+
if (test.type !== AST_NODE_TYPES.BinaryExpression) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (test.operator !== "===" && test.operator !== "!==") {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
(isTrimCallOnTemplate(test.left) && isEmptyStringLiteral(test.right)) ||
|
|
58
|
+
(isEmptyStringLiteral(test.left) && isTrimCallOnTemplate(test.right))
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isTrimCallOnTemplate(node: TSESTree.Node): boolean {
|
|
63
|
+
if (node.type !== AST_NODE_TYPES.CallExpression) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (node.callee.type !== AST_NODE_TYPES.MemberExpression) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (
|
|
72
|
+
node.callee.property.type !== AST_NODE_TYPES.Identifier ||
|
|
73
|
+
node.callee.property.name !== "trim"
|
|
74
|
+
) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return node.callee.object.type === AST_NODE_TYPES.TemplateLiteral;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isEmptyStringLiteral(node: TSESTree.Node): boolean {
|
|
82
|
+
return (
|
|
83
|
+
node.type === AST_NODE_TYPES.Literal &&
|
|
84
|
+
typeof node.value === "string" &&
|
|
85
|
+
node.value === ""
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
import type { RuleFixer } from "@typescript-eslint/utils/ts-eslint";
|
|
3
|
+
|
|
4
|
+
import { createRule } from "../../create-rule";
|
|
5
|
+
import {
|
|
6
|
+
buildGuardClauseReplacement,
|
|
7
|
+
findWrappedHappyPathIf,
|
|
8
|
+
getFunctionBlockBody,
|
|
9
|
+
} from "../utils/prefer-early-return";
|
|
10
|
+
|
|
11
|
+
export const RULE_NAME = "prefer-early-return";
|
|
12
|
+
|
|
13
|
+
type MessageIds = "preferEarlyReturn";
|
|
14
|
+
|
|
15
|
+
export const preferEarlyReturnRule = createRule<[], MessageIds>({
|
|
16
|
+
name: RULE_NAME,
|
|
17
|
+
meta: {
|
|
18
|
+
type: "problem",
|
|
19
|
+
docs: {
|
|
20
|
+
description:
|
|
21
|
+
"Prefer guard clauses (early return) over wrapping the function body in a multi-statement `if` without an `else`.",
|
|
22
|
+
},
|
|
23
|
+
schema: [],
|
|
24
|
+
messages: {
|
|
25
|
+
preferEarlyReturn:
|
|
26
|
+
"Use a guard clause (early return) instead of wrapping the function body in an `if`. Invert the condition and return early so the happy path stays at the top level.",
|
|
27
|
+
},
|
|
28
|
+
hasSuggestions: true,
|
|
29
|
+
},
|
|
30
|
+
defaultOptions: [],
|
|
31
|
+
create(context) {
|
|
32
|
+
const sourceCode = context.sourceCode;
|
|
33
|
+
|
|
34
|
+
function reportWrappedHappyPath(ifStatement: TSESTree.IfStatement): void {
|
|
35
|
+
const replacement = buildGuardClauseReplacement(sourceCode, ifStatement);
|
|
36
|
+
const suggest =
|
|
37
|
+
replacement === null
|
|
38
|
+
? []
|
|
39
|
+
: [
|
|
40
|
+
{
|
|
41
|
+
messageId: "preferEarlyReturn" as const,
|
|
42
|
+
fix(fixer: RuleFixer) {
|
|
43
|
+
return fixer.replaceText(ifStatement, replacement);
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
context.report({
|
|
49
|
+
node: ifStatement,
|
|
50
|
+
messageId: "preferEarlyReturn",
|
|
51
|
+
suggest,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function checkFunctionBody(
|
|
56
|
+
node:
|
|
57
|
+
| TSESTree.FunctionDeclaration
|
|
58
|
+
| TSESTree.FunctionExpression
|
|
59
|
+
| TSESTree.ArrowFunctionExpression
|
|
60
|
+
): void {
|
|
61
|
+
const body = getFunctionBlockBody(node);
|
|
62
|
+
|
|
63
|
+
if (body === null) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const wrappedIf = findWrappedHappyPathIf(body);
|
|
68
|
+
|
|
69
|
+
if (wrappedIf !== null) {
|
|
70
|
+
reportWrappedHappyPath(wrappedIf);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
FunctionDeclaration: checkFunctionBody,
|
|
76
|
+
FunctionExpression: checkFunctionBody,
|
|
77
|
+
ArrowFunctionExpression: checkFunctionBody,
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
});
|