@sarj/eslint-plugin 2.8.0 → 2.10.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/README.md +10 -1
- package/dist/index.cjs +738 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -2
- package/dist/index.d.ts +26 -2
- package/dist/index.js +1005 -303
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -287,12 +287,115 @@ function isProse(text) {
|
|
|
287
287
|
}
|
|
288
288
|
return false;
|
|
289
289
|
}
|
|
290
|
-
|
|
290
|
+
var NARRATION_MAX_WORDS = 6;
|
|
291
|
+
var NARRATION_MIN_CONTENT = 1;
|
|
292
|
+
var TOKEN_PLURAL_MIN = 4;
|
|
293
|
+
var RESTATABLE_STATEMENTS = /* @__PURE__ */ new Set([
|
|
294
|
+
import_utils3.AST_NODE_TYPES.ExpressionStatement,
|
|
295
|
+
import_utils3.AST_NODE_TYPES.ReturnStatement,
|
|
296
|
+
import_utils3.AST_NODE_TYPES.ThrowStatement,
|
|
297
|
+
import_utils3.AST_NODE_TYPES.VariableDeclaration
|
|
298
|
+
]);
|
|
299
|
+
var NARRATION_VERB_RE = /^(?:add|append|assign|build|calculate|call|check|clear|close|compute|convert|copy|count|create|declare|decrement|define|delete|extract|fetch|filter|find|format|generate|get|handle|increment|init|initialise|initialize|insert|iterate|join|load|log|loop|make|map|merge|open|parse|print|process|push|read|remove|render|reset|return|save|send|set|setup|sort|split|start|stop|store|update|validate|wrap|write)(?:s|es|d|ed|ing)?$/i;
|
|
300
|
+
var NARRATION_STOPWORDS = /* @__PURE__ */ new Set([
|
|
301
|
+
"a",
|
|
302
|
+
"all",
|
|
303
|
+
"an",
|
|
304
|
+
"and",
|
|
305
|
+
"any",
|
|
306
|
+
"are",
|
|
307
|
+
"as",
|
|
308
|
+
"at",
|
|
309
|
+
"back",
|
|
310
|
+
"be",
|
|
311
|
+
"both",
|
|
312
|
+
"by",
|
|
313
|
+
"each",
|
|
314
|
+
"for",
|
|
315
|
+
"from",
|
|
316
|
+
"here",
|
|
317
|
+
"if",
|
|
318
|
+
"in",
|
|
319
|
+
"into",
|
|
320
|
+
"is",
|
|
321
|
+
"it",
|
|
322
|
+
"its",
|
|
323
|
+
"just",
|
|
324
|
+
"new",
|
|
325
|
+
"of",
|
|
326
|
+
"on",
|
|
327
|
+
"one",
|
|
328
|
+
"onto",
|
|
329
|
+
"or",
|
|
330
|
+
"our",
|
|
331
|
+
"out",
|
|
332
|
+
"over",
|
|
333
|
+
"so",
|
|
334
|
+
"that",
|
|
335
|
+
"the",
|
|
336
|
+
"then",
|
|
337
|
+
"this",
|
|
338
|
+
"to",
|
|
339
|
+
"up",
|
|
340
|
+
"us",
|
|
341
|
+
"we",
|
|
342
|
+
"when",
|
|
343
|
+
"with"
|
|
344
|
+
]);
|
|
345
|
+
function normalizeToken(word) {
|
|
346
|
+
const lower = word.toLowerCase();
|
|
347
|
+
return lower.length > TOKEN_PLURAL_MIN && lower.endsWith("s") && !lower.endsWith("ss") ? lower.slice(0, -1) : lower;
|
|
348
|
+
}
|
|
349
|
+
function codeTokens(source) {
|
|
350
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
351
|
+
for (const identifier of source.match(/[A-Za-z_$][\w$]*/g) ?? []) {
|
|
352
|
+
tokens.add(normalizeToken(identifier));
|
|
353
|
+
for (const part of identifier.split(/[_$]+|(?<=[a-z0-9])(?=[A-Z])/)) {
|
|
354
|
+
if (part.length > 0) tokens.add(normalizeToken(part));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return tokens;
|
|
358
|
+
}
|
|
359
|
+
function isTrivialInitializer(node) {
|
|
360
|
+
return node.declarations.every((declarator) => {
|
|
361
|
+
const init = declarator.init;
|
|
362
|
+
if (init == null || init.type === import_utils3.AST_NODE_TYPES.Literal) return true;
|
|
363
|
+
return init.type === import_utils3.AST_NODE_TYPES.ArrayExpression && init.elements.length === 0 || init.type === import_utils3.AST_NODE_TYPES.ObjectExpression && init.properties.length === 0;
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
function restatableStatementBelow(comment, sourceCode) {
|
|
367
|
+
const token = sourceCode.getTokenAfter(comment, { includeComments: false });
|
|
368
|
+
if (token === null || token.loc.start.line !== comment.loc.end.line + 1) return null;
|
|
369
|
+
for (let node = sourceCode.getNodeByRangeIndex(token.range[0]); node != null && node.type !== import_utils3.AST_NODE_TYPES.Program; node = node.parent) {
|
|
370
|
+
if (!RESTATABLE_STATEMENTS.has(node.type)) continue;
|
|
371
|
+
if (node.loc.start.line !== token.loc.start.line || node.loc.end.line !== node.loc.start.line) {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
if (node.type === import_utils3.AST_NODE_TYPES.VariableDeclaration && isTrivialInitializer(node)) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
return sourceCode.getText(node);
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
function restatesNextLine(body, statement) {
|
|
382
|
+
if (statement === null) return false;
|
|
383
|
+
const words = body.match(/[A-Za-z][\w$]*/g) ?? [];
|
|
384
|
+
const opener = words[0];
|
|
385
|
+
if (opener === void 0 || words.length > NARRATION_MAX_WORDS) return false;
|
|
386
|
+
if (!NARRATION_VERB_RE.test(opener)) return false;
|
|
387
|
+
const content = words.slice(1).map(normalizeToken).filter((word) => !NARRATION_STOPWORDS.has(word));
|
|
388
|
+
if (content.length < NARRATION_MIN_CONTENT) return false;
|
|
389
|
+
const head = statement.split("(")[0] ?? statement;
|
|
390
|
+
const code = codeTokens(head);
|
|
391
|
+
return content.every((word) => code.has(word));
|
|
392
|
+
}
|
|
393
|
+
function isRedundantNarration(body, statementBelow) {
|
|
291
394
|
const t = body.trim();
|
|
292
395
|
if (!t || looksLikeCode(t) || hasPseudocode(t)) return false;
|
|
293
396
|
if (STEP_NARRATION_RE.test(t)) return true;
|
|
294
397
|
if (META_COMMENTARY_RE.test(t)) return true;
|
|
295
|
-
return
|
|
398
|
+
return restatesNextLine(t, statementBelow);
|
|
296
399
|
}
|
|
297
400
|
function hasCommentedOutCode(texts, precedingProse) {
|
|
298
401
|
for (let i = 0; i < texts.length; i++) {
|
|
@@ -377,7 +480,8 @@ var no_comment_cruft_default = import_utils3.ESLintUtils.RuleCreator(
|
|
|
377
480
|
}
|
|
378
481
|
if (comment.type === "Line" && texts.length === 1) {
|
|
379
482
|
const body = texts[0];
|
|
380
|
-
|
|
483
|
+
const statement = restatableStatementBelow(comment, sourceCode);
|
|
484
|
+
if (body !== void 0 && isRedundantNarration(body, statement)) {
|
|
381
485
|
context.report({ node: comment, messageId: "redundantNarration" });
|
|
382
486
|
}
|
|
383
487
|
}
|
|
@@ -3638,6 +3742,10 @@ function isSecretName(identifier, innocuous = INNOCUOUS_WORDS) {
|
|
|
3638
3742
|
if (last !== void 0 && innocuous.has(last)) {
|
|
3639
3743
|
return false;
|
|
3640
3744
|
}
|
|
3745
|
+
const first = leadingWord(identifier);
|
|
3746
|
+
if (first !== void 0 && FLAG_PREFIXES.has(first)) {
|
|
3747
|
+
return false;
|
|
3748
|
+
}
|
|
3641
3749
|
if (tokens.some((tok) => SECRET_WORDS.has(tok))) {
|
|
3642
3750
|
return true;
|
|
3643
3751
|
}
|
|
@@ -3648,10 +3756,6 @@ function isAuthSecretName(identifier) {
|
|
|
3648
3756
|
return false;
|
|
3649
3757
|
}
|
|
3650
3758
|
const tokens = tokenize(identifier);
|
|
3651
|
-
const first = leadingWord(identifier);
|
|
3652
|
-
if (first !== void 0 && FLAG_PREFIXES.has(first)) {
|
|
3653
|
-
return false;
|
|
3654
|
-
}
|
|
3655
3759
|
const last = tokens.at(-1);
|
|
3656
3760
|
if (last !== void 0 && DESCRIPTOR_WORDS.has(last)) {
|
|
3657
3761
|
return false;
|
|
@@ -3728,6 +3832,49 @@ function isRawSecretValue(prop) {
|
|
|
3728
3832
|
}
|
|
3729
3833
|
return prop.value.type === "Identifier" || prop.value.type === "MemberExpression";
|
|
3730
3834
|
}
|
|
3835
|
+
var RAW_BLOB_WORDS = /* @__PURE__ */ new Set([
|
|
3836
|
+
"body",
|
|
3837
|
+
"bodies",
|
|
3838
|
+
"payload",
|
|
3839
|
+
"payloads",
|
|
3840
|
+
"params"
|
|
3841
|
+
]);
|
|
3842
|
+
var RAW_BLOB_IDENTIFIERS = /* @__PURE__ */ new Set(["formdata"]);
|
|
3843
|
+
var BLOB_REDACTION_RE = /redact|sanit|scrub|mask|truncat|anonym|filtered|preview|summar/i;
|
|
3844
|
+
var BLOB_REDACTION_TOKENS = /* @__PURE__ */ new Set([
|
|
3845
|
+
"safe",
|
|
3846
|
+
"clean",
|
|
3847
|
+
"shape",
|
|
3848
|
+
"keys",
|
|
3849
|
+
"public"
|
|
3850
|
+
]);
|
|
3851
|
+
function isRawBlobName(name) {
|
|
3852
|
+
if (REDACTION_RE.test(name) || BLOB_REDACTION_RE.test(name)) {
|
|
3853
|
+
return false;
|
|
3854
|
+
}
|
|
3855
|
+
const tokens = tokenize(name);
|
|
3856
|
+
if (tokens.some((tok) => BLOB_REDACTION_TOKENS.has(tok))) {
|
|
3857
|
+
return false;
|
|
3858
|
+
}
|
|
3859
|
+
const first = leadingWord(name);
|
|
3860
|
+
if (first !== void 0 && FLAG_PREFIXES.has(first)) {
|
|
3861
|
+
return false;
|
|
3862
|
+
}
|
|
3863
|
+
if (RAW_BLOB_IDENTIFIERS.has(name.toLowerCase())) {
|
|
3864
|
+
return true;
|
|
3865
|
+
}
|
|
3866
|
+
const last = tokens.at(-1);
|
|
3867
|
+
return last !== void 0 && RAW_BLOB_WORDS.has(last);
|
|
3868
|
+
}
|
|
3869
|
+
function rawBlobValueName(value) {
|
|
3870
|
+
if (value.type === "Identifier") {
|
|
3871
|
+
return isRawBlobName(value.name) ? value.name : null;
|
|
3872
|
+
}
|
|
3873
|
+
if (value.type === "MemberExpression" && !value.computed && value.property.type === "Identifier") {
|
|
3874
|
+
return isRawBlobName(value.property.name) ? value.property.name : null;
|
|
3875
|
+
}
|
|
3876
|
+
return null;
|
|
3877
|
+
}
|
|
3731
3878
|
function propertyKeyName2(prop) {
|
|
3732
3879
|
if (prop.computed) {
|
|
3733
3880
|
return null;
|
|
@@ -3747,7 +3894,7 @@ var no_secret_in_log_default = import_utils28.ESLintUtils.RuleCreator(
|
|
|
3747
3894
|
meta: {
|
|
3748
3895
|
type: "problem",
|
|
3749
3896
|
docs: {
|
|
3750
|
-
description: "Disallow passing a secret-named value to a logging call;
|
|
3897
|
+
description: "Disallow passing a secret-named value or a raw request/response blob to a logging call; both leak to log sinks. Redact or omit."
|
|
3751
3898
|
},
|
|
3752
3899
|
schema: [
|
|
3753
3900
|
{
|
|
@@ -3757,52 +3904,58 @@ var no_secret_in_log_default = import_utils28.ESLintUtils.RuleCreator(
|
|
|
3757
3904
|
}
|
|
3758
3905
|
],
|
|
3759
3906
|
messages: {
|
|
3760
|
-
noSecretInLog: "Secret `{{name}}` passed to a logging call leaks it to log sinks. Redact (e.g. `{{name}}Prefix: {{name}}.slice(0, 6)`) or omit it."
|
|
3907
|
+
noSecretInLog: "Secret `{{name}}` passed to a logging call leaks it to log sinks. Redact (e.g. `{{name}}Prefix: {{name}}.slice(0, 6)`) or omit it.",
|
|
3908
|
+
noRawBodyInLog: "Raw `{{name}}` passed to a logging call. Request/response blobs carry PII and often echo credentials back, and log sinks have no retention policy. Log a derived value instead (a status, `{{name}}.id`, a length, a truncated issue list) or pass it through a redactor (`redact({{name}})`)."
|
|
3761
3909
|
}
|
|
3762
3910
|
},
|
|
3763
3911
|
defaultOptions: [{}],
|
|
3764
3912
|
create(context, [loggingOptions]) {
|
|
3765
3913
|
const matcher = createLogMatcher(loggingOptions);
|
|
3914
|
+
const blobArmApplies = !isTestFile(context.filename);
|
|
3915
|
+
function reportSecretArgument(arg) {
|
|
3916
|
+
const name = arg.type === "Identifier" ? arg.name : arg.type === "MemberExpression" && !arg.computed && arg.property.type === "Identifier" ? arg.property.name : null;
|
|
3917
|
+
if (name === null || !isSecretKeyword(name)) {
|
|
3918
|
+
return false;
|
|
3919
|
+
}
|
|
3920
|
+
context.report({ node: arg, messageId: "noSecretInLog", data: { name } });
|
|
3921
|
+
return true;
|
|
3922
|
+
}
|
|
3923
|
+
function reportSecretProperty(prop) {
|
|
3924
|
+
const keyName2 = propertyKeyName2(prop);
|
|
3925
|
+
if (keyName2 === null || !isSecretKeyword(keyName2) || !isRawSecretValue(prop)) {
|
|
3926
|
+
return false;
|
|
3927
|
+
}
|
|
3928
|
+
context.report({ node: prop, messageId: "noSecretInLog", data: { name: keyName2 } });
|
|
3929
|
+
return true;
|
|
3930
|
+
}
|
|
3931
|
+
function reportRawBlob(node, value) {
|
|
3932
|
+
if (!blobArmApplies) {
|
|
3933
|
+
return;
|
|
3934
|
+
}
|
|
3935
|
+
const name = rawBlobValueName(value);
|
|
3936
|
+
if (name !== null) {
|
|
3937
|
+
context.report({ node, messageId: "noRawBodyInLog", data: { name } });
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3766
3940
|
return {
|
|
3767
3941
|
CallExpression(node) {
|
|
3768
3942
|
if (!matcher.isLoggingCall(node)) {
|
|
3769
3943
|
return;
|
|
3770
3944
|
}
|
|
3771
3945
|
for (const arg of node.arguments) {
|
|
3772
|
-
if (arg.type === "Identifier") {
|
|
3773
|
-
if (isSecretKeyword(arg.name)) {
|
|
3774
|
-
context.report({
|
|
3775
|
-
node: arg,
|
|
3776
|
-
messageId: "noSecretInLog",
|
|
3777
|
-
data: { name: arg.name }
|
|
3778
|
-
});
|
|
3779
|
-
}
|
|
3780
|
-
continue;
|
|
3781
|
-
}
|
|
3782
|
-
if (arg.type === "MemberExpression") {
|
|
3783
|
-
if (!arg.computed && arg.property.type === "Identifier" && isSecretKeyword(arg.property.name)) {
|
|
3784
|
-
context.report({
|
|
3785
|
-
node: arg,
|
|
3786
|
-
messageId: "noSecretInLog",
|
|
3787
|
-
data: { name: arg.property.name }
|
|
3788
|
-
});
|
|
3789
|
-
}
|
|
3790
|
-
continue;
|
|
3791
|
-
}
|
|
3792
3946
|
if (arg.type === "ObjectExpression") {
|
|
3793
3947
|
for (const prop of arg.properties) {
|
|
3794
3948
|
if (prop.type !== "Property") {
|
|
3795
3949
|
continue;
|
|
3796
3950
|
}
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
context.report({
|
|
3800
|
-
node: prop,
|
|
3801
|
-
messageId: "noSecretInLog",
|
|
3802
|
-
data: { name: keyName2 }
|
|
3803
|
-
});
|
|
3951
|
+
if (!reportSecretProperty(prop)) {
|
|
3952
|
+
reportRawBlob(prop, prop.value);
|
|
3804
3953
|
}
|
|
3805
3954
|
}
|
|
3955
|
+
continue;
|
|
3956
|
+
}
|
|
3957
|
+
if (!reportSecretArgument(arg)) {
|
|
3958
|
+
reportRawBlob(arg, arg);
|
|
3806
3959
|
}
|
|
3807
3960
|
}
|
|
3808
3961
|
}
|
|
@@ -4577,7 +4730,7 @@ function functionName(node) {
|
|
|
4577
4730
|
}
|
|
4578
4731
|
return null;
|
|
4579
4732
|
}
|
|
4580
|
-
function
|
|
4733
|
+
function isInlineExported(node) {
|
|
4581
4734
|
for (let current = node; current != null; current = current.parent) {
|
|
4582
4735
|
const parent = current.parent;
|
|
4583
4736
|
if (parent?.type === import_utils35.AST_NODE_TYPES.ExportNamedDeclaration || parent?.type === import_utils35.AST_NODE_TYPES.ExportDefaultDeclaration) {
|
|
@@ -4586,6 +4739,58 @@ function isExported(node) {
|
|
|
4586
4739
|
}
|
|
4587
4740
|
return false;
|
|
4588
4741
|
}
|
|
4742
|
+
function moduleScopeBindingName(node) {
|
|
4743
|
+
let current = node;
|
|
4744
|
+
let child = node;
|
|
4745
|
+
while (current.parent != null && current.parent.type !== import_utils35.AST_NODE_TYPES.Program) {
|
|
4746
|
+
child = current;
|
|
4747
|
+
current = current.parent;
|
|
4748
|
+
}
|
|
4749
|
+
if (current.parent?.type !== import_utils35.AST_NODE_TYPES.Program) {
|
|
4750
|
+
return null;
|
|
4751
|
+
}
|
|
4752
|
+
if (current.type === import_utils35.AST_NODE_TYPES.FunctionDeclaration || current.type === import_utils35.AST_NODE_TYPES.ClassDeclaration) {
|
|
4753
|
+
return current.id?.name ?? null;
|
|
4754
|
+
}
|
|
4755
|
+
if (current.type === import_utils35.AST_NODE_TYPES.VariableDeclaration) {
|
|
4756
|
+
if (child.type !== import_utils35.AST_NODE_TYPES.VariableDeclarator || child.id.type !== import_utils35.AST_NODE_TYPES.Identifier) {
|
|
4757
|
+
return null;
|
|
4758
|
+
}
|
|
4759
|
+
return child.id.name;
|
|
4760
|
+
}
|
|
4761
|
+
return null;
|
|
4762
|
+
}
|
|
4763
|
+
function specifierExportedNames(program) {
|
|
4764
|
+
const names = /* @__PURE__ */ new Set();
|
|
4765
|
+
for (const statement of program.body) {
|
|
4766
|
+
if (statement.type === import_utils35.AST_NODE_TYPES.ExportNamedDeclaration && statement.declaration == null && statement.source == null && statement.exportKind !== "type") {
|
|
4767
|
+
for (const specifier of statement.specifiers) {
|
|
4768
|
+
if (specifier.exportKind !== "type" && specifier.local.type === import_utils35.AST_NODE_TYPES.Identifier) {
|
|
4769
|
+
names.add(specifier.local.name);
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
continue;
|
|
4773
|
+
}
|
|
4774
|
+
if (statement.type === import_utils35.AST_NODE_TYPES.ExportDefaultDeclaration && statement.declaration.type === import_utils35.AST_NODE_TYPES.Identifier) {
|
|
4775
|
+
names.add(statement.declaration.name);
|
|
4776
|
+
continue;
|
|
4777
|
+
}
|
|
4778
|
+
if (statement.type === import_utils35.AST_NODE_TYPES.TSExportAssignment && statement.expression.type === import_utils35.AST_NODE_TYPES.Identifier) {
|
|
4779
|
+
names.add(statement.expression.name);
|
|
4780
|
+
}
|
|
4781
|
+
}
|
|
4782
|
+
return names;
|
|
4783
|
+
}
|
|
4784
|
+
function isExported(node, specifierExports) {
|
|
4785
|
+
if (isInlineExported(node)) {
|
|
4786
|
+
return true;
|
|
4787
|
+
}
|
|
4788
|
+
if (specifierExports.size === 0) {
|
|
4789
|
+
return false;
|
|
4790
|
+
}
|
|
4791
|
+
const binding = moduleScopeBindingName(node);
|
|
4792
|
+
return binding !== null && specifierExports.has(binding);
|
|
4793
|
+
}
|
|
4589
4794
|
var no_positional_tuple_return_default = import_utils35.ESLintUtils.RuleCreator(
|
|
4590
4795
|
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
4591
4796
|
)({
|
|
@@ -4602,6 +4807,7 @@ var no_positional_tuple_return_default = import_utils35.ESLintUtils.RuleCreator(
|
|
|
4602
4807
|
},
|
|
4603
4808
|
defaultOptions: [],
|
|
4604
4809
|
create(context) {
|
|
4810
|
+
const specifierExports = specifierExportedNames(context.sourceCode.ast);
|
|
4605
4811
|
const check = (node) => {
|
|
4606
4812
|
const annotation = node.returnType?.typeAnnotation;
|
|
4607
4813
|
if (annotation === void 0) {
|
|
@@ -4615,7 +4821,7 @@ var no_positional_tuple_return_default = import_utils35.ESLintUtils.RuleCreator(
|
|
|
4615
4821
|
if (name === null || name.startsWith("_") || /^use[A-Z]/.test(name)) {
|
|
4616
4822
|
return;
|
|
4617
4823
|
}
|
|
4618
|
-
if (!isExported(node)) {
|
|
4824
|
+
if (!isExported(node, specifierExports)) {
|
|
4619
4825
|
return;
|
|
4620
4826
|
}
|
|
4621
4827
|
context.report({
|
|
@@ -5322,6 +5528,484 @@ var no_storage_in_stateless_modules_default = import_utils43.ESLintUtils.RuleCre
|
|
|
5322
5528
|
}
|
|
5323
5529
|
});
|
|
5324
5530
|
|
|
5531
|
+
// src/rules/no-zod-native-enum.ts
|
|
5532
|
+
var import_utils44 = require("@typescript-eslint/utils");
|
|
5533
|
+
var ts2 = __toESM(require("typescript"), 1);
|
|
5534
|
+
var IGNORE_PATTERNS2 = [
|
|
5535
|
+
/[\\/]generated[\\/]/,
|
|
5536
|
+
/\.gen\.tsx?$/,
|
|
5537
|
+
/\.generated\.tsx?$/,
|
|
5538
|
+
/\.d\.ts$/
|
|
5539
|
+
];
|
|
5540
|
+
function isIgnoredFile2(filename, sourceText) {
|
|
5541
|
+
if (IGNORE_PATTERNS2.some((re) => re.test(filename))) {
|
|
5542
|
+
return true;
|
|
5543
|
+
}
|
|
5544
|
+
return /@generated\b/.test(sourceText.slice(0, 1024));
|
|
5545
|
+
}
|
|
5546
|
+
function isZodModule(source) {
|
|
5547
|
+
return /(^|[/@-])zod([/-]|$)/.test(source);
|
|
5548
|
+
}
|
|
5549
|
+
function unwrap3(node) {
|
|
5550
|
+
if (node.type === import_utils44.AST_NODE_TYPES.TSAsExpression || node.type === import_utils44.AST_NODE_TYPES.TSSatisfiesExpression) {
|
|
5551
|
+
return unwrap3(node.expression);
|
|
5552
|
+
}
|
|
5553
|
+
return node;
|
|
5554
|
+
}
|
|
5555
|
+
function stringValueTexts(node, sourceCode) {
|
|
5556
|
+
const texts = [];
|
|
5557
|
+
for (const prop of node.properties) {
|
|
5558
|
+
if (prop.type !== import_utils44.AST_NODE_TYPES.Property) {
|
|
5559
|
+
return null;
|
|
5560
|
+
}
|
|
5561
|
+
if (prop.computed || prop.shorthand || prop.method || prop.kind !== "init") {
|
|
5562
|
+
return null;
|
|
5563
|
+
}
|
|
5564
|
+
const value = prop.value;
|
|
5565
|
+
if (value.type !== import_utils44.AST_NODE_TYPES.Literal || typeof value.value !== "string") {
|
|
5566
|
+
return null;
|
|
5567
|
+
}
|
|
5568
|
+
const text = sourceCode.getText(value);
|
|
5569
|
+
if (!texts.includes(text)) {
|
|
5570
|
+
texts.push(text);
|
|
5571
|
+
}
|
|
5572
|
+
}
|
|
5573
|
+
return texts.length > 0 ? texts : null;
|
|
5574
|
+
}
|
|
5575
|
+
function resolvesToLocalEnum(node, scope) {
|
|
5576
|
+
let current = scope;
|
|
5577
|
+
while (current !== null) {
|
|
5578
|
+
const variable = current.variables.find((v) => v.name === node.name);
|
|
5579
|
+
if (variable !== void 0) {
|
|
5580
|
+
return variable.defs.some(
|
|
5581
|
+
(def) => def.node.type === import_utils44.AST_NODE_TYPES.TSEnumDeclaration
|
|
5582
|
+
);
|
|
5583
|
+
}
|
|
5584
|
+
current = current.upper;
|
|
5585
|
+
}
|
|
5586
|
+
return false;
|
|
5587
|
+
}
|
|
5588
|
+
var ENUM_SYMBOL_FLAGS = ts2.SymbolFlags.RegularEnum | ts2.SymbolFlags.ConstEnum | ts2.SymbolFlags.Enum;
|
|
5589
|
+
function resolvesToImportedEnum(node, services) {
|
|
5590
|
+
const checker = services.program.getTypeChecker();
|
|
5591
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
|
|
5592
|
+
let symbol = checker.getSymbolAtLocation(tsNode);
|
|
5593
|
+
if (symbol === void 0) {
|
|
5594
|
+
return false;
|
|
5595
|
+
}
|
|
5596
|
+
if ((symbol.flags & ts2.SymbolFlags.Alias) !== 0) {
|
|
5597
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
5598
|
+
}
|
|
5599
|
+
return (symbol.flags & ENUM_SYMBOL_FLAGS) !== 0;
|
|
5600
|
+
}
|
|
5601
|
+
var no_zod_native_enum_default = import_utils44.ESLintUtils.RuleCreator(
|
|
5602
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
5603
|
+
)({
|
|
5604
|
+
name: "no-zod-native-enum",
|
|
5605
|
+
meta: {
|
|
5606
|
+
type: "suggestion",
|
|
5607
|
+
fixable: "code",
|
|
5608
|
+
docs: {
|
|
5609
|
+
description: 'Disallow `z.nativeEnum()` (and `z.enum()` over a TypeScript enum); use `z.enum(["a", "b"])` with a string-literal union instead.'
|
|
5610
|
+
},
|
|
5611
|
+
schema: [],
|
|
5612
|
+
messages: {
|
|
5613
|
+
nativeEnum: '`z.nativeEnum()` exists to wrap a TypeScript `enum`, which `no-enum` bans. Use `z.enum(["a", "b"])` and derive the union with `z.infer<typeof Schema>`.',
|
|
5614
|
+
enumOfTsEnum: '`z.enum()` is being passed the TypeScript enum `{{name}}`, which `no-enum` bans. Pass a string-literal array instead: `z.enum(["a", "b"])`.'
|
|
5615
|
+
}
|
|
5616
|
+
},
|
|
5617
|
+
defaultOptions: [],
|
|
5618
|
+
create(context) {
|
|
5619
|
+
const sourceCode = context.sourceCode;
|
|
5620
|
+
if (isIgnoredFile2(context.filename, sourceCode.getText())) {
|
|
5621
|
+
return {};
|
|
5622
|
+
}
|
|
5623
|
+
let services;
|
|
5624
|
+
try {
|
|
5625
|
+
services = import_utils44.ESLintUtils.getParserServices(context);
|
|
5626
|
+
} catch {
|
|
5627
|
+
services = null;
|
|
5628
|
+
}
|
|
5629
|
+
const zodImportedNames = /* @__PURE__ */ new Map();
|
|
5630
|
+
function isZodMemberCall(node, api) {
|
|
5631
|
+
const callee = node.callee;
|
|
5632
|
+
if (callee.type === import_utils44.AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === import_utils44.AST_NODE_TYPES.Identifier) {
|
|
5633
|
+
return callee.property.name === api;
|
|
5634
|
+
}
|
|
5635
|
+
if (callee.type === import_utils44.AST_NODE_TYPES.Identifier) {
|
|
5636
|
+
return zodImportedNames.get(callee.name) === api;
|
|
5637
|
+
}
|
|
5638
|
+
return false;
|
|
5639
|
+
}
|
|
5640
|
+
function buildFix(node) {
|
|
5641
|
+
const callee = node.callee;
|
|
5642
|
+
if (callee.type !== import_utils44.AST_NODE_TYPES.MemberExpression || callee.property.type !== import_utils44.AST_NODE_TYPES.Identifier) {
|
|
5643
|
+
return null;
|
|
5644
|
+
}
|
|
5645
|
+
const arg = node.arguments[0];
|
|
5646
|
+
if (arg === void 0 || node.arguments.length !== 1 || arg.type === import_utils44.AST_NODE_TYPES.SpreadElement) {
|
|
5647
|
+
return null;
|
|
5648
|
+
}
|
|
5649
|
+
const inner = unwrap3(arg);
|
|
5650
|
+
if (inner.type !== import_utils44.AST_NODE_TYPES.ObjectExpression) {
|
|
5651
|
+
return null;
|
|
5652
|
+
}
|
|
5653
|
+
const values = stringValueTexts(inner, sourceCode);
|
|
5654
|
+
if (values === null) {
|
|
5655
|
+
return null;
|
|
5656
|
+
}
|
|
5657
|
+
const property = callee.property;
|
|
5658
|
+
const replacementArg = `[${values.join(", ")}]`;
|
|
5659
|
+
return (fixer) => [
|
|
5660
|
+
fixer.replaceText(property, "enum"),
|
|
5661
|
+
fixer.replaceText(arg, replacementArg)
|
|
5662
|
+
];
|
|
5663
|
+
}
|
|
5664
|
+
return {
|
|
5665
|
+
ImportDeclaration(node) {
|
|
5666
|
+
if (!isZodModule(node.source.value)) {
|
|
5667
|
+
return;
|
|
5668
|
+
}
|
|
5669
|
+
for (const spec of node.specifiers) {
|
|
5670
|
+
if (spec.type === import_utils44.AST_NODE_TYPES.ImportSpecifier && spec.imported.type === import_utils44.AST_NODE_TYPES.Identifier) {
|
|
5671
|
+
zodImportedNames.set(spec.local.name, spec.imported.name);
|
|
5672
|
+
}
|
|
5673
|
+
}
|
|
5674
|
+
},
|
|
5675
|
+
CallExpression(node) {
|
|
5676
|
+
if (isZodMemberCall(node, "nativeEnum")) {
|
|
5677
|
+
const fix = buildFix(node);
|
|
5678
|
+
context.report({
|
|
5679
|
+
node,
|
|
5680
|
+
messageId: "nativeEnum",
|
|
5681
|
+
...fix === null ? {} : { fix }
|
|
5682
|
+
});
|
|
5683
|
+
return;
|
|
5684
|
+
}
|
|
5685
|
+
if (!isZodMemberCall(node, "enum")) {
|
|
5686
|
+
return;
|
|
5687
|
+
}
|
|
5688
|
+
const arg = node.arguments[0];
|
|
5689
|
+
if (arg === void 0 || arg.type !== import_utils44.AST_NODE_TYPES.Identifier) {
|
|
5690
|
+
return;
|
|
5691
|
+
}
|
|
5692
|
+
const isEnum = resolvesToLocalEnum(arg, sourceCode.getScope(arg)) || services !== null && resolvesToImportedEnum(arg, services);
|
|
5693
|
+
if (isEnum) {
|
|
5694
|
+
context.report({
|
|
5695
|
+
node,
|
|
5696
|
+
messageId: "enumOfTsEnum",
|
|
5697
|
+
data: { name: arg.name }
|
|
5698
|
+
});
|
|
5699
|
+
}
|
|
5700
|
+
}
|
|
5701
|
+
};
|
|
5702
|
+
}
|
|
5703
|
+
});
|
|
5704
|
+
|
|
5705
|
+
// src/rules/prefer-module-level-constant.ts
|
|
5706
|
+
var import_utils45 = require("@typescript-eslint/utils");
|
|
5707
|
+
var DEFAULT_MIN_ELEMENTS = 3;
|
|
5708
|
+
var MAX_LITERAL_DEPTH = 4;
|
|
5709
|
+
var IGNORE_PATTERNS3 = [
|
|
5710
|
+
/[\\/]generated[\\/]/,
|
|
5711
|
+
/\.gen\.tsx?$/,
|
|
5712
|
+
/\.generated\.tsx?$/,
|
|
5713
|
+
/\.d\.ts$/
|
|
5714
|
+
];
|
|
5715
|
+
var TEST_FILE_PATTERNS = [
|
|
5716
|
+
/\.(?:test|spec)\.[cm]?[jt]sx?$/,
|
|
5717
|
+
/[\\/]__tests__[\\/]/,
|
|
5718
|
+
/[\\/]__mocks__[\\/]/,
|
|
5719
|
+
/[\\/]tests?[\\/]/,
|
|
5720
|
+
/\.stories\.[cm]?[jt]sx?$/
|
|
5721
|
+
];
|
|
5722
|
+
var MUTATING_METHODS = /* @__PURE__ */ new Set([
|
|
5723
|
+
// Array
|
|
5724
|
+
"push",
|
|
5725
|
+
"pop",
|
|
5726
|
+
"shift",
|
|
5727
|
+
"unshift",
|
|
5728
|
+
"splice",
|
|
5729
|
+
"sort",
|
|
5730
|
+
"reverse",
|
|
5731
|
+
"fill",
|
|
5732
|
+
"copyWithin",
|
|
5733
|
+
// Set / Map
|
|
5734
|
+
"add",
|
|
5735
|
+
"set",
|
|
5736
|
+
"delete",
|
|
5737
|
+
"clear",
|
|
5738
|
+
// Object-ish escape hatches
|
|
5739
|
+
"assign"
|
|
5740
|
+
]);
|
|
5741
|
+
var FUNCTION_TYPES3 = /* @__PURE__ */ new Set([
|
|
5742
|
+
import_utils45.AST_NODE_TYPES.FunctionDeclaration,
|
|
5743
|
+
import_utils45.AST_NODE_TYPES.FunctionExpression,
|
|
5744
|
+
import_utils45.AST_NODE_TYPES.ArrowFunctionExpression
|
|
5745
|
+
]);
|
|
5746
|
+
var COLLECTION_CONSTRUCTORS = /* @__PURE__ */ new Set(["Set", "Map"]);
|
|
5747
|
+
function isIgnoredFile3(filename, sourceText) {
|
|
5748
|
+
if (IGNORE_PATTERNS3.some((re) => re.test(filename))) {
|
|
5749
|
+
return true;
|
|
5750
|
+
}
|
|
5751
|
+
return /@generated\b/.test(sourceText.slice(0, 1024));
|
|
5752
|
+
}
|
|
5753
|
+
function isTestFile2(filename) {
|
|
5754
|
+
return TEST_FILE_PATTERNS.some((re) => re.test(filename));
|
|
5755
|
+
}
|
|
5756
|
+
function unwrap4(node) {
|
|
5757
|
+
if (node.type === import_utils45.AST_NODE_TYPES.TSAsExpression || node.type === import_utils45.AST_NODE_TYPES.TSSatisfiesExpression || node.type === import_utils45.AST_NODE_TYPES.TSNonNullExpression) {
|
|
5758
|
+
return unwrap4(node.expression);
|
|
5759
|
+
}
|
|
5760
|
+
return node;
|
|
5761
|
+
}
|
|
5762
|
+
function isRegexLiteral(node) {
|
|
5763
|
+
return node.type === import_utils45.AST_NODE_TYPES.Literal && "regex" in node && node.regex !== void 0;
|
|
5764
|
+
}
|
|
5765
|
+
function isLiteralOnly(node, depth) {
|
|
5766
|
+
if (depth > MAX_LITERAL_DEPTH) {
|
|
5767
|
+
return false;
|
|
5768
|
+
}
|
|
5769
|
+
const inner = unwrap4(node);
|
|
5770
|
+
switch (inner.type) {
|
|
5771
|
+
case import_utils45.AST_NODE_TYPES.Literal: {
|
|
5772
|
+
return true;
|
|
5773
|
+
}
|
|
5774
|
+
case import_utils45.AST_NODE_TYPES.TemplateLiteral: {
|
|
5775
|
+
return inner.expressions.length === 0;
|
|
5776
|
+
}
|
|
5777
|
+
case import_utils45.AST_NODE_TYPES.UnaryExpression: {
|
|
5778
|
+
return (inner.operator === "-" || inner.operator === "+") && inner.argument.type === import_utils45.AST_NODE_TYPES.Literal && typeof inner.argument.value === "number";
|
|
5779
|
+
}
|
|
5780
|
+
case import_utils45.AST_NODE_TYPES.ArrayExpression: {
|
|
5781
|
+
return inner.elements.every(
|
|
5782
|
+
(el) => el !== null && el.type !== import_utils45.AST_NODE_TYPES.SpreadElement && isLiteralOnly(el, depth + 1)
|
|
5783
|
+
);
|
|
5784
|
+
}
|
|
5785
|
+
case import_utils45.AST_NODE_TYPES.ObjectExpression: {
|
|
5786
|
+
return inner.properties.every((prop) => {
|
|
5787
|
+
if (prop.type !== import_utils45.AST_NODE_TYPES.Property) {
|
|
5788
|
+
return false;
|
|
5789
|
+
}
|
|
5790
|
+
if (prop.shorthand || prop.method || prop.kind !== "init") {
|
|
5791
|
+
return false;
|
|
5792
|
+
}
|
|
5793
|
+
if (prop.computed && prop.key.type !== import_utils45.AST_NODE_TYPES.Literal) {
|
|
5794
|
+
return false;
|
|
5795
|
+
}
|
|
5796
|
+
return isLiteralOnly(prop.value, depth + 1);
|
|
5797
|
+
});
|
|
5798
|
+
}
|
|
5799
|
+
default: {
|
|
5800
|
+
return false;
|
|
5801
|
+
}
|
|
5802
|
+
}
|
|
5803
|
+
}
|
|
5804
|
+
function unwrapObjectFreeze(node) {
|
|
5805
|
+
const inner = unwrap4(node);
|
|
5806
|
+
if (inner.type === import_utils45.AST_NODE_TYPES.CallExpression && inner.callee.type === import_utils45.AST_NODE_TYPES.MemberExpression && !inner.callee.computed && inner.callee.object.type === import_utils45.AST_NODE_TYPES.Identifier && inner.callee.object.name === "Object" && inner.callee.property.type === import_utils45.AST_NODE_TYPES.Identifier && inner.callee.property.name === "freeze" && inner.arguments.length === 1 && inner.arguments[0] !== void 0 && inner.arguments[0].type !== import_utils45.AST_NODE_TYPES.SpreadElement) {
|
|
5807
|
+
return unwrap4(inner.arguments[0]);
|
|
5808
|
+
}
|
|
5809
|
+
return inner;
|
|
5810
|
+
}
|
|
5811
|
+
function classify(init, checkRegex) {
|
|
5812
|
+
const node = unwrapObjectFreeze(init);
|
|
5813
|
+
if (isRegexLiteral(node)) {
|
|
5814
|
+
if (!checkRegex) {
|
|
5815
|
+
return null;
|
|
5816
|
+
}
|
|
5817
|
+
if (/[gy]/.test(node.regex.flags)) {
|
|
5818
|
+
return null;
|
|
5819
|
+
}
|
|
5820
|
+
return { kind: "regex", size: 1 };
|
|
5821
|
+
}
|
|
5822
|
+
if (node.type === import_utils45.AST_NODE_TYPES.ArrayExpression) {
|
|
5823
|
+
return isLiteralOnly(node, 0) ? { kind: "array", size: node.elements.length } : null;
|
|
5824
|
+
}
|
|
5825
|
+
if (node.type === import_utils45.AST_NODE_TYPES.ObjectExpression) {
|
|
5826
|
+
return isLiteralOnly(node, 0) ? { kind: "object", size: node.properties.length } : null;
|
|
5827
|
+
}
|
|
5828
|
+
if (node.type === import_utils45.AST_NODE_TYPES.NewExpression && node.callee.type === import_utils45.AST_NODE_TYPES.Identifier && COLLECTION_CONSTRUCTORS.has(node.callee.name)) {
|
|
5829
|
+
const arg = node.arguments[0];
|
|
5830
|
+
if (node.arguments.length !== 1 || arg === void 0 || arg.type === import_utils45.AST_NODE_TYPES.SpreadElement) {
|
|
5831
|
+
return null;
|
|
5832
|
+
}
|
|
5833
|
+
const entries = unwrap4(arg);
|
|
5834
|
+
if (entries.type !== import_utils45.AST_NODE_TYPES.ArrayExpression) {
|
|
5835
|
+
return null;
|
|
5836
|
+
}
|
|
5837
|
+
return isLiteralOnly(entries, 0) ? { kind: node.callee.name === "Set" ? "Set" : "Map", size: entries.elements.length } : null;
|
|
5838
|
+
}
|
|
5839
|
+
return null;
|
|
5840
|
+
}
|
|
5841
|
+
function enclosingFunction2(node) {
|
|
5842
|
+
let current = node.parent;
|
|
5843
|
+
while (current !== void 0 && current !== null) {
|
|
5844
|
+
if (FUNCTION_TYPES3.has(current.type)) {
|
|
5845
|
+
return current;
|
|
5846
|
+
}
|
|
5847
|
+
current = current.parent;
|
|
5848
|
+
}
|
|
5849
|
+
return null;
|
|
5850
|
+
}
|
|
5851
|
+
var NON_RETAINING_BUILTINS = /* @__PURE__ */ new Map(
|
|
5852
|
+
[
|
|
5853
|
+
[
|
|
5854
|
+
"Object",
|
|
5855
|
+
/* @__PURE__ */ new Set(["keys", "values", "entries", "freeze", "fromEntries", "assign"])
|
|
5856
|
+
],
|
|
5857
|
+
["Array", /* @__PURE__ */ new Set(["from", "isArray"])],
|
|
5858
|
+
["JSON", /* @__PURE__ */ new Set(["stringify"])]
|
|
5859
|
+
]
|
|
5860
|
+
);
|
|
5861
|
+
function isNonRetainingBuiltinCall(node, argument) {
|
|
5862
|
+
const callee = node.callee;
|
|
5863
|
+
if (callee.type === import_utils45.AST_NODE_TYPES.Identifier && callee.name === "structuredClone") {
|
|
5864
|
+
return true;
|
|
5865
|
+
}
|
|
5866
|
+
if (callee.type !== import_utils45.AST_NODE_TYPES.MemberExpression || callee.computed || callee.object.type !== import_utils45.AST_NODE_TYPES.Identifier || callee.property.type !== import_utils45.AST_NODE_TYPES.Identifier) {
|
|
5867
|
+
return false;
|
|
5868
|
+
}
|
|
5869
|
+
const members = NON_RETAINING_BUILTINS.get(callee.object.name);
|
|
5870
|
+
if (members === void 0 || !members.has(callee.property.name)) {
|
|
5871
|
+
return false;
|
|
5872
|
+
}
|
|
5873
|
+
if (callee.object.name === "Object" && callee.property.name === "assign") {
|
|
5874
|
+
return node.arguments[0] !== argument;
|
|
5875
|
+
}
|
|
5876
|
+
return true;
|
|
5877
|
+
}
|
|
5878
|
+
function isSafeRead(identifier) {
|
|
5879
|
+
const parent = identifier.parent;
|
|
5880
|
+
if (parent.type === import_utils45.AST_NODE_TYPES.MemberExpression) {
|
|
5881
|
+
if (parent.object !== identifier) {
|
|
5882
|
+
return true;
|
|
5883
|
+
}
|
|
5884
|
+
const grandparent = parent.parent;
|
|
5885
|
+
if (grandparent.type === import_utils45.AST_NODE_TYPES.AssignmentExpression && grandparent.left === parent) {
|
|
5886
|
+
return false;
|
|
5887
|
+
}
|
|
5888
|
+
if (grandparent.type === import_utils45.AST_NODE_TYPES.UpdateExpression) {
|
|
5889
|
+
return false;
|
|
5890
|
+
}
|
|
5891
|
+
if (grandparent.type === import_utils45.AST_NODE_TYPES.UnaryExpression && grandparent.operator === "delete") {
|
|
5892
|
+
return false;
|
|
5893
|
+
}
|
|
5894
|
+
if (!parent.computed && parent.property.type === import_utils45.AST_NODE_TYPES.Identifier && MUTATING_METHODS.has(parent.property.name) && grandparent.type === import_utils45.AST_NODE_TYPES.CallExpression && grandparent.callee === parent) {
|
|
5895
|
+
return false;
|
|
5896
|
+
}
|
|
5897
|
+
return true;
|
|
5898
|
+
}
|
|
5899
|
+
if (parent.type === import_utils45.AST_NODE_TYPES.ForOfStatement && parent.right === identifier) {
|
|
5900
|
+
return true;
|
|
5901
|
+
}
|
|
5902
|
+
if (parent.type === import_utils45.AST_NODE_TYPES.SpreadElement) {
|
|
5903
|
+
return true;
|
|
5904
|
+
}
|
|
5905
|
+
if (parent.type === import_utils45.AST_NODE_TYPES.BinaryExpression) {
|
|
5906
|
+
return true;
|
|
5907
|
+
}
|
|
5908
|
+
if (parent.type === import_utils45.AST_NODE_TYPES.CallExpression && parent.arguments.includes(identifier) && isNonRetainingBuiltinCall(parent, identifier)) {
|
|
5909
|
+
return true;
|
|
5910
|
+
}
|
|
5911
|
+
if (parent.type === import_utils45.AST_NODE_TYPES.UnaryExpression && parent.operator !== "delete") {
|
|
5912
|
+
return true;
|
|
5913
|
+
}
|
|
5914
|
+
return false;
|
|
5915
|
+
}
|
|
5916
|
+
var prefer_module_level_constant_default = import_utils45.ESLintUtils.RuleCreator(
|
|
5917
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
5918
|
+
)({
|
|
5919
|
+
name: "prefer-module-level-constant",
|
|
5920
|
+
meta: {
|
|
5921
|
+
type: "suggestion",
|
|
5922
|
+
docs: {
|
|
5923
|
+
description: "Hoist literal-only constant collections and regexes out of function bodies to module scope so they are allocated once."
|
|
5924
|
+
},
|
|
5925
|
+
schema: [
|
|
5926
|
+
{
|
|
5927
|
+
type: "object",
|
|
5928
|
+
additionalProperties: false,
|
|
5929
|
+
properties: {
|
|
5930
|
+
minElements: { type: "number", minimum: 1 },
|
|
5931
|
+
checkRegex: { type: "boolean" },
|
|
5932
|
+
ignoreTestFiles: { type: "boolean" }
|
|
5933
|
+
}
|
|
5934
|
+
}
|
|
5935
|
+
],
|
|
5936
|
+
messages: {
|
|
5937
|
+
hoistCollection: "`{{name}}` is a literal-only {{kind}} rebuilt on every call. Hoist it to module scope so it is allocated once and can be reused, exported, and tested.",
|
|
5938
|
+
hoistRegex: "`{{name}}` is a constant regex recompiled on every call. Hoist it to module scope."
|
|
5939
|
+
}
|
|
5940
|
+
},
|
|
5941
|
+
defaultOptions: [{}],
|
|
5942
|
+
create(context, [optionsArg]) {
|
|
5943
|
+
const options = optionsArg ?? {};
|
|
5944
|
+
const minElements = options.minElements ?? DEFAULT_MIN_ELEMENTS;
|
|
5945
|
+
const checkRegex = options.checkRegex ?? true;
|
|
5946
|
+
const ignoreTestFiles = options.ignoreTestFiles ?? true;
|
|
5947
|
+
const sourceCode = context.sourceCode;
|
|
5948
|
+
const filename = context.filename;
|
|
5949
|
+
if (isIgnoredFile3(filename, sourceCode.getText())) {
|
|
5950
|
+
return {};
|
|
5951
|
+
}
|
|
5952
|
+
if (ignoreTestFiles && isTestFile2(filename)) {
|
|
5953
|
+
return {};
|
|
5954
|
+
}
|
|
5955
|
+
function allReferencesAreSafeReads(declarator) {
|
|
5956
|
+
const variables = sourceCode.getDeclaredVariables(declarator);
|
|
5957
|
+
const variable = variables[0];
|
|
5958
|
+
if (variable === void 0) {
|
|
5959
|
+
return false;
|
|
5960
|
+
}
|
|
5961
|
+
for (const reference of variable.references) {
|
|
5962
|
+
if (reference.init === true) {
|
|
5963
|
+
continue;
|
|
5964
|
+
}
|
|
5965
|
+
if (reference.isWrite()) {
|
|
5966
|
+
return false;
|
|
5967
|
+
}
|
|
5968
|
+
if (reference.identifier.type !== import_utils45.AST_NODE_TYPES.Identifier) {
|
|
5969
|
+
return false;
|
|
5970
|
+
}
|
|
5971
|
+
if (!isSafeRead(reference.identifier)) {
|
|
5972
|
+
return false;
|
|
5973
|
+
}
|
|
5974
|
+
}
|
|
5975
|
+
return true;
|
|
5976
|
+
}
|
|
5977
|
+
return {
|
|
5978
|
+
VariableDeclarator(node) {
|
|
5979
|
+
const declaration = node.parent;
|
|
5980
|
+
if (declaration.type !== import_utils45.AST_NODE_TYPES.VariableDeclaration || declaration.kind !== "const" || declaration.declare === true) {
|
|
5981
|
+
return;
|
|
5982
|
+
}
|
|
5983
|
+
if (node.id.type !== import_utils45.AST_NODE_TYPES.Identifier || node.init === null) {
|
|
5984
|
+
return;
|
|
5985
|
+
}
|
|
5986
|
+
if (enclosingFunction2(node) === null) {
|
|
5987
|
+
return;
|
|
5988
|
+
}
|
|
5989
|
+
const candidate = classify(node.init, checkRegex);
|
|
5990
|
+
if (candidate === null) {
|
|
5991
|
+
return;
|
|
5992
|
+
}
|
|
5993
|
+
if (candidate.kind !== "regex" && candidate.size < minElements) {
|
|
5994
|
+
return;
|
|
5995
|
+
}
|
|
5996
|
+
if (!allReferencesAreSafeReads(node)) {
|
|
5997
|
+
return;
|
|
5998
|
+
}
|
|
5999
|
+
context.report({
|
|
6000
|
+
node: node.id,
|
|
6001
|
+
messageId: candidate.kind === "regex" ? "hoistRegex" : "hoistCollection",
|
|
6002
|
+
data: { name: node.id.name, kind: candidate.kind }
|
|
6003
|
+
});
|
|
6004
|
+
}
|
|
6005
|
+
};
|
|
6006
|
+
}
|
|
6007
|
+
});
|
|
6008
|
+
|
|
5325
6009
|
// src/index.ts
|
|
5326
6010
|
var rules = {
|
|
5327
6011
|
"enforce-file-structure": enforce_file_structure_default,
|
|
@@ -5362,12 +6046,14 @@ var rules = {
|
|
|
5362
6046
|
"store-insert-requires-on-conflict": store_insert_requires_on_conflict_default,
|
|
5363
6047
|
"no-dynamic-sql": no_dynamic_sql_default,
|
|
5364
6048
|
"no-raw-fetch-outside-clients": no_raw_fetch_outside_clients_default,
|
|
5365
|
-
"no-storage-in-stateless-modules": no_storage_in_stateless_modules_default
|
|
6049
|
+
"no-storage-in-stateless-modules": no_storage_in_stateless_modules_default,
|
|
6050
|
+
"no-zod-native-enum": no_zod_native_enum_default,
|
|
6051
|
+
"prefer-module-level-constant": prefer_module_level_constant_default
|
|
5366
6052
|
};
|
|
5367
6053
|
var plugin = {
|
|
5368
6054
|
meta: {
|
|
5369
6055
|
name: "@sarj/eslint-plugin",
|
|
5370
|
-
version: "2.
|
|
6056
|
+
version: "2.10.0"
|
|
5371
6057
|
},
|
|
5372
6058
|
rules,
|
|
5373
6059
|
configs: {
|
|
@@ -5416,7 +6102,14 @@ var plugin = {
|
|
|
5416
6102
|
"@sarj/no-repeated-string-literal": "warn",
|
|
5417
6103
|
"@sarj/no-positional-tuple-return": "warn",
|
|
5418
6104
|
// Injection guard — low FP, applies to any repo touching SQL.
|
|
5419
|
-
"@sarj/no-dynamic-sql": "warn"
|
|
6105
|
+
"@sarj/no-dynamic-sql": "warn",
|
|
6106
|
+
// Mined from two years of PR review (SARJ-928). Schema-layer sibling of
|
|
6107
|
+
// `no-enum`; autofixable for inline string-literal objects.
|
|
6108
|
+
"@sarj/no-zod-native-enum": "warn",
|
|
6109
|
+
// Mined from two years of PR review — the single most frequent uncovered
|
|
6110
|
+
// theme (~37 PRs). Measured 17 hits / 1085 real TS files, all true
|
|
6111
|
+
// positives, so it is safe to run everywhere.
|
|
6112
|
+
"@sarj/prefer-module-level-constant": "warn"
|
|
5420
6113
|
}
|
|
5421
6114
|
},
|
|
5422
6115
|
strict: {
|
|
@@ -5476,7 +6169,10 @@ var plugin = {
|
|
|
5476
6169
|
// and takes an `allow` list for repos that lay their client layer out
|
|
5477
6170
|
// differently.
|
|
5478
6171
|
"@sarj/no-raw-fetch-outside-clients": "error",
|
|
5479
|
-
"@sarj/no-storage-in-stateless-modules": "error"
|
|
6172
|
+
"@sarj/no-storage-in-stateless-modules": "error",
|
|
6173
|
+
// Mined from two years of PR review (SARJ-928).
|
|
6174
|
+
"@sarj/no-zod-native-enum": "error",
|
|
6175
|
+
"@sarj/prefer-module-level-constant": "error"
|
|
5480
6176
|
}
|
|
5481
6177
|
}
|
|
5482
6178
|
}
|