@sarj/eslint-plugin 2.9.0 → 2.11.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 CHANGED
@@ -287,12 +287,115 @@ function isProse(text) {
287
287
  }
288
288
  return false;
289
289
  }
290
- function isRedundantNarration(body) {
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 false;
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
- if (body !== void 0 && isRedundantNarration(body)) {
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; it leaks to log sinks. Redact or omit it."
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
- const keyName2 = propertyKeyName2(prop);
3798
- if (keyName2 !== null && isSecretKeyword(keyName2) && isRawSecretValue(prop)) {
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 isExported(node) {
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.9.0"
6056
+ version: "2.11.0"
5851
6057
  },
5852
6058
  rules,
5853
6059
  configs: {
@@ -5931,15 +6137,15 @@ var plugin = {
5931
6137
  "@sarj/no-comment-cruft": "error",
5932
6138
  // Frontend / styling — distilled from frontend PR-review mining. Stylistic,
5933
6139
  // no autofix → warn (rollout should prove the FP rate before raising it).
5934
- "@sarj/prefer-semantic-colors": "warn",
6140
+ "@sarj/prefer-semantic-colors": "error",
5935
6141
  // Ported from sarj-python-lint (SARJ), corpus-validated FP~0.
5936
6142
  "@sarj/no-fat-try-blocks": "error",
5937
6143
  "@sarj/no-cors-wildcard-with-credentials": "error",
5938
6144
  "@sarj/no-secret-in-log": "error",
5939
- "@sarj/no-unsafe-cast": "warn",
6145
+ "@sarj/no-unsafe-cast": "error",
5940
6146
  "@sarj/single-public-export": "error",
5941
- // High-volume/stylisticwarn until rollout proves FP rate.
5942
- "@sarj/prefer-string-literal-union": "warn",
6147
+ // Promoted to error 2026-07-25strict means strict (user directive).
6148
+ "@sarj/prefer-string-literal-union": "error",
5943
6149
  // Mined from 2y of PR review feedback + 5-repo code-smell audit (2026-07).
5944
6150
  "@sarj/require-fetch-timeout": "error",
5945
6151
  "@sarj/no-silent-promise-catch": "error",
@@ -5954,7 +6160,7 @@ var plugin = {
5954
6160
  // API-shape advice rather than a runtime defect — a corpus sweep found its
5955
6161
  // only hits are parser `[value, cursor]` returns, which are conventional.
5956
6162
  // Warn even in strict until a rollout justifies more.
5957
- "@sarj/no-positional-tuple-return": "warn",
6163
+ "@sarj/no-positional-tuple-return": "error",
5958
6164
  "@sarj/no-dynamic-sql": "error",
5959
6165
  // Architectural: both need per-repo config to be meaningful, so they
5960
6166
  // are strict-only. `no-storage-in-stateless-modules` is a no-op until