@sarj/eslint-plugin 15.6.8 → 15.7.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
@@ -4292,6 +4292,87 @@ var no_long_comment_default = createRule({
4292
4292
  }
4293
4293
  });
4294
4294
 
4295
+ // src/rules/no-vague-suppression-description.ts
4296
+ var DIRECTIVE_RE3 = /(?:eslint-(?:disable|disable-next-line|disable-line)|@ts-(?:expect-error|ignore))\b/iu;
4297
+ var DESCRIPTION_RE = /(?::|--)\s*(.+?)\s*$/u;
4298
+ var VAGUE_RE = /^(?:needed|required|intentional(?:ly)?|ignore(?:d)?|false positive|type error|typescript|to satisfy (?:the )?(?:linter|typescript|type checker))\.?$/iu;
4299
+ var noVagueSuppressionDescriptionDocumentation = {
4300
+ summary: "Require suppression descriptions to name the concrete mismatch or invariant instead of a generic non-reason.",
4301
+ rationale: "Generic phrases satisfy require-description mechanically while leaving reviewers unable to audit the risk or remove stale debt.",
4302
+ remediation: "Name the exact type/runtime mismatch, external contract, or safety invariant that makes this suppression acceptable.",
4303
+ category: "maintainability",
4304
+ limitations: [
4305
+ "Only ESLint disable comments and TypeScript expect-error/ignore directives are checked.",
4306
+ "The rule uses a small anchored vocabulary and does not score prose quality generally.",
4307
+ "Generated files and descriptions containing any concrete context are excluded."
4308
+ ],
4309
+ examples: [
4310
+ {
4311
+ id: "concrete-runtime-mismatch",
4312
+ title: "Explain the concrete runtime contract",
4313
+ outcome: "no-match",
4314
+ files: [
4315
+ {
4316
+ path: "src/adapter.ts",
4317
+ source: "// @ts-expect-error -- vendor types omit the runtime requestId field\nreturn response.requestId;"
4318
+ }
4319
+ ],
4320
+ focusPath: "src/adapter.ts",
4321
+ expectedCount: 0,
4322
+ public: true
4323
+ },
4324
+ {
4325
+ id: "generic-suppression-reason",
4326
+ title: "Reject a suppression with no auditable reason",
4327
+ outcome: "match",
4328
+ files: [
4329
+ {
4330
+ path: "src/adapter.ts",
4331
+ source: "// @ts-expect-error -- false positive\nreturn response.requestId;"
4332
+ }
4333
+ ],
4334
+ focusPath: "src/adapter.ts",
4335
+ expectedCount: 1,
4336
+ public: true
4337
+ }
4338
+ ]
4339
+ };
4340
+ var no_vague_suppression_description_default = createRule({
4341
+ name: "no-vague-suppression-description",
4342
+ documentation: noVagueSuppressionDescriptionDocumentation,
4343
+ meta: {
4344
+ type: "suggestion",
4345
+ docs: {
4346
+ description: "Require suppression descriptions to name the concrete mismatch or invariant instead of a generic non-reason."
4347
+ },
4348
+ schema: [],
4349
+ messages: {
4350
+ vagueDescription: "Suppression description `{{description}}` does not explain why the suppressed diagnostic is safe here. Name the concrete type/runtime mismatch, external contract, or invariant."
4351
+ }
4352
+ },
4353
+ defaultOptions: [],
4354
+ create(context) {
4355
+ if (isGeneratedFile(context.filename, context.sourceCode.text)) {
4356
+ return {};
4357
+ }
4358
+ return {
4359
+ Program() {
4360
+ for (const comment of context.sourceCode.getAllComments()) {
4361
+ const text = comment.value.trim();
4362
+ if (!DIRECTIVE_RE3.test(text)) continue;
4363
+ const description = DESCRIPTION_RE.exec(text)?.[1]?.trim();
4364
+ if (description === void 0 || !VAGUE_RE.test(description)) continue;
4365
+ context.report({
4366
+ loc: comment.loc,
4367
+ messageId: "vagueDescription",
4368
+ data: { description }
4369
+ });
4370
+ }
4371
+ }
4372
+ };
4373
+ }
4374
+ });
4375
+
4295
4376
  // src/rules/no-generic-single-export-module.ts
4296
4377
  var import_utils21 = require("@typescript-eslint/utils");
