@sarj/eslint-plugin 2.9.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/dist/index.cjs +245 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +554 -348
- 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({
|
|
@@ -5847,7 +6053,7 @@ var rules = {
|
|
|
5847
6053
|
var plugin = {
|
|
5848
6054
|
meta: {
|
|
5849
6055
|
name: "@sarj/eslint-plugin",
|
|
5850
|
-
version: "2.
|
|
6056
|
+
version: "2.10.0"
|
|
5851
6057
|
},
|
|
5852
6058
|
rules,
|
|
5853
6059
|
configs: {
|