@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,221 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
|
|
3
|
+
|
|
4
|
+
import { createRule } from "../../create-rule";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_LOGGER_METHODS,
|
|
7
|
+
DEFAULT_LOGGER_NAMES,
|
|
8
|
+
getStructuredPayload,
|
|
9
|
+
matchLoggerCall,
|
|
10
|
+
} from "../utils/logger";
|
|
11
|
+
|
|
12
|
+
export const RULE_NAME = "mask-pii-fields";
|
|
13
|
+
|
|
14
|
+
export interface MaskPiiFieldsOptions {
|
|
15
|
+
readonly loggerNames?: readonly string[];
|
|
16
|
+
readonly loggerMethods?: readonly string[];
|
|
17
|
+
readonly piiFieldNames?: readonly string[];
|
|
18
|
+
readonly maskFunctions?: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type RuleOptions = [MaskPiiFieldsOptions];
|
|
22
|
+
type MessageIds = "unmaskedPii";
|
|
23
|
+
|
|
24
|
+
const DEFAULT_PII_FIELDS: readonly string[] = [
|
|
25
|
+
"email",
|
|
26
|
+
"phone",
|
|
27
|
+
"password",
|
|
28
|
+
"token",
|
|
29
|
+
"apiKey",
|
|
30
|
+
"secret",
|
|
31
|
+
"ssn",
|
|
32
|
+
"creditCard",
|
|
33
|
+
"authorization",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const DEFAULT_MASK_FUNCTIONS: readonly string[] = [
|
|
37
|
+
"maskEmailForLogging",
|
|
38
|
+
"maskToken",
|
|
39
|
+
"maskPii",
|
|
40
|
+
"redact",
|
|
41
|
+
"mask",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const optionSchema: JSONSchema4 = {
|
|
45
|
+
type: "object",
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
properties: {
|
|
48
|
+
loggerNames: {
|
|
49
|
+
type: "array",
|
|
50
|
+
items: { type: "string" },
|
|
51
|
+
uniqueItems: true,
|
|
52
|
+
minItems: 1,
|
|
53
|
+
},
|
|
54
|
+
loggerMethods: {
|
|
55
|
+
type: "array",
|
|
56
|
+
items: { type: "string" },
|
|
57
|
+
uniqueItems: true,
|
|
58
|
+
minItems: 1,
|
|
59
|
+
},
|
|
60
|
+
piiFieldNames: {
|
|
61
|
+
type: "array",
|
|
62
|
+
items: { type: "string" },
|
|
63
|
+
uniqueItems: true,
|
|
64
|
+
},
|
|
65
|
+
maskFunctions: {
|
|
66
|
+
type: "array",
|
|
67
|
+
items: { type: "string" },
|
|
68
|
+
uniqueItems: true,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function getPropertyKeyName(prop: TSESTree.Property): string | null {
|
|
74
|
+
if (prop.computed) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (prop.key.type === AST_NODE_TYPES.Identifier) {
|
|
79
|
+
return prop.key.name;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (
|
|
83
|
+
prop.key.type === AST_NODE_TYPES.Literal &&
|
|
84
|
+
typeof prop.key.value === "string"
|
|
85
|
+
) {
|
|
86
|
+
return prop.key.value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const REDACTED_LITERAL = /^(\[?REDACTED\]?|\*+|<masked>|<redacted>)$/i;
|
|
93
|
+
|
|
94
|
+
const PATTERN_TYPES = new Set<string>([
|
|
95
|
+
AST_NODE_TYPES.AssignmentPattern,
|
|
96
|
+
AST_NODE_TYPES.RestElement,
|
|
97
|
+
AST_NODE_TYPES.ArrayPattern,
|
|
98
|
+
AST_NODE_TYPES.ObjectPattern,
|
|
99
|
+
AST_NODE_TYPES.TSEmptyBodyFunctionExpression,
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
function isExpression(
|
|
103
|
+
node: TSESTree.Property["value"]
|
|
104
|
+
): node is TSESTree.Expression {
|
|
105
|
+
return !PATTERN_TYPES.has(node.type);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isMaskedValue(
|
|
109
|
+
value: TSESTree.Expression,
|
|
110
|
+
maskFunctions: ReadonlySet<string>
|
|
111
|
+
): boolean {
|
|
112
|
+
// Already-masked literal: `"[REDACTED]"`, `"***"`, etc.
|
|
113
|
+
if (
|
|
114
|
+
value.type === AST_NODE_TYPES.Literal &&
|
|
115
|
+
typeof value.value === "string" &&
|
|
116
|
+
REDACTED_LITERAL.test(value.value)
|
|
117
|
+
) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Wrapped in a configured mask function: `maskEmailForLogging(email)`,
|
|
122
|
+
// `redact(token)`, `someObj.maskToken(t)`.
|
|
123
|
+
if (value.type !== AST_NODE_TYPES.CallExpression) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const callee = value.callee;
|
|
128
|
+
|
|
129
|
+
if (callee.type === AST_NODE_TYPES.Identifier) {
|
|
130
|
+
return maskFunctions.has(callee.name);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (
|
|
134
|
+
callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
135
|
+
!callee.computed &&
|
|
136
|
+
callee.property.type === AST_NODE_TYPES.Identifier
|
|
137
|
+
) {
|
|
138
|
+
return maskFunctions.has(callee.property.name);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const maskPiiFieldsRule = createRule<RuleOptions, MessageIds>({
|
|
145
|
+
name: RULE_NAME,
|
|
146
|
+
meta: {
|
|
147
|
+
type: "problem",
|
|
148
|
+
docs: {
|
|
149
|
+
description:
|
|
150
|
+
"Disallow unmasked PII (email, phone, password, token, ...) in structured-logger payloads — the #1 way data leaks quietly.",
|
|
151
|
+
},
|
|
152
|
+
schema: [optionSchema],
|
|
153
|
+
messages: {
|
|
154
|
+
unmaskedPii:
|
|
155
|
+
"Field '{{field}}' may contain PII — wrap it in {{maskExample}}(...) or use a literal mask before logging.",
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
defaultOptions: [
|
|
159
|
+
{
|
|
160
|
+
loggerNames: [...DEFAULT_LOGGER_NAMES],
|
|
161
|
+
loggerMethods: [...DEFAULT_LOGGER_METHODS],
|
|
162
|
+
piiFieldNames: [...DEFAULT_PII_FIELDS],
|
|
163
|
+
maskFunctions: [...DEFAULT_MASK_FUNCTIONS],
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
create(context, [options]) {
|
|
167
|
+
const loggerNames = new Set(options.loggerNames ?? DEFAULT_LOGGER_NAMES);
|
|
168
|
+
const loggerMethods = new Set(
|
|
169
|
+
options.loggerMethods ?? DEFAULT_LOGGER_METHODS
|
|
170
|
+
);
|
|
171
|
+
const piiFieldNames = new Set(options.piiFieldNames ?? DEFAULT_PII_FIELDS);
|
|
172
|
+
const maskFunctions = new Set(
|
|
173
|
+
options.maskFunctions ?? DEFAULT_MASK_FUNCTIONS
|
|
174
|
+
);
|
|
175
|
+
const maskExample =
|
|
176
|
+
options.maskFunctions?.[0] ?? DEFAULT_MASK_FUNCTIONS[0] ?? "mask";
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
CallExpression(node) {
|
|
180
|
+
if (matchLoggerCall(node, loggerNames, loggerMethods) === null) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const payload = getStructuredPayload(node);
|
|
185
|
+
|
|
186
|
+
if (payload === null) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const prop of payload.properties) {
|
|
191
|
+
if (prop.type !== AST_NODE_TYPES.Property) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const name = getPropertyKeyName(prop);
|
|
196
|
+
|
|
197
|
+
if (name === null || !piiFieldNames.has(name)) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const value = prop.value;
|
|
202
|
+
|
|
203
|
+
if (!isExpression(value)) {
|
|
204
|
+
// Pattern nodes appear only in destructuring contexts; skip.
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (isMaskedValue(value, maskFunctions)) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
context.report({
|
|
213
|
+
node: prop,
|
|
214
|
+
messageId: "unmaskedPii",
|
|
215
|
+
data: { field: name, maskExample },
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
|
|
3
|
+
|
|
4
|
+
import { createRule } from "../../create-rule";
|
|
5
|
+
|
|
6
|
+
export const RULE_NAME = "no-error-stringify";
|
|
7
|
+
|
|
8
|
+
export interface NoErrorStringifyOptions {
|
|
9
|
+
readonly errorIdentifierNames?: readonly string[];
|
|
10
|
+
readonly extractorName?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type RuleOptions = [NoErrorStringifyOptions];
|
|
14
|
+
type MessageIds = "noErrorStringify";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_ERROR_NAMES: readonly string[] = ["error", "err", "e", "cause"];
|
|
17
|
+
const DEFAULT_EXTRACTOR = "getErrorMessage";
|
|
18
|
+
|
|
19
|
+
const optionSchema: JSONSchema4 = {
|
|
20
|
+
type: "object",
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
properties: {
|
|
23
|
+
errorIdentifierNames: {
|
|
24
|
+
type: "array",
|
|
25
|
+
items: { type: "string" },
|
|
26
|
+
uniqueItems: true,
|
|
27
|
+
minItems: 1,
|
|
28
|
+
},
|
|
29
|
+
extractorName: { type: "string", minLength: 1 },
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function isErrorIdentifier(
|
|
34
|
+
node: TSESTree.Node,
|
|
35
|
+
names: ReadonlySet<string>
|
|
36
|
+
): node is TSESTree.Identifier {
|
|
37
|
+
return node.type === AST_NODE_TYPES.Identifier && names.has(node.name);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isExtractorImported(
|
|
41
|
+
program: TSESTree.Program,
|
|
42
|
+
extractor: string
|
|
43
|
+
): boolean {
|
|
44
|
+
for (const statement of program.body) {
|
|
45
|
+
if (statement.type !== AST_NODE_TYPES.ImportDeclaration) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const spec of statement.specifiers) {
|
|
50
|
+
if (
|
|
51
|
+
spec.type === AST_NODE_TYPES.ImportSpecifier &&
|
|
52
|
+
spec.local.name === extractor
|
|
53
|
+
) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const noErrorStringifyRule = createRule<RuleOptions, MessageIds>({
|
|
63
|
+
name: RULE_NAME,
|
|
64
|
+
meta: {
|
|
65
|
+
type: "problem",
|
|
66
|
+
docs: {
|
|
67
|
+
description:
|
|
68
|
+
"Disallow stringifying errors with `String(error)` / `${error}` / `error.toString()` — strips the cause chain. Use a configured extractor instead.",
|
|
69
|
+
},
|
|
70
|
+
fixable: "code",
|
|
71
|
+
schema: [optionSchema],
|
|
72
|
+
messages: {
|
|
73
|
+
noErrorStringify:
|
|
74
|
+
"Stringifying an error drops its cause chain — call `{{extractor}}(error)` instead.",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
defaultOptions: [
|
|
78
|
+
{
|
|
79
|
+
errorIdentifierNames: [...DEFAULT_ERROR_NAMES],
|
|
80
|
+
extractorName: DEFAULT_EXTRACTOR,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
create(context, [options]) {
|
|
84
|
+
const errorNames = new Set(
|
|
85
|
+
options.errorIdentifierNames ?? DEFAULT_ERROR_NAMES
|
|
86
|
+
);
|
|
87
|
+
const extractor = options.extractorName ?? DEFAULT_EXTRACTOR;
|
|
88
|
+
|
|
89
|
+
let extractorImported = false;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
Program(program) {
|
|
93
|
+
extractorImported = isExtractorImported(program, extractor);
|
|
94
|
+
},
|
|
95
|
+
// `String(error)` / `String(err)` ...
|
|
96
|
+
CallExpression(node) {
|
|
97
|
+
const firstArg = node.arguments[0];
|
|
98
|
+
|
|
99
|
+
if (
|
|
100
|
+
node.callee.type === AST_NODE_TYPES.Identifier &&
|
|
101
|
+
node.callee.name === "String" &&
|
|
102
|
+
node.arguments.length === 1 &&
|
|
103
|
+
firstArg !== undefined &&
|
|
104
|
+
isErrorIdentifier(firstArg, errorNames)
|
|
105
|
+
) {
|
|
106
|
+
const errIdent = firstArg;
|
|
107
|
+
|
|
108
|
+
context.report({
|
|
109
|
+
node,
|
|
110
|
+
messageId: "noErrorStringify",
|
|
111
|
+
data: { extractor },
|
|
112
|
+
...(extractorImported
|
|
113
|
+
? {
|
|
114
|
+
fix(fixer) {
|
|
115
|
+
return fixer.replaceText(
|
|
116
|
+
node,
|
|
117
|
+
`${extractor}(${errIdent.name})`
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
: {}),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
// `error.toString()`
|
|
126
|
+
"CallExpression[callee.type='MemberExpression']"(
|
|
127
|
+
node: TSESTree.CallExpression
|
|
128
|
+
) {
|
|
129
|
+
const { callee } = node;
|
|
130
|
+
|
|
131
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
!callee.computed &&
|
|
137
|
+
callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
138
|
+
callee.property.name === "toString" &&
|
|
139
|
+
isErrorIdentifier(callee.object, errorNames) &&
|
|
140
|
+
node.arguments.length === 0
|
|
141
|
+
) {
|
|
142
|
+
const errIdent = callee.object;
|
|
143
|
+
|
|
144
|
+
context.report({
|
|
145
|
+
node,
|
|
146
|
+
messageId: "noErrorStringify",
|
|
147
|
+
data: { extractor },
|
|
148
|
+
...(extractorImported
|
|
149
|
+
? {
|
|
150
|
+
fix(fixer) {
|
|
151
|
+
return fixer.replaceText(
|
|
152
|
+
node,
|
|
153
|
+
`${extractor}(${errIdent.name})`
|
|
154
|
+
);
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
: {}),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
// `${error}` inside a TemplateLiteral
|
|
162
|
+
TemplateLiteral(node) {
|
|
163
|
+
for (const expr of node.expressions) {
|
|
164
|
+
if (isErrorIdentifier(expr, errorNames)) {
|
|
165
|
+
const errIdent = expr;
|
|
166
|
+
|
|
167
|
+
context.report({
|
|
168
|
+
node: expr,
|
|
169
|
+
messageId: "noErrorStringify",
|
|
170
|
+
data: { extractor },
|
|
171
|
+
...(extractorImported
|
|
172
|
+
? {
|
|
173
|
+
fix(fixer) {
|
|
174
|
+
return fixer.replaceText(
|
|
175
|
+
errIdent,
|
|
176
|
+
`${extractor}(${errIdent.name})`
|
|
177
|
+
);
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
: {}),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
// `error + ""` or `"" + error`
|
|
186
|
+
BinaryExpression(node) {
|
|
187
|
+
if (node.operator !== "+") {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const sides: TSESTree.Node[] = [node.left, node.right];
|
|
192
|
+
const hasEmptyString = sides.some(
|
|
193
|
+
(n) =>
|
|
194
|
+
n.type === AST_NODE_TYPES.Literal &&
|
|
195
|
+
typeof n.value === "string" &&
|
|
196
|
+
n.value === ""
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
if (!hasEmptyString) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for (const side of sides) {
|
|
204
|
+
if (isErrorIdentifier(side, errorNames)) {
|
|
205
|
+
context.report({
|
|
206
|
+
node,
|
|
207
|
+
messageId: "noErrorStringify",
|
|
208
|
+
data: { extractor },
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
|
|
3
|
+
|
|
4
|
+
import { createRule } from "../../create-rule";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_LOGGER_METHODS,
|
|
7
|
+
DEFAULT_LOGGER_NAMES,
|
|
8
|
+
getStructuredPayload,
|
|
9
|
+
matchLoggerCall,
|
|
10
|
+
} from "../utils/logger";
|
|
11
|
+
|
|
12
|
+
export const RULE_NAME = "require-event-field";
|
|
13
|
+
|
|
14
|
+
export interface RequireEventFieldOptions {
|
|
15
|
+
readonly loggerNames?: readonly string[];
|
|
16
|
+
readonly loggerMethods?: readonly string[];
|
|
17
|
+
readonly eventField?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type RuleOptions = [RequireEventFieldOptions];
|
|
21
|
+
type MessageIds = "missingEventField";
|
|
22
|
+
|
|
23
|
+
const optionSchema: JSONSchema4 = {
|
|
24
|
+
type: "object",
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
properties: {
|
|
27
|
+
loggerNames: {
|
|
28
|
+
type: "array",
|
|
29
|
+
items: { type: "string" },
|
|
30
|
+
uniqueItems: true,
|
|
31
|
+
minItems: 1,
|
|
32
|
+
},
|
|
33
|
+
loggerMethods: {
|
|
34
|
+
type: "array",
|
|
35
|
+
items: { type: "string" },
|
|
36
|
+
uniqueItems: true,
|
|
37
|
+
minItems: 1,
|
|
38
|
+
},
|
|
39
|
+
eventField: { type: "string", minLength: 1 },
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function payloadHasField(
|
|
44
|
+
payload: TSESTree.ObjectExpression,
|
|
45
|
+
field: string
|
|
46
|
+
): boolean {
|
|
47
|
+
for (const prop of payload.properties) {
|
|
48
|
+
if (prop.type === AST_NODE_TYPES.SpreadElement) {
|
|
49
|
+
// Spread of an external object — can't inspect statically.
|
|
50
|
+
// Be permissive: assume the spread might supply the field.
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (prop.type !== AST_NODE_TYPES.Property) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (
|
|
59
|
+
prop.key.type === AST_NODE_TYPES.Identifier &&
|
|
60
|
+
prop.key.name === field
|
|
61
|
+
) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
prop.key.type === AST_NODE_TYPES.Literal &&
|
|
67
|
+
typeof prop.key.value === "string" &&
|
|
68
|
+
prop.key.value === field
|
|
69
|
+
) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const requireEventFieldRule = createRule<RuleOptions, MessageIds>({
|
|
78
|
+
name: RULE_NAME,
|
|
79
|
+
meta: {
|
|
80
|
+
type: "problem",
|
|
81
|
+
docs: {
|
|
82
|
+
description:
|
|
83
|
+
"Require structured logger calls to include an `event` field in their payload, so log searches in ELK/Datadog/Loki don't fall back to substring match.",
|
|
84
|
+
},
|
|
85
|
+
schema: [optionSchema],
|
|
86
|
+
messages: {
|
|
87
|
+
missingEventField:
|
|
88
|
+
"Logger call '{{method}}' missing `{{field}}:` field — add a stable string identifier so searches can filter by event.",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
defaultOptions: [
|
|
92
|
+
{
|
|
93
|
+
loggerNames: [...DEFAULT_LOGGER_NAMES],
|
|
94
|
+
loggerMethods: [...DEFAULT_LOGGER_METHODS],
|
|
95
|
+
eventField: "event",
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
create(context, [options]) {
|
|
99
|
+
const loggerNames = new Set(options.loggerNames ?? DEFAULT_LOGGER_NAMES);
|
|
100
|
+
const loggerMethods = new Set(
|
|
101
|
+
options.loggerMethods ?? DEFAULT_LOGGER_METHODS
|
|
102
|
+
);
|
|
103
|
+
const eventField = options.eventField ?? "event";
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
CallExpression(node) {
|
|
107
|
+
const method = matchLoggerCall(node, loggerNames, loggerMethods);
|
|
108
|
+
|
|
109
|
+
if (method === null) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const payload = getStructuredPayload(node);
|
|
114
|
+
|
|
115
|
+
if (payload === null) {
|
|
116
|
+
// No structured payload at all — report on the whole call.
|
|
117
|
+
context.report({
|
|
118
|
+
node,
|
|
119
|
+
messageId: "missingEventField",
|
|
120
|
+
data: { method, field: eventField },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!payloadHasField(payload, eventField)) {
|
|
127
|
+
context.report({
|
|
128
|
+
node: payload,
|
|
129
|
+
messageId: "missingEventField",
|
|
130
|
+
data: { method, field: eventField },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_LOGGER_NAMES: readonly string[] = [
|
|
4
|
+
"logger",
|
|
5
|
+
"log",
|
|
6
|
+
"reqLogger",
|
|
7
|
+
"requestLogger",
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_LOGGER_METHODS: readonly string[] = [
|
|
11
|
+
"fatal",
|
|
12
|
+
"error",
|
|
13
|
+
"warn",
|
|
14
|
+
"info",
|
|
15
|
+
"debug",
|
|
16
|
+
"trace",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns the matched method name when `node` is a logger call —
|
|
21
|
+
* `<id>.<method>(...)` where the receiver name is in `loggerNames`
|
|
22
|
+
* and the method is in `loggerMethods`. Returns null otherwise.
|
|
23
|
+
*
|
|
24
|
+
* Best-effort: we don't try to resolve types. A symbol named `logger`
|
|
25
|
+
* is the strong convention; project-specific aliases (e.g. `reqLogger`
|
|
26
|
+
* from `logger.child(...)`) are configurable via `loggerNames`.
|
|
27
|
+
*/
|
|
28
|
+
export function matchLoggerCall(
|
|
29
|
+
node: TSESTree.CallExpression,
|
|
30
|
+
loggerNames: ReadonlySet<string>,
|
|
31
|
+
loggerMethods: ReadonlySet<string>
|
|
32
|
+
): string | null {
|
|
33
|
+
const callee = node.callee;
|
|
34
|
+
|
|
35
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (callee.property.type !== AST_NODE_TYPES.Identifier) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!loggerMethods.has(callee.property.name)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const receiver = unwrapReceiver(callee.object);
|
|
48
|
+
|
|
49
|
+
if (!receiver) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!loggerNames.has(receiver)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return callee.property.name;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Walks down a possibly-chained MemberExpression, returning the
|
|
62
|
+
* leftmost identifier name. Catches `this.logger.info(...)`,
|
|
63
|
+
* `ctx.log.info(...)`, etc. (returns "logger" / "log" — useful when
|
|
64
|
+
* those names are in the configured set).
|
|
65
|
+
*/
|
|
66
|
+
function unwrapReceiver(node: TSESTree.Expression): string | null {
|
|
67
|
+
if (node.type === AST_NODE_TYPES.Identifier) {
|
|
68
|
+
return node.name;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (
|
|
72
|
+
node.type === AST_NODE_TYPES.MemberExpression &&
|
|
73
|
+
!node.computed &&
|
|
74
|
+
node.property.type === AST_NODE_TYPES.Identifier
|
|
75
|
+
) {
|
|
76
|
+
return node.property.name;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Returns the second positional argument when it's an ObjectExpression
|
|
84
|
+
* — that's where structured-logging payloads live. Many loggers also
|
|
85
|
+
* accept `(obj, message)` (e.g. pino) — handle both shapes by checking
|
|
86
|
+
* the FIRST arg if no ObjectExpression is found at index 1.
|
|
87
|
+
*/
|
|
88
|
+
export function getStructuredPayload(
|
|
89
|
+
node: TSESTree.CallExpression
|
|
90
|
+
): TSESTree.ObjectExpression | null {
|
|
91
|
+
const second = node.arguments[1];
|
|
92
|
+
|
|
93
|
+
if (second?.type === AST_NODE_TYPES.ObjectExpression) {
|
|
94
|
+
return second;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const first = node.arguments[0];
|
|
98
|
+
|
|
99
|
+
if (first?.type === AST_NODE_TYPES.ObjectExpression) {
|
|
100
|
+
return first;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TSESLint } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { prefixQueryKeyMustUseSetQueriesDataRule } from "./rules/prefix-query-key-must-use-set-queries-data";
|
|
4
|
+
import type { IRulePack } from "../rule-packs.types";
|
|
5
|
+
|
|
6
|
+
const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
|
|
7
|
+
"prefix-query-key-must-use-set-queries-data":
|
|
8
|
+
prefixQueryKeyMustUseSetQueriesDataRule,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const tanstackQueryPack: IRulePack = {
|
|
12
|
+
id: "tanstack-query",
|
|
13
|
+
description: "Patterns for data fetching with TanStack Query",
|
|
14
|
+
rules,
|
|
15
|
+
rulesConfig: {
|
|
16
|
+
"prefix-query-key-must-use-set-queries-data": "error",
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default tanstackQueryPack;
|