4297
4378
  var noGenericSingleExportModuleDocumentation = {
@@ -5431,7 +5512,7 @@ var no_repeated_string_literal_default = createRule({
5431
5512
  var import_utils28 = require("@typescript-eslint/utils");
5432
5513
  var MAX_WORDS = 8;
5433
5514
  var MIN_CONTENT_TOKENS = 2;
5434
- var DIRECTIVE_RE3 = /^(eslint\b|eslint-|sarj-noqa\b|@ts-|prettier-ignore|prettier\b|biome-|c8\b|v8\b|istanbul\b|@type\b|@vite|webpack|<reference|<amd|global\b|noinspection|todo\b|fixme\b|hack\b|xxx\b)/i;
5515
+ var DIRECTIVE_RE4 = /^(eslint\b|eslint-|sarj-noqa\b|@ts-|prettier-ignore|prettier\b|biome-|c8\b|v8\b|istanbul\b|@type\b|@vite|webpack|<reference|<amd|global\b|noinspection|todo\b|fixme\b|hack\b|xxx\b)/i;
5435
5516
  var CODEY_RE = /^[\w.$[\]'"]+\s*[:=]\s*\S|^[\w.$]+\s*\(|^(?:return|throw|await|import|export|const|let|var)\b.*[=()[\]{}]/;
5436
5517
  var BANNERISH_RE = /[=\-─-╿*#~_.]{3,}|^[A-Z0-9 _:-]+$/;
5437
5518
  var MODALITY_RE = /\b(?:can|could|should|shall|may|might|must|will|would|cannot)\b/i;
@@ -5530,7 +5611,7 @@ var no_restated_comment_default = createRule({
5530
5611
  }
5531
5612
  const body2 = comment.value.replace(/^\/*/, "").trim();
5532
5613
  if (body2.length === 0 || body2.endsWith("?")) continue;
5533
- if (DIRECTIVE_RE3.test(body2) || CODEY_RE.test(body2) || BANNERISH_RE.test(body2)) continue;
5614
+ if (DIRECTIVE_RE4.test(body2) || CODEY_RE.test(body2) || BANNERISH_RE.test(body2)) continue;
5534
5615
  if (NON_ASCII_LETTER_RE.test(body2) || isProtected(body2)) continue;
5535
5616
  if (MODALITY_RE.test(body2) || LEAD_IN_RE.test(body2) || EMPHASIS_RE.test(body2)) continue;
5536
5617
  if (NEGATION_WORD_RE.test(body2)) continue;
@@ -5578,7 +5659,7 @@ var MODELLED_TAGS = /* @__PURE__ */ new Set([
5578
5659
  ]);
5579
5660
  var PARAM_TAGS = /* @__PURE__ */ new Set(["arg", "argument", "param"]);
5580
5661
  var RETURN_TAGS = /* @__PURE__ */ new Set(["return", "returns"]);
5581
- var DIRECTIVE_RE4 = /^\s*(?:eslint\b|eslint-|@ts-|prettier|biome-|c8\b|v8\b|istanbul\b|@vite|webpack|@jsx|@jest-environment|@vitest-environment|#__)/i;
5662
+ var DIRECTIVE_RE5 = /^\s*(?:eslint\b|eslint-|@ts-|prettier|biome-|c8\b|v8\b|istanbul\b|@vite|webpack|@jsx|@jest-environment|@vitest-environment|#__)/i;
5582
5663
  var STOPWORDS2 = new Set(
5583
5664
  `the a an of to for in on with and or as at by is are was be been being
5584
5665
  this that it its if whether when where which what will would can could should
@@ -5694,7 +5775,7 @@ var no_restated_jsdoc_default = createRule({
5694
5775
  description,
5695
5776
  ...tags.filter((tag) => tag.name === "description").map((tag) => tag.text)
5696
5777
  ].filter((text) => text.length > 0).join("\n");
5697
- if (DIRECTIVE_RE4.test(describedText)) continue;
5778
+ if (DIRECTIVE_RE5.test(describedText)) continue;
5698
5779
  const tagNames = new Set(tags.map((tag) => tag.name));
5699
5780
  if ([...tagNames].some((name) => !MODELLED_TAGS.has(name))) continue;
5700
5781
  if (isProtected(describedText)) continue;
@@ -7656,10 +7737,10 @@ var STOPWORDS3 = /* @__PURE__ */ new Set([
7656
7737
  "we",
7657
7738
  "with"
7658
7739
  ]);
7659
- var DIRECTIVE_RE5 = /^\s*(?:eslint\b|eslint-|sarj-noqa\b|@ts-|prettier|biome-|c8\b|v8\b|istanbul\b|todo\b|fixme\b|hack\b|xxx\b)/i;
7740
+ var DIRECTIVE_RE6 = /^\s*(?:eslint\b|eslint-|sarj-noqa\b|@ts-|prettier|biome-|c8\b|v8\b|istanbul\b|todo\b|fixme\b|hack\b|xxx\b)/i;
7660
7741
  var UNIT_NAME_SUFFIX_RE = /(?:_(?:NS|US|MS|S|SEC|SECS|SECOND|SECONDS|MIN|MINS|MINUTE|MINUTES|HOUR|HOURS|DAY|DAYS|BYTE|BYTES|KB|MB|GB|HZ|KHZ|MHZ|PX)|(?:Ns|Us|Ms|Sec|Secs|Second|Seconds|Min|Mins|Minute|Minutes|Hour|Hours|Day|Days|Byte|Bytes|Kb|Mb|Gb|Hz|Khz|Mhz|Px))$/u;
7661
7742
  function narratesValue(body2, code) {
7662
- if (body2.length === 0 || DIRECTIVE_RE5.test(body2) || hasExternalReference(body2)) return false;
7743
+ if (body2.length === 0 || DIRECTIVE_RE6.test(body2) || hasExternalReference(body2)) return false;
7663
7744
  const codeNumbers = numbersIn(code);
7664
7745
  if (codeNumbers.size === 0) return false;
7665
7746
  const words2 = (body2.match(WORD_RE3) ?? []).map((word) => word.toLowerCase());
@@ -14787,7 +14868,7 @@ var stepdown_default = createRule({
14787
14868
 
14788
14869
  // src/rules/source-coupled-test.ts
14789
14870
  var import_utils70 = require("@typescript-eslint/utils");
14790
- var SOURCE_SUFFIX_RE = /\.(?:bash|hcl|sh|tf|tfvars|ya?ml|py|[cm]?[jt]s)$/iu;
14871
+ var GENERAL_SOURCE_SUFFIX_RE = /\.(?:bash|sh|ya?ml|py|[cm]?[jt]s)$/iu;
14791
14872
  var FS_MODULES = /* @__PURE__ */ new Set(["fs", "node:fs", "fs/promises", "node:fs/promises"]);
14792
14873
  var FS_READERS = /* @__PURE__ */ new Set(["readFile", "readFileSync"]);
14793
14874
  var TEXT_TRANSFORMS = /* @__PURE__ */ new Set([
@@ -14801,7 +14882,8 @@ var TEXT_TRANSFORMS = /* @__PURE__ */ new Set([
14801
14882
  "trimEnd",
14802
14883
  "trimStart",
14803
14884
  "replace",
14804
- "replaceAll"
14885
+ "replaceAll",
14886
+ "split"
14805
14887
  ]);
14806
14888
  var TEXT_PREDICATES = /* @__PURE__ */ new Set(["endsWith", "includes", "indexOf", "lastIndexOf", "match", "matchAll", "search", "startsWith"]);
14807
14889
  var REGEXP_PREDICATES = /* @__PURE__ */ new Set(["exec", "test"]);
@@ -14826,7 +14908,7 @@ var ASSERT_MATCHERS = /* @__PURE__ */ new Set(["deepEqual", "doesNotMatch", "equ
14826
14908
  var sourceCoupledTestDocumentation = {
14827
14909
  summary: "Disallow raw repository source text as a test oracle; parse or execute the artifact instead.",
14828
14910
  rationale: "Substring and regex checks can pass on comments or unreachable configuration and fail after behavior-preserving formatting changes.",
14829
- remediation: "Parse the artifact, execute its validator, or assert on Terraform plan JSON or another runtime contract.",
14911
+ remediation: "Parse the artifact, execute its validator, or assert on another runtime contract.",
14830
14912
  category: "testing",
14831
14913
  limitations: [
14832
14914
  "The rule follows lexical aliases, source-path collections, awaited reads, and common text operations; interprocedural flows remain unreported.",
@@ -14843,10 +14925,10 @@ var sourceCoupledTestDocumentation = {
14843
14925
  public: true
14844
14926
  },
14845
14927
  {
14846
- id: "terraform-substring-contract",
14847
- title: "Do not prove Terraform behavior with a regex",
14928
+ id: "workflow-substring-contract",
14929
+ title: "Do not prove workflow behavior with a regex",
14848
14930
  outcome: "match",
14849
- files: [{ path: "src/policy.test.ts", source: "import { readFileSync } from 'node:fs'; test('policy', () => { const source = readFileSync('main.tf', 'utf8'); expect(source).toMatch(/prevent_destroy/); });" }],
14931
+ files: [{ path: "src/policy.test.ts", source: "import { readFileSync } from 'node:fs'; test('policy', () => { const source = readFileSync('workflow.yml', 'utf8'); expect(source).toMatch(/permissions/); });" }],
14850
14932
  focusPath: "src/policy.test.ts",
14851
14933
  expectedCount: 1,
14852
14934
  public: true
@@ -14881,191 +14963,237 @@ function requireSource(node) {
14881
14963
  function newScope() {
14882
14964
  return { collections: /* @__PURE__ */ new Set(), declared: /* @__PURE__ */ new Set(), fsObjects: /* @__PURE__ */ new Set(), fsReaders: /* @__PURE__ */ new Set(), paths: /* @__PURE__ */ new Set(), rawOrigins: /* @__PURE__ */ new Map() };
14883
14965
  }
14884
- var source_coupled_test_default = createRule({
14885
- name: "source-coupled-test",
14886
- documentation: sourceCoupledTestDocumentation,
14887
- meta: {
14888
- type: "suggestion",
14889
- docs: { description: sourceCoupledTestDocumentation.summary },
14890
- schema: [],
14891
- messages: { rawSourceOracle: "Raw repository source text is the oracle. Parse or execute the artifact so comments, formatting, and unreachable blocks cannot satisfy the contract." }
14892
- },
14893
- defaultOptions: [],
14894
- create(context) {
14895
- if (!isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
14896
- const scopes = [newScope()];
14897
- const reportedOrigins = /* @__PURE__ */ new Set();
14898
- const currentScope = () => scopes.at(-1) ?? scopes[0];
14899
- const visible = (kind, name) => {
14900
- for (let index = scopes.length - 1; index >= 0; index--) {
14901
- const scope = scopes[index];
14902
- if (scope.declared.has(name)) return scope[kind].has(name);
14903
- }
14904
- return false;
14905
- };
14906
- const visibleRawOrigins = (name) => {
14907
- for (let index = scopes.length - 1; index >= 0; index--) {
14908
- const scope = scopes[index];
14909
- if (scope.declared.has(name)) return scope.rawOrigins.get(name) ?? /* @__PURE__ */ new Set();
14910
- }
14911
- return /* @__PURE__ */ new Set();
14912
- };
14913
- const sourcePath = (node) => {
14914
- const current = unwrap5(node);
14915
- const value = stringValue(current);
14916
- if (value !== null) return SOURCE_SUFFIX_RE.test(value);
14917
- if (current.type === import_utils70.AST_NODE_TYPES.Identifier) return visible("paths", current.name);
14918
- if (current.type === import_utils70.AST_NODE_TYPES.BinaryExpression && current.operator === "+") {
14919
- return sourcePath(current.left) || sourcePath(current.right);
14920
- }
14921
- if (current.type === import_utils70.AST_NODE_TYPES.TemplateLiteral) return current.expressions.some(sourcePath);
14922
- if (current.type === import_utils70.AST_NODE_TYPES.CallExpression || current.type === import_utils70.AST_NODE_TYPES.NewExpression) {
14923
- return current.arguments.some((argument) => argument.type !== import_utils70.AST_NODE_TYPES.SpreadElement && sourcePath(argument));
14924
- }
14925
- if (current.type === import_utils70.AST_NODE_TYPES.MemberExpression) return sourcePath(current.object);
14926
- return false;
14927
- };
14928
- const rawRead = (node) => {
14929
- const current = unwrap5(node);
14930
- if (current.type !== import_utils70.AST_NODE_TYPES.CallExpression || current.arguments.length === 0) return false;
14931
- const callee = unwrap5(current.callee);
14932
- if (callee.type === import_utils70.AST_NODE_TYPES.Identifier) {
14933
- return visible("fsReaders", callee.name) && sourcePath(current.arguments[0]);
14934
- }
14935
- if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return false;
14936
- const name = staticMemberName5(callee);
14937
- const object = unwrap5(callee.object);
14938
- return name !== null && FS_READERS.has(name) && object.type === import_utils70.AST_NODE_TYPES.Identifier && visible("fsObjects", object.name) && sourcePath(current.arguments[0]);
14939
- };
14940
- const rawOrigins = (node) => {
14941
- const current = unwrap5(node);
14942
- if (current.type === import_utils70.AST_NODE_TYPES.Identifier) return visibleRawOrigins(current.name);
14943
- if (rawRead(current)) return /* @__PURE__ */ new Set([`${current.range[0]}:${current.range[1]}`]);
14944
- if (current.type === import_utils70.AST_NODE_TYPES.BinaryExpression && current.operator === "+") return /* @__PURE__ */ new Set([...rawOrigins(current.left), ...rawOrigins(current.right)]);
14945
- if (current.type !== import_utils70.AST_NODE_TYPES.CallExpression) return /* @__PURE__ */ new Set();
14946
- const callee = unwrap5(current.callee);
14947
- if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return /* @__PURE__ */ new Set();
14948
- const name = staticMemberName5(callee);
14949
- return name !== null && TEXT_TRANSFORMS.has(name) ? rawOrigins(callee.object) : /* @__PURE__ */ new Set();
14950
- };
14951
- const evidenceOrigins = (node) => {
14952
- const current = unwrap5(node);
14953
- const direct = rawOrigins(current);
14954
- if (direct.size > 0) return direct;
14955
- if (current.type === import_utils70.AST_NODE_TYPES.BinaryExpression || current.type === import_utils70.AST_NODE_TYPES.LogicalExpression) return /* @__PURE__ */ new Set([...evidenceOrigins(current.left), ...evidenceOrigins(current.right)]);
14956
- if (current.type === import_utils70.AST_NODE_TYPES.UnaryExpression) return evidenceOrigins(current.argument);
14957
- if (current.type !== import_utils70.AST_NODE_TYPES.CallExpression) return /* @__PURE__ */ new Set();
14958
- const callee = unwrap5(current.callee);
14959
- if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return /* @__PURE__ */ new Set();
14960
- const name = staticMemberName5(callee);
14961
- if (name !== null && TEXT_PREDICATES.has(name)) return rawOrigins(callee.object);
14962
- if (name !== null && REGEXP_PREDICATES.has(name)) return new Set(current.arguments.flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...rawOrigins(argument)]));
14963
- return /* @__PURE__ */ new Set();
14964
- };
14965
- const rawAssertionOrigins = (node) => {
14966
- const callee = unwrap5(node.callee);
14967
- if (callee.type === import_utils70.AST_NODE_TYPES.Identifier && callee.name === "assert") {
14968
- return new Set(node.arguments.flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
14969
- }
14970
- if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return /* @__PURE__ */ new Set();
14971
- const matcher = staticMemberName5(callee);
14972
- if (matcher === null) return /* @__PURE__ */ new Set();
14973
- let receiver = unwrap5(callee.object);
14974
- while (receiver.type === import_utils70.AST_NODE_TYPES.MemberExpression && EXPECT_MODIFIERS.has(staticMemberName5(receiver) ?? "")) receiver = unwrap5(receiver.object);
14975
- if (receiver.type === import_utils70.AST_NODE_TYPES.CallExpression && receiver.callee.type === import_utils70.AST_NODE_TYPES.Identifier && receiver.callee.name === "expect") {
14976
- if (!EXPECT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
14977
- return new Set([...receiver.arguments, ...node.arguments].flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
14978
- }
14979
- if (receiver.type !== import_utils70.AST_NODE_TYPES.Identifier || receiver.name !== "assert" || !ASSERT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
14980
- return new Set(node.arguments.flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
14981
- };
14982
- const declare = (name, state) => {
14983
- const scope = currentScope();
14984
- scope.declared.add(name);
14985
- scope.collections.delete(name);
14986
- scope.fsObjects.delete(name);
14987
- scope.fsReaders.delete(name);
14988
- scope.paths.delete(name);
14989
- scope.rawOrigins.delete(name);
14990
- if (state.collection === true) scope.collections.add(name);
14991
- if (state.fsObject === true) scope.fsObjects.add(name);
14992
- if (state.fsReader === true) scope.fsReaders.add(name);
14993
- if (state.path === true) scope.paths.add(name);
14994
- if (state.rawOrigins !== void 0 && state.rawOrigins.size > 0) {
14995
- scope.rawOrigins.set(name, state.rawOrigins);
14996
- }
14997
- };
14998
- const sourceCollection = (node) => {
14999
- const current = unwrap5(node);
15000
- return current.type === import_utils70.AST_NODE_TYPES.ArrayExpression && current.elements.length > 0 && current.elements.every((element) => element !== null && element.type !== import_utils70.AST_NODE_TYPES.SpreadElement && sourcePath(element));
15001
- };
15002
- const declaredNames2 = (node) => {
15003
- const current = unwrap5(node);
15004
- if (current.type === import_utils70.AST_NODE_TYPES.Identifier) return [current.name];
15005
- if (current.type === import_utils70.AST_NODE_TYPES.AssignmentPattern) return declaredNames2(current.left);
15006
- if (current.type === import_utils70.AST_NODE_TYPES.RestElement) return declaredNames2(current.argument);
15007
- if (current.type === import_utils70.AST_NODE_TYPES.ArrayPattern) return current.elements.flatMap((element) => element === null ? [] : declaredNames2(element));
15008
- if (current.type === import_utils70.AST_NODE_TYPES.ObjectPattern) return current.properties.flatMap((property) => property.type === import_utils70.AST_NODE_TYPES.RestElement ? declaredNames2(property.argument) : declaredNames2(property.value));
15009
- return [];
15010
- };
15011
- const enterFunction = (node) => {
15012
- scopes.push(newScope());
15013
- for (const parameter of node.params) for (const name of declaredNames2(parameter)) declare(name, {});
15014
- };
15015
- const exitFunction = () => {
15016
- scopes.pop();
15017
- };
15018
- return {
15019
- ImportDeclaration(node) {
15020
- const source = importSource(node);
15021
- if (source === null || !FS_MODULES.has(source)) return;
15022
- for (const specifier of node.specifiers) {
15023
- if (specifier.type === import_utils70.AST_NODE_TYPES.ImportSpecifier) {
15024
- const imported = specifier.imported.type === import_utils70.AST_NODE_TYPES.Identifier ? specifier.imported.name : String(specifier.imported.value);
15025
- if (FS_READERS.has(imported)) declare(specifier.local.name, { fsReader: true });
15026
- } else {
15027
- declare(specifier.local.name, { fsObject: true });
15028
- }
14966
+ function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
14967
+ return createRule({
14968
+ name,
14969
+ documentation,
14970
+ meta: {
14971
+ type: "suggestion",
14972
+ docs: { description: documentation.summary },
14973
+ schema: [],
14974
+ messages: { rawSourceOracle: "Raw repository source text is the oracle. Parse or execute the artifact so comments, formatting, and unreachable blocks cannot satisfy the contract." }
14975
+ },
14976
+ defaultOptions: [],
14977
+ create(context) {
14978
+ if (!isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
14979
+ const scopes = [newScope()];
14980
+ const reportedOrigins = /* @__PURE__ */ new Set();
14981
+ const currentScope = () => scopes.at(-1) ?? scopes[0];
14982
+ const visible = (kind, name2) => {
14983
+ for (let index = scopes.length - 1; index >= 0; index--) {
14984
+ const scope = scopes[index];
14985
+ if (scope.declared.has(name2)) return scope[kind].has(name2);
15029
14986
  }
15030
- },
15031
- ":function": enterFunction,
15032
- ":function:exit": exitFunction,
15033
- VariableDeclarator(node) {
15034
- if (node.init === null) return;
15035
- const required = requireSource(node.init);
15036
- if (required !== null && FS_MODULES.has(required) && node.id.type === import_utils70.AST_NODE_TYPES.Identifier) {
15037
- declare(node.id.name, { fsObject: true });
15038
- return;
14987
+ return false;
14988
+ };
14989
+ const visibleRawOrigins = (name2) => {
14990
+ for (let index = scopes.length - 1; index >= 0; index--) {
14991
+ const scope = scopes[index];
14992
+ if (scope.declared.has(name2)) return scope.rawOrigins.get(name2) ?? /* @__PURE__ */ new Set();
14993
+ }
14994
+ return /* @__PURE__ */ new Set();
14995
+ };
14996
+ const sourcePath = (node) => {
14997
+ const current = unwrap5(node);
14998
+ const value = stringValue(current);
14999
+ if (value !== null) return sourceSuffixRe.test(value);
15000
+ if (current.type === import_utils70.AST_NODE_TYPES.Identifier) return visible("paths", current.name);
15001
+ if (current.type === import_utils70.AST_NODE_TYPES.BinaryExpression && current.operator === "+") {
15002
+ return sourcePath(current.left) || sourcePath(current.right);
15003
+ }
15004
+ if (current.type === import_utils70.AST_NODE_TYPES.TemplateLiteral) return current.expressions.some(sourcePath);
15005
+ if (current.type === import_utils70.AST_NODE_TYPES.CallExpression || current.type === import_utils70.AST_NODE_TYPES.NewExpression) {
15006
+ return current.arguments.some((argument) => argument.type !== import_utils70.AST_NODE_TYPES.SpreadElement && sourcePath(argument));
15007
+ }
15008
+ if (current.type === import_utils70.AST_NODE_TYPES.MemberExpression) return sourcePath(current.object);
15009
+ return false;
15010
+ };
15011
+ const rawRead = (node) => {
15012
+ const current = unwrap5(node);
15013
+ if (current.type !== import_utils70.AST_NODE_TYPES.CallExpression || current.arguments.length === 0) return false;
15014
+ const callee = unwrap5(current.callee);
15015
+ if (callee.type === import_utils70.AST_NODE_TYPES.Identifier) {
15016
+ return visible("fsReaders", callee.name) && sourcePath(current.arguments[0]);
15017
+ }
15018
+ if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return false;
15019
+ const name2 = staticMemberName5(callee);
15020
+ const object = unwrap5(callee.object);
15021
+ return name2 !== null && FS_READERS.has(name2) && object.type === import_utils70.AST_NODE_TYPES.Identifier && visible("fsObjects", object.name) && sourcePath(current.arguments[0]);
15022
+ };
15023
+ const rawOrigins = (node) => {
15024
+ const current = unwrap5(node);
15025
+ if (current.type === import_utils70.AST_NODE_TYPES.Identifier) return visibleRawOrigins(current.name);
15026
+ if (rawRead(current)) return /* @__PURE__ */ new Set([`${current.range[0]}:${current.range[1]}`]);
15027
+ if (current.type === import_utils70.AST_NODE_TYPES.BinaryExpression && current.operator === "+") return /* @__PURE__ */ new Set([...rawOrigins(current.left), ...rawOrigins(current.right)]);
15028
+ if (current.type === import_utils70.AST_NODE_TYPES.MemberExpression && staticMemberName5(current) === "length") return rawOrigins(current.object);
15029
+ if (current.type !== import_utils70.AST_NODE_TYPES.CallExpression) return /* @__PURE__ */ new Set();
15030
+ const callee = unwrap5(current.callee);
15031
+ if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return /* @__PURE__ */ new Set();
15032
+ const name2 = staticMemberName5(callee);
15033
+ return name2 !== null && TEXT_TRANSFORMS.has(name2) ? rawOrigins(callee.object) : /* @__PURE__ */ new Set();
15034
+ };
15035
+ const evidenceOrigins = (node) => {
15036
+ const current = unwrap5(node);
15037
+ const direct = rawOrigins(current);
15038
+ if (direct.size > 0) return direct;
15039
+ if (current.type === import_utils70.AST_NODE_TYPES.BinaryExpression || current.type === import_utils70.AST_NODE_TYPES.LogicalExpression) return /* @__PURE__ */ new Set([...evidenceOrigins(current.left), ...evidenceOrigins(current.right)]);
15040
+ if (current.type === import_utils70.AST_NODE_TYPES.UnaryExpression) return evidenceOrigins(current.argument);
15041
+ if (current.type !== import_utils70.AST_NODE_TYPES.CallExpression) return /* @__PURE__ */ new Set();
15042
+ const callee = unwrap5(current.callee);
15043
+ if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return /* @__PURE__ */ new Set();
15044
+ const name2 = staticMemberName5(callee);
15045
+ if (name2 !== null && TEXT_PREDICATES.has(name2)) return rawOrigins(callee.object);
15046
+ if (name2 !== null && REGEXP_PREDICATES.has(name2)) return new Set(current.arguments.flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...rawOrigins(argument)]));
15047
+ return /* @__PURE__ */ new Set();
15048
+ };
15049
+ const rawAssertionOrigins = (node) => {
15050
+ const callee = unwrap5(node.callee);
15051
+ if (callee.type === import_utils70.AST_NODE_TYPES.Identifier && callee.name === "assert") {
15052
+ return new Set(node.arguments.flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
15053
+ }
15054
+ if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return /* @__PURE__ */ new Set();
15055
+ const matcher = staticMemberName5(callee);
15056
+ if (matcher === null) return /* @__PURE__ */ new Set();
15057
+ let receiver = unwrap5(callee.object);
15058
+ while (receiver.type === import_utils70.AST_NODE_TYPES.MemberExpression && EXPECT_MODIFIERS.has(staticMemberName5(receiver) ?? "")) receiver = unwrap5(receiver.object);
15059
+ if (receiver.type === import_utils70.AST_NODE_TYPES.CallExpression && receiver.callee.type === import_utils70.AST_NODE_TYPES.Identifier && receiver.callee.name === "expect") {
15060
+ if (!EXPECT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
15061
+ return new Set([...receiver.arguments, ...node.arguments].flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
15062
+ }
15063
+ if (receiver.type !== import_utils70.AST_NODE_TYPES.Identifier || receiver.name !== "assert" || !ASSERT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
15064
+ return new Set(node.arguments.flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
15065
+ };
15066
+ const declare = (name2, state) => {
15067
+ const scope = currentScope();
15068
+ scope.declared.add(name2);
15069
+ scope.collections.delete(name2);
15070
+ scope.fsObjects.delete(name2);
15071
+ scope.fsReaders.delete(name2);
15072
+ scope.paths.delete(name2);
15073
+ scope.rawOrigins.delete(name2);
15074
+ if (state.collection === true) scope.collections.add(name2);
15075
+ if (state.fsObject === true) scope.fsObjects.add(name2);
15076
+ if (state.fsReader === true) scope.fsReaders.add(name2);
15077
+ if (state.path === true) scope.paths.add(name2);
15078
+ if (state.rawOrigins !== void 0 && state.rawOrigins.size > 0) {
15079
+ scope.rawOrigins.set(name2, state.rawOrigins);
15039
15080
  }
15040
- if (node.id.type === import_utils70.AST_NODE_TYPES.ObjectPattern && required !== null && FS_MODULES.has(required)) {
15041
- for (const property of node.id.properties) {
15042
- if (property.type !== import_utils70.AST_NODE_TYPES.Property || property.value.type !== import_utils70.AST_NODE_TYPES.Identifier) continue;
15043
- const key = property.key.type === import_utils70.AST_NODE_TYPES.Identifier ? property.key.name : property.key.type === import_utils70.AST_NODE_TYPES.Literal ? String(property.key.value) : "";
15044
- if (FS_READERS.has(key)) declare(property.value.name, { fsReader: true });
15081
+ };
15082
+ const sourceCollection = (node) => {
15083
+ const current = unwrap5(node);
15084
+ return current.type === import_utils70.AST_NODE_TYPES.ArrayExpression && current.elements.length > 0 && current.elements.every((element) => element !== null && element.type !== import_utils70.AST_NODE_TYPES.SpreadElement && sourcePath(element));
15085
+ };
15086
+ const declaredNames2 = (node) => {
15087
+ const current = unwrap5(node);
15088
+ if (current.type === import_utils70.AST_NODE_TYPES.Identifier) return [current.name];
15089
+ if (current.type === import_utils70.AST_NODE_TYPES.AssignmentPattern) return declaredNames2(current.left);
15090
+ if (current.type === import_utils70.AST_NODE_TYPES.RestElement) return declaredNames2(current.argument);
15091
+ if (current.type === import_utils70.AST_NODE_TYPES.ArrayPattern) return current.elements.flatMap((element) => element === null ? [] : declaredNames2(element));
15092
+ if (current.type === import_utils70.AST_NODE_TYPES.ObjectPattern) return current.properties.flatMap((property) => property.type === import_utils70.AST_NODE_TYPES.RestElement ? declaredNames2(property.argument) : declaredNames2(property.value));
15093
+ return [];
15094
+ };
15095
+ const enterFunction = (node) => {
15096
+ scopes.push(newScope());
15097
+ for (const parameter of node.params) for (const name2 of declaredNames2(parameter)) declare(name2, {});
15098
+ };
15099
+ const exitFunction = () => {
15100
+ scopes.pop();
15101
+ };
15102
+ return {
15103
+ ImportDeclaration(node) {
15104
+ const source = importSource(node);
15105
+ if (source === null || !FS_MODULES.has(source)) return;
15106
+ for (const specifier of node.specifiers) {
15107
+ if (specifier.type === import_utils70.AST_NODE_TYPES.ImportSpecifier) {
15108
+ const imported = specifier.imported.type === import_utils70.AST_NODE_TYPES.Identifier ? specifier.imported.name : String(specifier.imported.value);
15109
+ if (FS_READERS.has(imported)) declare(specifier.local.name, { fsReader: true });
15110
+ } else {
15111
+ declare(specifier.local.name, { fsObject: true });
15112
+ }
15045
15113
  }
15046
- return;
15114
+ },
15115
+ ":function": enterFunction,
15116
+ ":function:exit": exitFunction,
15117
+ VariableDeclarator(node) {
15118
+ if (node.init === null) return;
15119
+ const required = requireSource(node.init);
15120
+ if (required !== null && FS_MODULES.has(required) && node.id.type === import_utils70.AST_NODE_TYPES.Identifier) {
15121
+ declare(node.id.name, { fsObject: true });
15122
+ return;
15123
+ }
15124
+ if (node.id.type === import_utils70.AST_NODE_TYPES.ObjectPattern && required !== null && FS_MODULES.has(required)) {
15125
+ for (const property of node.id.properties) {
15126
+ if (property.type !== import_utils70.AST_NODE_TYPES.Property || property.value.type !== import_utils70.AST_NODE_TYPES.Identifier) continue;
15127
+ const key = property.key.type === import_utils70.AST_NODE_TYPES.Identifier ? property.key.name : property.key.type === import_utils70.AST_NODE_TYPES.Literal ? String(property.key.value) : "";
15128
+ if (FS_READERS.has(key)) declare(property.value.name, { fsReader: true });
15129
+ }
15130
+ return;
15131
+ }
15132
+ if (node.id.type !== import_utils70.AST_NODE_TYPES.Identifier) return;
15133
+ declare(node.id.name, { collection: sourceCollection(node.init), path: sourcePath(node.init), rawOrigins: rawOrigins(node.init) });
15134
+ },
15135
+ AssignmentExpression(node) {
15136
+ if (node.left.type === import_utils70.AST_NODE_TYPES.Identifier) declare(node.left.name, { path: sourcePath(node.right), rawOrigins: rawOrigins(node.right) });
15137
+ },
15138
+ ForOfStatement(node) {
15139
+ const right = unwrap5(node.right);
15140
+ const collection = right.type === import_utils70.AST_NODE_TYPES.Identifier && visible("collections", right.name);
15141
+ const left = node.left.type === import_utils70.AST_NODE_TYPES.VariableDeclaration ? node.left.declarations[0]?.id : node.left;
15142
+ if (collection && left?.type === import_utils70.AST_NODE_TYPES.Identifier) declare(left.name, { path: true });
15143
+ },
15144
+ CallExpression(node) {
15145
+ const origins = rawAssertionOrigins(node);
15146
+ if (origins.size === 0 || [...origins].every((origin) => reportedOrigins.has(origin))) return;
15147
+ for (const origin of origins) reportedOrigins.add(origin);
15148
+ context.report({ node, messageId: "rawSourceOracle" });
15047
15149
  }
15048
- if (node.id.type !== import_utils70.AST_NODE_TYPES.Identifier) return;
15049
- declare(node.id.name, { collection: sourceCollection(node.init), path: sourcePath(node.init), rawOrigins: rawOrigins(node.init) });
15050
- },
15051
- AssignmentExpression(node) {
15052
- if (node.left.type === import_utils70.AST_NODE_TYPES.Identifier) declare(node.left.name, { path: sourcePath(node.right), rawOrigins: rawOrigins(node.right) });
15053
- },
15054
- ForOfStatement(node) {
15055
- const right = unwrap5(node.right);
15056
- const collection = right.type === import_utils70.AST_NODE_TYPES.Identifier && visible("collections", right.name);
15057
- const left = node.left.type === import_utils70.AST_NODE_TYPES.VariableDeclaration ? node.left.declarations[0]?.id : node.left;
15058
- if (collection && left?.type === import_utils70.AST_NODE_TYPES.Identifier) declare(left.name, { path: true });
15059
- },
15060
- CallExpression(node) {
15061
- const origins = rawAssertionOrigins(node);
15062
- if (origins.size === 0 || [...origins].every((origin) => reportedOrigins.has(origin))) return;
15063
- for (const origin of origins) reportedOrigins.add(origin);
15064
- context.report({ node, messageId: "rawSourceOracle" });
15065
- }
15066
- };
15067
- }
15068
- });
15150
+ };
15151
+ }
15152
+ });
15153
+ }
15154
+ var source_coupled_test_default = createSourceCoupledRule(
15155
+ "source-coupled-test",
15156
+ sourceCoupledTestDocumentation,
15157
+ GENERAL_SOURCE_SUFFIX_RE
15158
+ );
15159
+
15160
+ // src/rules/iac-source-coupled-test.ts
15161
+ var IAC_SOURCE_SUFFIX_RE = /(?:\.tf\.json|\.tftest\.(?:hcl|json)|\.(?:hcl|tf|tfvars))$/iu;
15162
+ var iacSourceCoupledTestDocumentation = {
15163
+ summary: "Disallow raw IaC source text as a test oracle; inspect a rendered plan, provider state, or runtime behavior.",
15164
+ rationale: "Substring and regex checks can pass on comments, formatting, or unreachable Terraform configuration while clients fail silently.",
15165
+ remediation: "Parse rendered plan JSON, query the provider, or exercise the deployed runtime contract.",
15166
+ category: "testing",
15167
+ limitations: [
15168
+ "The rule follows lexical aliases, source-path collections, awaited reads, and common text operations; interprocedural flows remain unreported.",
15169
+ "The warning-stage rule remains suppressible for calibration; promotion may make the locked policy non-suppressible."
15170
+ ],
15171
+ examples: [
15172
+ {
15173
+ id: "rendered-plan-contract",
15174
+ title: "Assert on rendered plan behavior",
15175
+ outcome: "no-match",
15176
+ files: [{ path: "src/policy.test.ts", source: "import { readFileSync } from 'node:fs'; test('policy', () => { const plan = JSON.parse(readFileSync('plan.json', 'utf8')); expect(validate(plan)).toEqual([]); });" }],
15177
+ focusPath: "src/policy.test.ts",
15178
+ expectedCount: 0,
15179
+ public: true
15180
+ },
15181
+ {
15182
+ id: "terraform-substring-contract",
15183
+ title: "Do not prove Terraform behavior with a regex",
15184
+ outcome: "match",
15185
+ files: [{ path: "src/policy.test.ts", source: "import { readFileSync } from 'node:fs'; test('policy', () => { const source = readFileSync('main.tf', 'utf8'); expect(source).toMatch(/prevent_destroy/); });" }],
15186
+ focusPath: "src/policy.test.ts",
15187
+ expectedCount: 1,
15188
+ public: true
15189
+ }
15190
+ ]
15191
+ };
15192
+ var iac_source_coupled_test_default = createSourceCoupledRule(
15193
+ "iac-source-coupled-test",
15194
+ iacSourceCoupledTestDocumentation,
15195
+ IAC_SOURCE_SUFFIX_RE
15196
+ );
15069
15197
 
15070
15198
  // src/rules/zod-naming-convention.ts
15071
15199
  var import_utils71 = require("@typescript-eslint/utils");
@@ -15290,6 +15418,7 @@ var retiredRules = {
15290
15418
 
15291
15419
  // src/index.ts
15292
15420
  var rules = {
15421
+ "iac-source-coupled-test": iac_source_coupled_test_default,
15293
15422
  "duplicate-test-body": duplicate_test_body_default,
15294
15423
  "enforce-file-structure": enforce_file_structure_default,
15295
15424
  "no-client-side-data-fetching": no_client_side_data_fetching_default,
@@ -15305,6 +15434,7 @@ var rules = {
15305
15434
  "no-impossible-zod-literal-bounds": no_impossible_zod_literal_bounds_default,
15306
15435
  "no-log-only-catch": no_log_only_catch_default,
15307
15436
  "no-long-comment": no_long_comment_default,
15437
+ "no-vague-suppression-description": no_vague_suppression_description_default,
15308
15438
  "no-generic-single-export-module": no_generic_single_export_module_default,
15309
15439
  "no-offset-pagination": no_offset_pagination_default,
15310
15440
  "no-positional-tuple-return": no_positional_tuple_return_default,
@@ -15358,15 +15488,16 @@ var rules = {
15358
15488
  };
15359
15489
  var meta = {
15360
15490
  name: "@sarj/eslint-plugin",
15361
- version: "15.6.8"
15491
+ version: "15.7.0"
15362
15492
  };
15363
15493
  var applicationOnlyRules = [
15364
15494
  "no-restricted-library-load",
15365
15495
  "prefer-native-random-uuid",
15366
15496
  "prefer-shadcn-primitives"
15367
15497
  ];
15368
- var advisoryRules = ["source-coupled-test"];
15498
+ var advisoryRules = ["no-vague-suppression-description", "iac-source-coupled-test", "source-coupled-test"];
15369
15499
  var recommendedRules = {
15500
+ "@sarj/iac-source-coupled-test": "warn",
15370
15501
  "@sarj/duplicate-test-body": "error",
15371
15502
  "@sarj/enforce-file-structure": "error",
15372
15503
  "@sarj/no-client-side-data-fetching": "error",
@@ -15381,6 +15512,7 @@ var recommendedRules = {
15381
15512
  "@sarj/no-impossible-zod-literal-bounds": "error",
15382
15513
  "@sarj/no-log-only-catch": "error",
15383
15514
  "@sarj/no-long-comment": "error",
15515
+ "@sarj/no-vague-suppression-description": "warn",
15384
15516
  "@sarj/no-generic-single-export-module": "error",
15385
15517
  "@sarj/no-offset-pagination": "error",
15386
15518
  "@sarj/no-positional-tuple-return": "error",
@@ -15427,6 +15559,7 @@ var recommendedRules = {
15427
15559
  "@sarj/zod-naming-convention": "error"
15428
15560
  };
15429
15561
  var strictRules = {
15562
+ "@sarj/iac-source-coupled-test": "warn",
15430
15563
  "@sarj/duplicate-test-body": "error",
15431
15564
  "@sarj/enforce-file-structure": "error",
15432
15565
  "@sarj/no-client-side-data-fetching": "error",
@@ -15442,6 +15575,7 @@ var strictRules = {
15442
15575
  "@sarj/no-impossible-zod-literal-bounds": "error",
15443
15576
  "@sarj/no-log-only-catch": "error",
15444
15577
  "@sarj/no-long-comment": "error",
15578
+ "@sarj/no-vague-suppression-description": "warn",
15445
15579
  "@sarj/no-generic-single-export-module": "error",
15446
15580
  "@sarj/no-offset-pagination": "error",
15447
15581
  "@sarj/no-positional-tuple-return": "error",