@sarj/eslint-plugin 15.6.9 → 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 +239 -189
- package/dist/index.d.cts +6 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.js +239 -189
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -14868,7 +14868,7 @@ var stepdown_default = createRule({
|
|
|
14868
14868
|
|
|
14869
14869
|
// src/rules/source-coupled-test.ts
|
|
14870
14870
|
var import_utils70 = require("@typescript-eslint/utils");
|
|
14871
|
-
var
|
|
14871
|
+
var GENERAL_SOURCE_SUFFIX_RE = /\.(?:bash|sh|ya?ml|py|[cm]?[jt]s)$/iu;
|
|
14872
14872
|
var FS_MODULES = /* @__PURE__ */ new Set(["fs", "node:fs", "fs/promises", "node:fs/promises"]);
|
|
14873
14873
|
var FS_READERS = /* @__PURE__ */ new Set(["readFile", "readFileSync"]);
|
|
14874
14874
|
var TEXT_TRANSFORMS = /* @__PURE__ */ new Set([
|
|
@@ -14882,7 +14882,8 @@ var TEXT_TRANSFORMS = /* @__PURE__ */ new Set([
|
|
|
14882
14882
|
"trimEnd",
|
|
14883
14883
|
"trimStart",
|
|
14884
14884
|
"replace",
|
|
14885
|
-
"replaceAll"
|
|
14885
|
+
"replaceAll",
|
|
14886
|
+
"split"
|
|
14886
14887
|
]);
|
|
14887
14888
|
var TEXT_PREDICATES = /* @__PURE__ */ new Set(["endsWith", "includes", "indexOf", "lastIndexOf", "match", "matchAll", "search", "startsWith"]);
|
|
14888
14889
|
var REGEXP_PREDICATES = /* @__PURE__ */ new Set(["exec", "test"]);
|
|
@@ -14907,7 +14908,7 @@ var ASSERT_MATCHERS = /* @__PURE__ */ new Set(["deepEqual", "doesNotMatch", "equ
|
|
|
14907
14908
|
var sourceCoupledTestDocumentation = {
|
|
14908
14909
|
summary: "Disallow raw repository source text as a test oracle; parse or execute the artifact instead.",
|
|
14909
14910
|
rationale: "Substring and regex checks can pass on comments or unreachable configuration and fail after behavior-preserving formatting changes.",
|
|
14910
|
-
remediation: "Parse the artifact, execute its validator, or assert on
|
|
14911
|
+
remediation: "Parse the artifact, execute its validator, or assert on another runtime contract.",
|
|
14911
14912
|
category: "testing",
|
|
14912
14913
|
limitations: [
|
|
14913
14914
|
"The rule follows lexical aliases, source-path collections, awaited reads, and common text operations; interprocedural flows remain unreported.",
|
|
@@ -14924,10 +14925,10 @@ var sourceCoupledTestDocumentation = {
|
|
|
14924
14925
|
public: true
|
|
14925
14926
|
},
|
|
14926
14927
|
{
|
|
14927
|
-
id: "
|
|
14928
|
-
title: "Do not prove
|
|
14928
|
+
id: "workflow-substring-contract",
|
|
14929
|
+
title: "Do not prove workflow behavior with a regex",
|
|
14929
14930
|
outcome: "match",
|
|
14930
|
-
files: [{ path: "src/policy.test.ts", source: "import { readFileSync } from 'node:fs'; test('policy', () => { const source = readFileSync('
|
|
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/); });" }],
|
|
14931
14932
|
focusPath: "src/policy.test.ts",
|
|
14932
14933
|
expectedCount: 1,
|
|
14933
14934
|
public: true
|
|
@@ -14962,191 +14963,237 @@ function requireSource(node) {
|
|
|
14962
14963
|
function newScope() {
|
|
14963
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() };
|
|
14964
14965
|
}
|
|
14965
|
-
|
|
14966
|
-
|
|
14967
|
-
|
|
14968
|
-
|
|
14969
|
-
|
|
14970
|
-
|
|
14971
|
-
|
|
14972
|
-
|
|
14973
|
-
|
|
14974
|
-
|
|
14975
|
-
|
|
14976
|
-
|
|
14977
|
-
|
|
14978
|
-
|
|
14979
|
-
|
|
14980
|
-
|
|
14981
|
-
|
|
14982
|
-
|
|
14983
|
-
|
|
14984
|
-
|
|
14985
|
-
return false;
|
|
14986
|
-
};
|
|
14987
|
-
const visibleRawOrigins = (name) => {
|
|
14988
|
-
for (let index = scopes.length - 1; index >= 0; index--) {
|
|
14989
|
-
const scope = scopes[index];
|
|
14990
|
-
if (scope.declared.has(name)) return scope.rawOrigins.get(name) ?? /* @__PURE__ */ new Set();
|
|
14991
|
-
}
|
|
14992
|
-
return /* @__PURE__ */ new Set();
|
|
14993
|
-
};
|
|
14994
|
-
const sourcePath = (node) => {
|
|
14995
|
-
const current = unwrap5(node);
|
|
14996
|
-
const value = stringValue(current);
|
|
14997
|
-
if (value !== null) return SOURCE_SUFFIX_RE.test(value);
|
|
14998
|
-
if (current.type === import_utils70.AST_NODE_TYPES.Identifier) return visible("paths", current.name);
|
|
14999
|
-
if (current.type === import_utils70.AST_NODE_TYPES.BinaryExpression && current.operator === "+") {
|
|
15000
|
-
return sourcePath(current.left) || sourcePath(current.right);
|
|
15001
|
-
}
|
|
15002
|
-
if (current.type === import_utils70.AST_NODE_TYPES.TemplateLiteral) return current.expressions.some(sourcePath);
|
|
15003
|
-
if (current.type === import_utils70.AST_NODE_TYPES.CallExpression || current.type === import_utils70.AST_NODE_TYPES.NewExpression) {
|
|
15004
|
-
return current.arguments.some((argument) => argument.type !== import_utils70.AST_NODE_TYPES.SpreadElement && sourcePath(argument));
|
|
15005
|
-
}
|
|
15006
|
-
if (current.type === import_utils70.AST_NODE_TYPES.MemberExpression) return sourcePath(current.object);
|
|
15007
|
-
return false;
|
|
15008
|
-
};
|
|
15009
|
-
const rawRead = (node) => {
|
|
15010
|
-
const current = unwrap5(node);
|
|
15011
|
-
if (current.type !== import_utils70.AST_NODE_TYPES.CallExpression || current.arguments.length === 0) return false;
|
|
15012
|
-
const callee = unwrap5(current.callee);
|
|
15013
|
-
if (callee.type === import_utils70.AST_NODE_TYPES.Identifier) {
|
|
15014
|
-
return visible("fsReaders", callee.name) && sourcePath(current.arguments[0]);
|
|
15015
|
-
}
|
|
15016
|
-
if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return false;
|
|
15017
|
-
const name = staticMemberName5(callee);
|
|
15018
|
-
const object = unwrap5(callee.object);
|
|
15019
|
-
return name !== null && FS_READERS.has(name) && object.type === import_utils70.AST_NODE_TYPES.Identifier && visible("fsObjects", object.name) && sourcePath(current.arguments[0]);
|
|
15020
|
-
};
|
|
15021
|
-
const rawOrigins = (node) => {
|
|
15022
|
-
const current = unwrap5(node);
|
|
15023
|
-
if (current.type === import_utils70.AST_NODE_TYPES.Identifier) return visibleRawOrigins(current.name);
|
|
15024
|
-
if (rawRead(current)) return /* @__PURE__ */ new Set([`${current.range[0]}:${current.range[1]}`]);
|
|
15025
|
-
if (current.type === import_utils70.AST_NODE_TYPES.BinaryExpression && current.operator === "+") return /* @__PURE__ */ new Set([...rawOrigins(current.left), ...rawOrigins(current.right)]);
|
|
15026
|
-
if (current.type !== import_utils70.AST_NODE_TYPES.CallExpression) return /* @__PURE__ */ new Set();
|
|
15027
|
-
const callee = unwrap5(current.callee);
|
|
15028
|
-
if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return /* @__PURE__ */ new Set();
|
|
15029
|
-
const name = staticMemberName5(callee);
|
|
15030
|
-
return name !== null && TEXT_TRANSFORMS.has(name) ? rawOrigins(callee.object) : /* @__PURE__ */ new Set();
|
|
15031
|
-
};
|
|
15032
|
-
const evidenceOrigins = (node) => {
|
|
15033
|
-
const current = unwrap5(node);
|
|
15034
|
-
const direct = rawOrigins(current);
|
|
15035
|
-
if (direct.size > 0) return direct;
|
|
15036
|
-
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)]);
|
|
15037
|
-
if (current.type === import_utils70.AST_NODE_TYPES.UnaryExpression) return evidenceOrigins(current.argument);
|
|
15038
|
-
if (current.type !== import_utils70.AST_NODE_TYPES.CallExpression) return /* @__PURE__ */ new Set();
|
|
15039
|
-
const callee = unwrap5(current.callee);
|
|
15040
|
-
if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return /* @__PURE__ */ new Set();
|
|
15041
|
-
const name = staticMemberName5(callee);
|
|
15042
|
-
if (name !== null && TEXT_PREDICATES.has(name)) return rawOrigins(callee.object);
|
|
15043
|
-
if (name !== null && REGEXP_PREDICATES.has(name)) return new Set(current.arguments.flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...rawOrigins(argument)]));
|
|
15044
|
-
return /* @__PURE__ */ new Set();
|
|
15045
|
-
};
|
|
15046
|
-
const rawAssertionOrigins = (node) => {
|
|
15047
|
-
const callee = unwrap5(node.callee);
|
|
15048
|
-
if (callee.type === import_utils70.AST_NODE_TYPES.Identifier && callee.name === "assert") {
|
|
15049
|
-
return new Set(node.arguments.flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
|
|
15050
|
-
}
|
|
15051
|
-
if (callee.type !== import_utils70.AST_NODE_TYPES.MemberExpression) return /* @__PURE__ */ new Set();
|
|
15052
|
-
const matcher = staticMemberName5(callee);
|
|
15053
|
-
if (matcher === null) return /* @__PURE__ */ new Set();
|
|
15054
|
-
let receiver = unwrap5(callee.object);
|
|
15055
|
-
while (receiver.type === import_utils70.AST_NODE_TYPES.MemberExpression && EXPECT_MODIFIERS.has(staticMemberName5(receiver) ?? "")) receiver = unwrap5(receiver.object);
|
|
15056
|
-
if (receiver.type === import_utils70.AST_NODE_TYPES.CallExpression && receiver.callee.type === import_utils70.AST_NODE_TYPES.Identifier && receiver.callee.name === "expect") {
|
|
15057
|
-
if (!EXPECT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
|
|
15058
|
-
return new Set([...receiver.arguments, ...node.arguments].flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
|
|
15059
|
-
}
|
|
15060
|
-
if (receiver.type !== import_utils70.AST_NODE_TYPES.Identifier || receiver.name !== "assert" || !ASSERT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
|
|
15061
|
-
return new Set(node.arguments.flatMap((argument) => argument.type === import_utils70.AST_NODE_TYPES.SpreadElement ? [] : [...evidenceOrigins(argument)]));
|
|
15062
|
-
};
|
|
15063
|
-
const declare = (name, state) => {
|
|
15064
|
-
const scope = currentScope();
|
|
15065
|
-
scope.declared.add(name);
|
|
15066
|
-
scope.collections.delete(name);
|
|
15067
|
-
scope.fsObjects.delete(name);
|
|
15068
|
-
scope.fsReaders.delete(name);
|
|
15069
|
-
scope.paths.delete(name);
|
|
15070
|
-
scope.rawOrigins.delete(name);
|
|
15071
|
-
if (state.collection === true) scope.collections.add(name);
|
|
15072
|
-
if (state.fsObject === true) scope.fsObjects.add(name);
|
|
15073
|
-
if (state.fsReader === true) scope.fsReaders.add(name);
|
|
15074
|
-
if (state.path === true) scope.paths.add(name);
|
|
15075
|
-
if (state.rawOrigins !== void 0 && state.rawOrigins.size > 0) {
|
|
15076
|
-
scope.rawOrigins.set(name, state.rawOrigins);
|
|
15077
|
-
}
|
|
15078
|
-
};
|
|
15079
|
-
const sourceCollection = (node) => {
|
|
15080
|
-
const current = unwrap5(node);
|
|
15081
|
-
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));
|
|
15082
|
-
};
|
|
15083
|
-
const declaredNames2 = (node) => {
|
|
15084
|
-
const current = unwrap5(node);
|
|
15085
|
-
if (current.type === import_utils70.AST_NODE_TYPES.Identifier) return [current.name];
|
|
15086
|
-
if (current.type === import_utils70.AST_NODE_TYPES.AssignmentPattern) return declaredNames2(current.left);
|
|
15087
|
-
if (current.type === import_utils70.AST_NODE_TYPES.RestElement) return declaredNames2(current.argument);
|
|
15088
|
-
if (current.type === import_utils70.AST_NODE_TYPES.ArrayPattern) return current.elements.flatMap((element) => element === null ? [] : declaredNames2(element));
|
|
15089
|
-
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));
|
|
15090
|
-
return [];
|
|
15091
|
-
};
|
|
15092
|
-
const enterFunction = (node) => {
|
|
15093
|
-
scopes.push(newScope());
|
|
15094
|
-
for (const parameter of node.params) for (const name of declaredNames2(parameter)) declare(name, {});
|
|
15095
|
-
};
|
|
15096
|
-
const exitFunction = () => {
|
|
15097
|
-
scopes.pop();
|
|
15098
|
-
};
|
|
15099
|
-
return {
|
|
15100
|
-
ImportDeclaration(node) {
|
|
15101
|
-
const source = importSource(node);
|
|
15102
|
-
if (source === null || !FS_MODULES.has(source)) return;
|
|
15103
|
-
for (const specifier of node.specifiers) {
|
|
15104
|
-
if (specifier.type === import_utils70.AST_NODE_TYPES.ImportSpecifier) {
|
|
15105
|
-
const imported = specifier.imported.type === import_utils70.AST_NODE_TYPES.Identifier ? specifier.imported.name : String(specifier.imported.value);
|
|
15106
|
-
if (FS_READERS.has(imported)) declare(specifier.local.name, { fsReader: true });
|
|
15107
|
-
} else {
|
|
15108
|
-
declare(specifier.local.name, { fsObject: true });
|
|
15109
|
-
}
|
|
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);
|
|
15110
14986
|
}
|
|
15111
|
-
|
|
15112
|
-
|
|
15113
|
-
|
|
15114
|
-
|
|
15115
|
-
|
|
15116
|
-
|
|
15117
|
-
if (required !== null && FS_MODULES.has(required) && node.id.type === import_utils70.AST_NODE_TYPES.Identifier) {
|
|
15118
|
-
declare(node.id.name, { fsObject: true });
|
|
15119
|
-
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();
|
|
15120
14993
|
}
|
|
15121
|
-
|
|
15122
|
-
|
|
15123
|
-
|
|
15124
|
-
|
|
15125
|
-
|
|
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);
|
|
15080
|
+
}
|
|
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
|
+
}
|
|
15126
15113
|
}
|
|
15127
|
-
|
|
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" });
|
|
15128
15149
|
}
|
|
15129
|
-
|
|
15130
|
-
|
|
15131
|
-
|
|
15132
|
-
|
|
15133
|
-
|
|
15134
|
-
|
|
15135
|
-
|
|
15136
|
-
|
|
15137
|
-
|
|
15138
|
-
|
|
15139
|
-
|
|
15140
|
-
|
|
15141
|
-
|
|
15142
|
-
|
|
15143
|
-
|
|
15144
|
-
|
|
15145
|
-
|
|
15146
|
-
|
|
15147
|
-
|
|
15148
|
-
|
|
15149
|
-
|
|
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
|
+
);
|
|
15150
15197
|
|
|
15151
15198
|
// src/rules/zod-naming-convention.ts
|
|
15152
15199
|
var import_utils71 = require("@typescript-eslint/utils");
|
|
@@ -15371,6 +15418,7 @@ var retiredRules = {
|
|
|
15371
15418
|
|
|
15372
15419
|
// src/index.ts
|
|
15373
15420
|
var rules = {
|
|
15421
|
+
"iac-source-coupled-test": iac_source_coupled_test_default,
|
|
15374
15422
|
"duplicate-test-body": duplicate_test_body_default,
|
|
15375
15423
|
"enforce-file-structure": enforce_file_structure_default,
|
|
15376
15424
|
"no-client-side-data-fetching": no_client_side_data_fetching_default,
|
|
@@ -15440,15 +15488,16 @@ var rules = {
|
|
|
15440
15488
|
};
|
|
15441
15489
|
var meta = {
|
|
15442
15490
|
name: "@sarj/eslint-plugin",
|
|
15443
|
-
version: "15.
|
|
15491
|
+
version: "15.7.0"
|
|
15444
15492
|
};
|
|
15445
15493
|
var applicationOnlyRules = [
|
|
15446
15494
|
"no-restricted-library-load",
|
|
15447
15495
|
"prefer-native-random-uuid",
|
|
15448
15496
|
"prefer-shadcn-primitives"
|
|
15449
15497
|
];
|
|
15450
|
-
var advisoryRules = ["no-vague-suppression-description", "source-coupled-test"];
|
|
15498
|
+
var advisoryRules = ["no-vague-suppression-description", "iac-source-coupled-test", "source-coupled-test"];
|
|
15451
15499
|
var recommendedRules = {
|
|
15500
|
+
"@sarj/iac-source-coupled-test": "warn",
|
|
15452
15501
|
"@sarj/duplicate-test-body": "error",
|
|
15453
15502
|
"@sarj/enforce-file-structure": "error",
|
|
15454
15503
|
"@sarj/no-client-side-data-fetching": "error",
|
|
@@ -15510,6 +15559,7 @@ var recommendedRules = {
|
|
|
15510
15559
|
"@sarj/zod-naming-convention": "error"
|
|
15511
15560
|
};
|
|
15512
15561
|
var strictRules = {
|
|
15562
|
+
"@sarj/iac-source-coupled-test": "warn",
|
|
15513
15563
|
"@sarj/duplicate-test-body": "error",
|
|
15514
15564
|
"@sarj/enforce-file-structure": "error",
|
|
15515
15565
|
"@sarj/no-client-side-data-fetching": "error",
|
package/dist/index.d.cts
CHANGED
|
@@ -188,6 +188,7 @@ declare const renamedRules: {
|
|
|
188
188
|
};
|
|
189
189
|
|
|
190
190
|
declare const rules: {
|
|
191
|
+
readonly "iac-source-coupled-test": DocumentedRule<readonly [], "rawSourceOracle">;
|
|
191
192
|
readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
|
|
192
193
|
readonly "enforce-file-structure": DocumentedRule<readonly [], "importsFirst" | "useServerDirective">;
|
|
193
194
|
readonly "no-client-side-data-fetching": DocumentedRule<readonly [], "noClientFetch">;
|
|
@@ -283,8 +284,9 @@ declare const rules: {
|
|
|
283
284
|
/** Rules registered for application-profile configs but intentionally absent from general presets. */
|
|
284
285
|
declare const applicationOnlyRules: readonly ["no-restricted-library-load", "prefer-native-random-uuid", "prefer-shadcn-primitives"];
|
|
285
286
|
/** Calibrated rules that intentionally remain warnings until consumer-corpus precision is proven. */
|
|
286
|
-
declare const advisoryRules: readonly ["no-vague-suppression-description", "source-coupled-test"];
|
|
287
|
+
declare const advisoryRules: readonly ["no-vague-suppression-description", "iac-source-coupled-test", "source-coupled-test"];
|
|
287
288
|
declare const recommendedRules: {
|
|
289
|
+
readonly "@sarj/iac-source-coupled-test": "warn";
|
|
288
290
|
readonly "@sarj/duplicate-test-body": "error";
|
|
289
291
|
readonly "@sarj/enforce-file-structure": "error";
|
|
290
292
|
readonly "@sarj/no-client-side-data-fetching": "error";
|
|
@@ -350,6 +352,7 @@ declare const recommendedRules: {
|
|
|
350
352
|
readonly "@sarj/zod-naming-convention": "error";
|
|
351
353
|
};
|
|
352
354
|
declare const strictRules: {
|
|
355
|
+
readonly "@sarj/iac-source-coupled-test": "warn";
|
|
353
356
|
readonly "@sarj/duplicate-test-body": "error";
|
|
354
357
|
readonly "@sarj/enforce-file-structure": "error";
|
|
355
358
|
readonly "@sarj/no-client-side-data-fetching": "error";
|
|
@@ -426,9 +429,10 @@ type FlatPreset = {
|
|
|
426
429
|
declare const plugin: {
|
|
427
430
|
readonly meta: {
|
|
428
431
|
readonly name: "@sarj/eslint-plugin";
|
|
429
|
-
readonly version: "15.
|
|
432
|
+
readonly version: "15.7.0";
|
|
430
433
|
};
|
|
431
434
|
readonly rules: {
|
|
435
|
+
readonly "iac-source-coupled-test": DocumentedRule<readonly [], "rawSourceOracle">;
|
|
432
436
|
readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
|
|
433
437
|
readonly "enforce-file-structure": DocumentedRule<readonly [], "importsFirst" | "useServerDirective">;
|
|
434
438
|
readonly "no-client-side-data-fetching": DocumentedRule<readonly [], "noClientFetch">;
|
package/dist/index.d.ts
CHANGED
|
@@ -188,6 +188,7 @@ declare const renamedRules: {
|
|
|
188
188
|
};
|
|
189
189
|
|
|
190
190
|
declare const rules: {
|
|
191
|
+
readonly "iac-source-coupled-test": DocumentedRule<readonly [], "rawSourceOracle">;
|
|
191
192
|
readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
|
|
192
193
|
readonly "enforce-file-structure": DocumentedRule<readonly [], "importsFirst" | "useServerDirective">;
|
|
193
194
|
readonly "no-client-side-data-fetching": DocumentedRule<readonly [], "noClientFetch">;
|
|
@@ -283,8 +284,9 @@ declare const rules: {
|
|
|
283
284
|
/** Rules registered for application-profile configs but intentionally absent from general presets. */
|
|
284
285
|
declare const applicationOnlyRules: readonly ["no-restricted-library-load", "prefer-native-random-uuid", "prefer-shadcn-primitives"];
|
|
285
286
|
/** Calibrated rules that intentionally remain warnings until consumer-corpus precision is proven. */
|
|
286
|
-
declare const advisoryRules: readonly ["no-vague-suppression-description", "source-coupled-test"];
|
|
287
|
+
declare const advisoryRules: readonly ["no-vague-suppression-description", "iac-source-coupled-test", "source-coupled-test"];
|
|
287
288
|
declare const recommendedRules: {
|
|
289
|
+
readonly "@sarj/iac-source-coupled-test": "warn";
|
|
288
290
|
readonly "@sarj/duplicate-test-body": "error";
|
|
289
291
|
readonly "@sarj/enforce-file-structure": "error";
|
|
290
292
|
readonly "@sarj/no-client-side-data-fetching": "error";
|
|
@@ -350,6 +352,7 @@ declare const recommendedRules: {
|
|
|
350
352
|
readonly "@sarj/zod-naming-convention": "error";
|
|
351
353
|
};
|
|
352
354
|
declare const strictRules: {
|
|
355
|
+
readonly "@sarj/iac-source-coupled-test": "warn";
|
|
353
356
|
readonly "@sarj/duplicate-test-body": "error";
|
|
354
357
|
readonly "@sarj/enforce-file-structure": "error";
|
|
355
358
|
readonly "@sarj/no-client-side-data-fetching": "error";
|
|
@@ -426,9 +429,10 @@ type FlatPreset = {
|
|
|
426
429
|
declare const plugin: {
|
|
427
430
|
readonly meta: {
|
|
428
431
|
readonly name: "@sarj/eslint-plugin";
|
|
429
|
-
readonly version: "15.
|
|
432
|
+
readonly version: "15.7.0";
|
|
430
433
|
};
|
|
431
434
|
readonly rules: {
|
|
435
|
+
readonly "iac-source-coupled-test": DocumentedRule<readonly [], "rawSourceOracle">;
|
|
432
436
|
readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
|
|
433
437
|
readonly "enforce-file-structure": DocumentedRule<readonly [], "importsFirst" | "useServerDirective">;
|
|
434
438
|
readonly "no-client-side-data-fetching": DocumentedRule<readonly [], "noClientFetch">;
|
package/dist/index.js
CHANGED
|
@@ -14845,7 +14845,7 @@ var stepdown_default = createRule({
|
|
|
14845
14845
|
|
|
14846
14846
|
// src/rules/source-coupled-test.ts
|
|
14847
14847
|
import { AST_NODE_TYPES as AST_NODE_TYPES56 } from "@typescript-eslint/utils";
|
|
14848
|
-
var
|
|
14848
|
+
var GENERAL_SOURCE_SUFFIX_RE = /\.(?:bash|sh|ya?ml|py|[cm]?[jt]s)$/iu;
|
|
14849
14849
|
var FS_MODULES = /* @__PURE__ */ new Set(["fs", "node:fs", "fs/promises", "node:fs/promises"]);
|
|
14850
14850
|
var FS_READERS = /* @__PURE__ */ new Set(["readFile", "readFileSync"]);
|
|
14851
14851
|
var TEXT_TRANSFORMS = /* @__PURE__ */ new Set([
|
|
@@ -14859,7 +14859,8 @@ var TEXT_TRANSFORMS = /* @__PURE__ */ new Set([
|
|
|
14859
14859
|
"trimEnd",
|
|
14860
14860
|
"trimStart",
|
|
14861
14861
|
"replace",
|
|
14862
|
-
"replaceAll"
|
|
14862
|
+
"replaceAll",
|
|
14863
|
+
"split"
|
|
14863
14864
|
]);
|
|
14864
14865
|
var TEXT_PREDICATES = /* @__PURE__ */ new Set(["endsWith", "includes", "indexOf", "lastIndexOf", "match", "matchAll", "search", "startsWith"]);
|
|
14865
14866
|
var REGEXP_PREDICATES = /* @__PURE__ */ new Set(["exec", "test"]);
|
|
@@ -14884,7 +14885,7 @@ var ASSERT_MATCHERS = /* @__PURE__ */ new Set(["deepEqual", "doesNotMatch", "equ
|
|
|
14884
14885
|
var sourceCoupledTestDocumentation = {
|
|
14885
14886
|
summary: "Disallow raw repository source text as a test oracle; parse or execute the artifact instead.",
|
|
14886
14887
|
rationale: "Substring and regex checks can pass on comments or unreachable configuration and fail after behavior-preserving formatting changes.",
|
|
14887
|
-
remediation: "Parse the artifact, execute its validator, or assert on
|
|
14888
|
+
remediation: "Parse the artifact, execute its validator, or assert on another runtime contract.",
|
|
14888
14889
|
category: "testing",
|
|
14889
14890
|
limitations: [
|
|
14890
14891
|
"The rule follows lexical aliases, source-path collections, awaited reads, and common text operations; interprocedural flows remain unreported.",
|
|
@@ -14901,10 +14902,10 @@ var sourceCoupledTestDocumentation = {
|
|
|
14901
14902
|
public: true
|
|
14902
14903
|
},
|
|
14903
14904
|
{
|
|
14904
|
-
id: "
|
|
14905
|
-
title: "Do not prove
|
|
14905
|
+
id: "workflow-substring-contract",
|
|
14906
|
+
title: "Do not prove workflow behavior with a regex",
|
|
14906
14907
|
outcome: "match",
|
|
14907
|
-
files: [{ path: "src/policy.test.ts", source: "import { readFileSync } from 'node:fs'; test('policy', () => { const source = readFileSync('
|
|
14908
|
+
files: [{ path: "src/policy.test.ts", source: "import { readFileSync } from 'node:fs'; test('policy', () => { const source = readFileSync('workflow.yml', 'utf8'); expect(source).toMatch(/permissions/); });" }],
|
|
14908
14909
|
focusPath: "src/policy.test.ts",
|
|
14909
14910
|
expectedCount: 1,
|
|
14910
14911
|
public: true
|
|
@@ -14939,191 +14940,237 @@ function requireSource(node) {
|
|
|
14939
14940
|
function newScope() {
|
|
14940
14941
|
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() };
|
|
14941
14942
|
}
|
|
14942
|
-
|
|
14943
|
-
|
|
14944
|
-
|
|
14945
|
-
|
|
14946
|
-
|
|
14947
|
-
|
|
14948
|
-
|
|
14949
|
-
|
|
14950
|
-
|
|
14951
|
-
|
|
14952
|
-
|
|
14953
|
-
|
|
14954
|
-
|
|
14955
|
-
|
|
14956
|
-
|
|
14957
|
-
|
|
14958
|
-
|
|
14959
|
-
|
|
14960
|
-
|
|
14961
|
-
|
|
14962
|
-
return false;
|
|
14963
|
-
};
|
|
14964
|
-
const visibleRawOrigins = (name) => {
|
|
14965
|
-
for (let index = scopes.length - 1; index >= 0; index--) {
|
|
14966
|
-
const scope = scopes[index];
|
|
14967
|
-
if (scope.declared.has(name)) return scope.rawOrigins.get(name) ?? /* @__PURE__ */ new Set();
|
|
14968
|
-
}
|
|
14969
|
-
return /* @__PURE__ */ new Set();
|
|
14970
|
-
};
|
|
14971
|
-
const sourcePath = (node) => {
|
|
14972
|
-
const current = unwrap5(node);
|
|
14973
|
-
const value = stringValue(current);
|
|
14974
|
-
if (value !== null) return SOURCE_SUFFIX_RE.test(value);
|
|
14975
|
-
if (current.type === AST_NODE_TYPES56.Identifier) return visible("paths", current.name);
|
|
14976
|
-
if (current.type === AST_NODE_TYPES56.BinaryExpression && current.operator === "+") {
|
|
14977
|
-
return sourcePath(current.left) || sourcePath(current.right);
|
|
14978
|
-
}
|
|
14979
|
-
if (current.type === AST_NODE_TYPES56.TemplateLiteral) return current.expressions.some(sourcePath);
|
|
14980
|
-
if (current.type === AST_NODE_TYPES56.CallExpression || current.type === AST_NODE_TYPES56.NewExpression) {
|
|
14981
|
-
return current.arguments.some((argument) => argument.type !== AST_NODE_TYPES56.SpreadElement && sourcePath(argument));
|
|
14982
|
-
}
|
|
14983
|
-
if (current.type === AST_NODE_TYPES56.MemberExpression) return sourcePath(current.object);
|
|
14984
|
-
return false;
|
|
14985
|
-
};
|
|
14986
|
-
const rawRead = (node) => {
|
|
14987
|
-
const current = unwrap5(node);
|
|
14988
|
-
if (current.type !== AST_NODE_TYPES56.CallExpression || current.arguments.length === 0) return false;
|
|
14989
|
-
const callee = unwrap5(current.callee);
|
|
14990
|
-
if (callee.type === AST_NODE_TYPES56.Identifier) {
|
|
14991
|
-
return visible("fsReaders", callee.name) && sourcePath(current.arguments[0]);
|
|
14992
|
-
}
|
|
14993
|
-
if (callee.type !== AST_NODE_TYPES56.MemberExpression) return false;
|
|
14994
|
-
const name = staticMemberName5(callee);
|
|
14995
|
-
const object = unwrap5(callee.object);
|
|
14996
|
-
return name !== null && FS_READERS.has(name) && object.type === AST_NODE_TYPES56.Identifier && visible("fsObjects", object.name) && sourcePath(current.arguments[0]);
|
|
14997
|
-
};
|
|
14998
|
-
const rawOrigins = (node) => {
|
|
14999
|
-
const current = unwrap5(node);
|
|
15000
|
-
if (current.type === AST_NODE_TYPES56.Identifier) return visibleRawOrigins(current.name);
|
|
15001
|
-
if (rawRead(current)) return /* @__PURE__ */ new Set([`${current.range[0]}:${current.range[1]}`]);
|
|
15002
|
-
if (current.type === AST_NODE_TYPES56.BinaryExpression && current.operator === "+") return /* @__PURE__ */ new Set([...rawOrigins(current.left), ...rawOrigins(current.right)]);
|
|
15003
|
-
if (current.type !== AST_NODE_TYPES56.CallExpression) return /* @__PURE__ */ new Set();
|
|
15004
|
-
const callee = unwrap5(current.callee);
|
|
15005
|
-
if (callee.type !== AST_NODE_TYPES56.MemberExpression) return /* @__PURE__ */ new Set();
|
|
15006
|
-
const name = staticMemberName5(callee);
|
|
15007
|
-
return name !== null && TEXT_TRANSFORMS.has(name) ? rawOrigins(callee.object) : /* @__PURE__ */ new Set();
|
|
15008
|
-
};
|
|
15009
|
-
const evidenceOrigins = (node) => {
|
|
15010
|
-
const current = unwrap5(node);
|
|
15011
|
-
const direct = rawOrigins(current);
|
|
15012
|
-
if (direct.size > 0) return direct;
|
|
15013
|
-
if (current.type === AST_NODE_TYPES56.BinaryExpression || current.type === AST_NODE_TYPES56.LogicalExpression) return /* @__PURE__ */ new Set([...evidenceOrigins(current.left), ...evidenceOrigins(current.right)]);
|
|
15014
|
-
if (current.type === AST_NODE_TYPES56.UnaryExpression) return evidenceOrigins(current.argument);
|
|
15015
|
-
if (current.type !== AST_NODE_TYPES56.CallExpression) return /* @__PURE__ */ new Set();
|
|
15016
|
-
const callee = unwrap5(current.callee);
|
|
15017
|
-
if (callee.type !== AST_NODE_TYPES56.MemberExpression) return /* @__PURE__ */ new Set();
|
|
15018
|
-
const name = staticMemberName5(callee);
|
|
15019
|
-
if (name !== null && TEXT_PREDICATES.has(name)) return rawOrigins(callee.object);
|
|
15020
|
-
if (name !== null && REGEXP_PREDICATES.has(name)) return new Set(current.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES56.SpreadElement ? [] : [...rawOrigins(argument)]));
|
|
15021
|
-
return /* @__PURE__ */ new Set();
|
|
15022
|
-
};
|
|
15023
|
-
const rawAssertionOrigins = (node) => {
|
|
15024
|
-
const callee = unwrap5(node.callee);
|
|
15025
|
-
if (callee.type === AST_NODE_TYPES56.Identifier && callee.name === "assert") {
|
|
15026
|
-
return new Set(node.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES56.SpreadElement ? [] : [...evidenceOrigins(argument)]));
|
|
15027
|
-
}
|
|
15028
|
-
if (callee.type !== AST_NODE_TYPES56.MemberExpression) return /* @__PURE__ */ new Set();
|
|
15029
|
-
const matcher = staticMemberName5(callee);
|
|
15030
|
-
if (matcher === null) return /* @__PURE__ */ new Set();
|
|
15031
|
-
let receiver = unwrap5(callee.object);
|
|
15032
|
-
while (receiver.type === AST_NODE_TYPES56.MemberExpression && EXPECT_MODIFIERS.has(staticMemberName5(receiver) ?? "")) receiver = unwrap5(receiver.object);
|
|
15033
|
-
if (receiver.type === AST_NODE_TYPES56.CallExpression && receiver.callee.type === AST_NODE_TYPES56.Identifier && receiver.callee.name === "expect") {
|
|
15034
|
-
if (!EXPECT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
|
|
15035
|
-
return new Set([...receiver.arguments, ...node.arguments].flatMap((argument) => argument.type === AST_NODE_TYPES56.SpreadElement ? [] : [...evidenceOrigins(argument)]));
|
|
15036
|
-
}
|
|
15037
|
-
if (receiver.type !== AST_NODE_TYPES56.Identifier || receiver.name !== "assert" || !ASSERT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
|
|
15038
|
-
return new Set(node.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES56.SpreadElement ? [] : [...evidenceOrigins(argument)]));
|
|
15039
|
-
};
|
|
15040
|
-
const declare = (name, state) => {
|
|
15041
|
-
const scope = currentScope();
|
|
15042
|
-
scope.declared.add(name);
|
|
15043
|
-
scope.collections.delete(name);
|
|
15044
|
-
scope.fsObjects.delete(name);
|
|
15045
|
-
scope.fsReaders.delete(name);
|
|
15046
|
-
scope.paths.delete(name);
|
|
15047
|
-
scope.rawOrigins.delete(name);
|
|
15048
|
-
if (state.collection === true) scope.collections.add(name);
|
|
15049
|
-
if (state.fsObject === true) scope.fsObjects.add(name);
|
|
15050
|
-
if (state.fsReader === true) scope.fsReaders.add(name);
|
|
15051
|
-
if (state.path === true) scope.paths.add(name);
|
|
15052
|
-
if (state.rawOrigins !== void 0 && state.rawOrigins.size > 0) {
|
|
15053
|
-
scope.rawOrigins.set(name, state.rawOrigins);
|
|
15054
|
-
}
|
|
15055
|
-
};
|
|
15056
|
-
const sourceCollection = (node) => {
|
|
15057
|
-
const current = unwrap5(node);
|
|
15058
|
-
return current.type === AST_NODE_TYPES56.ArrayExpression && current.elements.length > 0 && current.elements.every((element) => element !== null && element.type !== AST_NODE_TYPES56.SpreadElement && sourcePath(element));
|
|
15059
|
-
};
|
|
15060
|
-
const declaredNames2 = (node) => {
|
|
15061
|
-
const current = unwrap5(node);
|
|
15062
|
-
if (current.type === AST_NODE_TYPES56.Identifier) return [current.name];
|
|
15063
|
-
if (current.type === AST_NODE_TYPES56.AssignmentPattern) return declaredNames2(current.left);
|
|
15064
|
-
if (current.type === AST_NODE_TYPES56.RestElement) return declaredNames2(current.argument);
|
|
15065
|
-
if (current.type === AST_NODE_TYPES56.ArrayPattern) return current.elements.flatMap((element) => element === null ? [] : declaredNames2(element));
|
|
15066
|
-
if (current.type === AST_NODE_TYPES56.ObjectPattern) return current.properties.flatMap((property) => property.type === AST_NODE_TYPES56.RestElement ? declaredNames2(property.argument) : declaredNames2(property.value));
|
|
15067
|
-
return [];
|
|
15068
|
-
};
|
|
15069
|
-
const enterFunction = (node) => {
|
|
15070
|
-
scopes.push(newScope());
|
|
15071
|
-
for (const parameter of node.params) for (const name of declaredNames2(parameter)) declare(name, {});
|
|
15072
|
-
};
|
|
15073
|
-
const exitFunction = () => {
|
|
15074
|
-
scopes.pop();
|
|
15075
|
-
};
|
|
15076
|
-
return {
|
|
15077
|
-
ImportDeclaration(node) {
|
|
15078
|
-
const source = importSource(node);
|
|
15079
|
-
if (source === null || !FS_MODULES.has(source)) return;
|
|
15080
|
-
for (const specifier of node.specifiers) {
|
|
15081
|
-
if (specifier.type === AST_NODE_TYPES56.ImportSpecifier) {
|
|
15082
|
-
const imported = specifier.imported.type === AST_NODE_TYPES56.Identifier ? specifier.imported.name : String(specifier.imported.value);
|
|
15083
|
-
if (FS_READERS.has(imported)) declare(specifier.local.name, { fsReader: true });
|
|
15084
|
-
} else {
|
|
15085
|
-
declare(specifier.local.name, { fsObject: true });
|
|
15086
|
-
}
|
|
14943
|
+
function createSourceCoupledRule(name, documentation, sourceSuffixRe) {
|
|
14944
|
+
return createRule({
|
|
14945
|
+
name,
|
|
14946
|
+
documentation,
|
|
14947
|
+
meta: {
|
|
14948
|
+
type: "suggestion",
|
|
14949
|
+
docs: { description: documentation.summary },
|
|
14950
|
+
schema: [],
|
|
14951
|
+
messages: { rawSourceOracle: "Raw repository source text is the oracle. Parse or execute the artifact so comments, formatting, and unreachable blocks cannot satisfy the contract." }
|
|
14952
|
+
},
|
|
14953
|
+
defaultOptions: [],
|
|
14954
|
+
create(context) {
|
|
14955
|
+
if (!isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
|
|
14956
|
+
const scopes = [newScope()];
|
|
14957
|
+
const reportedOrigins = /* @__PURE__ */ new Set();
|
|
14958
|
+
const currentScope = () => scopes.at(-1) ?? scopes[0];
|
|
14959
|
+
const visible = (kind, name2) => {
|
|
14960
|
+
for (let index = scopes.length - 1; index >= 0; index--) {
|
|
14961
|
+
const scope = scopes[index];
|
|
14962
|
+
if (scope.declared.has(name2)) return scope[kind].has(name2);
|
|
15087
14963
|
}
|
|
15088
|
-
|
|
15089
|
-
|
|
15090
|
-
|
|
15091
|
-
|
|
15092
|
-
|
|
15093
|
-
|
|
15094
|
-
if (required !== null && FS_MODULES.has(required) && node.id.type === AST_NODE_TYPES56.Identifier) {
|
|
15095
|
-
declare(node.id.name, { fsObject: true });
|
|
15096
|
-
return;
|
|
14964
|
+
return false;
|
|
14965
|
+
};
|
|
14966
|
+
const visibleRawOrigins = (name2) => {
|
|
14967
|
+
for (let index = scopes.length - 1; index >= 0; index--) {
|
|
14968
|
+
const scope = scopes[index];
|
|
14969
|
+
if (scope.declared.has(name2)) return scope.rawOrigins.get(name2) ?? /* @__PURE__ */ new Set();
|
|
15097
14970
|
}
|
|
15098
|
-
|
|
15099
|
-
|
|
15100
|
-
|
|
15101
|
-
|
|
15102
|
-
|
|
14971
|
+
return /* @__PURE__ */ new Set();
|
|
14972
|
+
};
|
|
14973
|
+
const sourcePath = (node) => {
|
|
14974
|
+
const current = unwrap5(node);
|
|
14975
|
+
const value = stringValue(current);
|
|
14976
|
+
if (value !== null) return sourceSuffixRe.test(value);
|
|
14977
|
+
if (current.type === AST_NODE_TYPES56.Identifier) return visible("paths", current.name);
|
|
14978
|
+
if (current.type === AST_NODE_TYPES56.BinaryExpression && current.operator === "+") {
|
|
14979
|
+
return sourcePath(current.left) || sourcePath(current.right);
|
|
14980
|
+
}
|
|
14981
|
+
if (current.type === AST_NODE_TYPES56.TemplateLiteral) return current.expressions.some(sourcePath);
|
|
14982
|
+
if (current.type === AST_NODE_TYPES56.CallExpression || current.type === AST_NODE_TYPES56.NewExpression) {
|
|
14983
|
+
return current.arguments.some((argument) => argument.type !== AST_NODE_TYPES56.SpreadElement && sourcePath(argument));
|
|
14984
|
+
}
|
|
14985
|
+
if (current.type === AST_NODE_TYPES56.MemberExpression) return sourcePath(current.object);
|
|
14986
|
+
return false;
|
|
14987
|
+
};
|
|
14988
|
+
const rawRead = (node) => {
|
|
14989
|
+
const current = unwrap5(node);
|
|
14990
|
+
if (current.type !== AST_NODE_TYPES56.CallExpression || current.arguments.length === 0) return false;
|
|
14991
|
+
const callee = unwrap5(current.callee);
|
|
14992
|
+
if (callee.type === AST_NODE_TYPES56.Identifier) {
|
|
14993
|
+
return visible("fsReaders", callee.name) && sourcePath(current.arguments[0]);
|
|
14994
|
+
}
|
|
14995
|
+
if (callee.type !== AST_NODE_TYPES56.MemberExpression) return false;
|
|
14996
|
+
const name2 = staticMemberName5(callee);
|
|
14997
|
+
const object = unwrap5(callee.object);
|
|
14998
|
+
return name2 !== null && FS_READERS.has(name2) && object.type === AST_NODE_TYPES56.Identifier && visible("fsObjects", object.name) && sourcePath(current.arguments[0]);
|
|
14999
|
+
};
|
|
15000
|
+
const rawOrigins = (node) => {
|
|
15001
|
+
const current = unwrap5(node);
|
|
15002
|
+
if (current.type === AST_NODE_TYPES56.Identifier) return visibleRawOrigins(current.name);
|
|
15003
|
+
if (rawRead(current)) return /* @__PURE__ */ new Set([`${current.range[0]}:${current.range[1]}`]);
|
|
15004
|
+
if (current.type === AST_NODE_TYPES56.BinaryExpression && current.operator === "+") return /* @__PURE__ */ new Set([...rawOrigins(current.left), ...rawOrigins(current.right)]);
|
|
15005
|
+
if (current.type === AST_NODE_TYPES56.MemberExpression && staticMemberName5(current) === "length") return rawOrigins(current.object);
|
|
15006
|
+
if (current.type !== AST_NODE_TYPES56.CallExpression) return /* @__PURE__ */ new Set();
|
|
15007
|
+
const callee = unwrap5(current.callee);
|
|
15008
|
+
if (callee.type !== AST_NODE_TYPES56.MemberExpression) return /* @__PURE__ */ new Set();
|
|
15009
|
+
const name2 = staticMemberName5(callee);
|
|
15010
|
+
return name2 !== null && TEXT_TRANSFORMS.has(name2) ? rawOrigins(callee.object) : /* @__PURE__ */ new Set();
|
|
15011
|
+
};
|
|
15012
|
+
const evidenceOrigins = (node) => {
|
|
15013
|
+
const current = unwrap5(node);
|
|
15014
|
+
const direct = rawOrigins(current);
|
|
15015
|
+
if (direct.size > 0) return direct;
|
|
15016
|
+
if (current.type === AST_NODE_TYPES56.BinaryExpression || current.type === AST_NODE_TYPES56.LogicalExpression) return /* @__PURE__ */ new Set([...evidenceOrigins(current.left), ...evidenceOrigins(current.right)]);
|
|
15017
|
+
if (current.type === AST_NODE_TYPES56.UnaryExpression) return evidenceOrigins(current.argument);
|
|
15018
|
+
if (current.type !== AST_NODE_TYPES56.CallExpression) return /* @__PURE__ */ new Set();
|
|
15019
|
+
const callee = unwrap5(current.callee);
|
|
15020
|
+
if (callee.type !== AST_NODE_TYPES56.MemberExpression) return /* @__PURE__ */ new Set();
|
|
15021
|
+
const name2 = staticMemberName5(callee);
|
|
15022
|
+
if (name2 !== null && TEXT_PREDICATES.has(name2)) return rawOrigins(callee.object);
|
|
15023
|
+
if (name2 !== null && REGEXP_PREDICATES.has(name2)) return new Set(current.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES56.SpreadElement ? [] : [...rawOrigins(argument)]));
|
|
15024
|
+
return /* @__PURE__ */ new Set();
|
|
15025
|
+
};
|
|
15026
|
+
const rawAssertionOrigins = (node) => {
|
|
15027
|
+
const callee = unwrap5(node.callee);
|
|
15028
|
+
if (callee.type === AST_NODE_TYPES56.Identifier && callee.name === "assert") {
|
|
15029
|
+
return new Set(node.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES56.SpreadElement ? [] : [...evidenceOrigins(argument)]));
|
|
15030
|
+
}
|
|
15031
|
+
if (callee.type !== AST_NODE_TYPES56.MemberExpression) return /* @__PURE__ */ new Set();
|
|
15032
|
+
const matcher = staticMemberName5(callee);
|
|
15033
|
+
if (matcher === null) return /* @__PURE__ */ new Set();
|
|
15034
|
+
let receiver = unwrap5(callee.object);
|
|
15035
|
+
while (receiver.type === AST_NODE_TYPES56.MemberExpression && EXPECT_MODIFIERS.has(staticMemberName5(receiver) ?? "")) receiver = unwrap5(receiver.object);
|
|
15036
|
+
if (receiver.type === AST_NODE_TYPES56.CallExpression && receiver.callee.type === AST_NODE_TYPES56.Identifier && receiver.callee.name === "expect") {
|
|
15037
|
+
if (!EXPECT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
|
|
15038
|
+
return new Set([...receiver.arguments, ...node.arguments].flatMap((argument) => argument.type === AST_NODE_TYPES56.SpreadElement ? [] : [...evidenceOrigins(argument)]));
|
|
15039
|
+
}
|
|
15040
|
+
if (receiver.type !== AST_NODE_TYPES56.Identifier || receiver.name !== "assert" || !ASSERT_MATCHERS.has(matcher)) return /* @__PURE__ */ new Set();
|
|
15041
|
+
return new Set(node.arguments.flatMap((argument) => argument.type === AST_NODE_TYPES56.SpreadElement ? [] : [...evidenceOrigins(argument)]));
|
|
15042
|
+
};
|
|
15043
|
+
const declare = (name2, state) => {
|
|
15044
|
+
const scope = currentScope();
|
|
15045
|
+
scope.declared.add(name2);
|
|
15046
|
+
scope.collections.delete(name2);
|
|
15047
|
+
scope.fsObjects.delete(name2);
|
|
15048
|
+
scope.fsReaders.delete(name2);
|
|
15049
|
+
scope.paths.delete(name2);
|
|
15050
|
+
scope.rawOrigins.delete(name2);
|
|
15051
|
+
if (state.collection === true) scope.collections.add(name2);
|
|
15052
|
+
if (state.fsObject === true) scope.fsObjects.add(name2);
|
|
15053
|
+
if (state.fsReader === true) scope.fsReaders.add(name2);
|
|
15054
|
+
if (state.path === true) scope.paths.add(name2);
|
|
15055
|
+
if (state.rawOrigins !== void 0 && state.rawOrigins.size > 0) {
|
|
15056
|
+
scope.rawOrigins.set(name2, state.rawOrigins);
|
|
15057
|
+
}
|
|
15058
|
+
};
|
|
15059
|
+
const sourceCollection = (node) => {
|
|
15060
|
+
const current = unwrap5(node);
|
|
15061
|
+
return current.type === AST_NODE_TYPES56.ArrayExpression && current.elements.length > 0 && current.elements.every((element) => element !== null && element.type !== AST_NODE_TYPES56.SpreadElement && sourcePath(element));
|
|
15062
|
+
};
|
|
15063
|
+
const declaredNames2 = (node) => {
|
|
15064
|
+
const current = unwrap5(node);
|
|
15065
|
+
if (current.type === AST_NODE_TYPES56.Identifier) return [current.name];
|
|
15066
|
+
if (current.type === AST_NODE_TYPES56.AssignmentPattern) return declaredNames2(current.left);
|
|
15067
|
+
if (current.type === AST_NODE_TYPES56.RestElement) return declaredNames2(current.argument);
|
|
15068
|
+
if (current.type === AST_NODE_TYPES56.ArrayPattern) return current.elements.flatMap((element) => element === null ? [] : declaredNames2(element));
|
|
15069
|
+
if (current.type === AST_NODE_TYPES56.ObjectPattern) return current.properties.flatMap((property) => property.type === AST_NODE_TYPES56.RestElement ? declaredNames2(property.argument) : declaredNames2(property.value));
|
|
15070
|
+
return [];
|
|
15071
|
+
};
|
|
15072
|
+
const enterFunction = (node) => {
|
|
15073
|
+
scopes.push(newScope());
|
|
15074
|
+
for (const parameter of node.params) for (const name2 of declaredNames2(parameter)) declare(name2, {});
|
|
15075
|
+
};
|
|
15076
|
+
const exitFunction = () => {
|
|
15077
|
+
scopes.pop();
|
|
15078
|
+
};
|
|
15079
|
+
return {
|
|
15080
|
+
ImportDeclaration(node) {
|
|
15081
|
+
const source = importSource(node);
|
|
15082
|
+
if (source === null || !FS_MODULES.has(source)) return;
|
|
15083
|
+
for (const specifier of node.specifiers) {
|
|
15084
|
+
if (specifier.type === AST_NODE_TYPES56.ImportSpecifier) {
|
|
15085
|
+
const imported = specifier.imported.type === AST_NODE_TYPES56.Identifier ? specifier.imported.name : String(specifier.imported.value);
|
|
15086
|
+
if (FS_READERS.has(imported)) declare(specifier.local.name, { fsReader: true });
|
|
15087
|
+
} else {
|
|
15088
|
+
declare(specifier.local.name, { fsObject: true });
|
|
15089
|
+
}
|
|
15103
15090
|
}
|
|
15104
|
-
|
|
15091
|
+
},
|
|
15092
|
+
":function": enterFunction,
|
|
15093
|
+
":function:exit": exitFunction,
|
|
15094
|
+
VariableDeclarator(node) {
|
|
15095
|
+
if (node.init === null) return;
|
|
15096
|
+
const required = requireSource(node.init);
|
|
15097
|
+
if (required !== null && FS_MODULES.has(required) && node.id.type === AST_NODE_TYPES56.Identifier) {
|
|
15098
|
+
declare(node.id.name, { fsObject: true });
|
|
15099
|
+
return;
|
|
15100
|
+
}
|
|
15101
|
+
if (node.id.type === AST_NODE_TYPES56.ObjectPattern && required !== null && FS_MODULES.has(required)) {
|
|
15102
|
+
for (const property of node.id.properties) {
|
|
15103
|
+
if (property.type !== AST_NODE_TYPES56.Property || property.value.type !== AST_NODE_TYPES56.Identifier) continue;
|
|
15104
|
+
const key = property.key.type === AST_NODE_TYPES56.Identifier ? property.key.name : property.key.type === AST_NODE_TYPES56.Literal ? String(property.key.value) : "";
|
|
15105
|
+
if (FS_READERS.has(key)) declare(property.value.name, { fsReader: true });
|
|
15106
|
+
}
|
|
15107
|
+
return;
|
|
15108
|
+
}
|
|
15109
|
+
if (node.id.type !== AST_NODE_TYPES56.Identifier) return;
|
|
15110
|
+
declare(node.id.name, { collection: sourceCollection(node.init), path: sourcePath(node.init), rawOrigins: rawOrigins(node.init) });
|
|
15111
|
+
},
|
|
15112
|
+
AssignmentExpression(node) {
|
|
15113
|
+
if (node.left.type === AST_NODE_TYPES56.Identifier) declare(node.left.name, { path: sourcePath(node.right), rawOrigins: rawOrigins(node.right) });
|
|
15114
|
+
},
|
|
15115
|
+
ForOfStatement(node) {
|
|
15116
|
+
const right = unwrap5(node.right);
|
|
15117
|
+
const collection = right.type === AST_NODE_TYPES56.Identifier && visible("collections", right.name);
|
|
15118
|
+
const left = node.left.type === AST_NODE_TYPES56.VariableDeclaration ? node.left.declarations[0]?.id : node.left;
|
|
15119
|
+
if (collection && left?.type === AST_NODE_TYPES56.Identifier) declare(left.name, { path: true });
|
|
15120
|
+
},
|
|
15121
|
+
CallExpression(node) {
|
|
15122
|
+
const origins = rawAssertionOrigins(node);
|
|
15123
|
+
if (origins.size === 0 || [...origins].every((origin) => reportedOrigins.has(origin))) return;
|
|
15124
|
+
for (const origin of origins) reportedOrigins.add(origin);
|
|
15125
|
+
context.report({ node, messageId: "rawSourceOracle" });
|
|
15105
15126
|
}
|
|
15106
|
-
|
|
15107
|
-
|
|
15108
|
-
|
|
15109
|
-
|
|
15110
|
-
|
|
15111
|
-
|
|
15112
|
-
|
|
15113
|
-
|
|
15114
|
-
|
|
15115
|
-
|
|
15116
|
-
|
|
15117
|
-
|
|
15118
|
-
|
|
15119
|
-
|
|
15120
|
-
|
|
15121
|
-
|
|
15122
|
-
|
|
15123
|
-
|
|
15124
|
-
|
|
15125
|
-
|
|
15126
|
-
|
|
15127
|
+
};
|
|
15128
|
+
}
|
|
15129
|
+
});
|
|
15130
|
+
}
|
|
15131
|
+
var source_coupled_test_default = createSourceCoupledRule(
|
|
15132
|
+
"source-coupled-test",
|
|
15133
|
+
sourceCoupledTestDocumentation,
|
|
15134
|
+
GENERAL_SOURCE_SUFFIX_RE
|
|
15135
|
+
);
|
|
15136
|
+
|
|
15137
|
+
// src/rules/iac-source-coupled-test.ts
|
|
15138
|
+
var IAC_SOURCE_SUFFIX_RE = /(?:\.tf\.json|\.tftest\.(?:hcl|json)|\.(?:hcl|tf|tfvars))$/iu;
|
|
15139
|
+
var iacSourceCoupledTestDocumentation = {
|
|
15140
|
+
summary: "Disallow raw IaC source text as a test oracle; inspect a rendered plan, provider state, or runtime behavior.",
|
|
15141
|
+
rationale: "Substring and regex checks can pass on comments, formatting, or unreachable Terraform configuration while clients fail silently.",
|
|
15142
|
+
remediation: "Parse rendered plan JSON, query the provider, or exercise the deployed runtime contract.",
|
|
15143
|
+
category: "testing",
|
|
15144
|
+
limitations: [
|
|
15145
|
+
"The rule follows lexical aliases, source-path collections, awaited reads, and common text operations; interprocedural flows remain unreported.",
|
|
15146
|
+
"The warning-stage rule remains suppressible for calibration; promotion may make the locked policy non-suppressible."
|
|
15147
|
+
],
|
|
15148
|
+
examples: [
|
|
15149
|
+
{
|
|
15150
|
+
id: "rendered-plan-contract",
|
|
15151
|
+
title: "Assert on rendered plan behavior",
|
|
15152
|
+
outcome: "no-match",
|
|
15153
|
+
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([]); });" }],
|
|
15154
|
+
focusPath: "src/policy.test.ts",
|
|
15155
|
+
expectedCount: 0,
|
|
15156
|
+
public: true
|
|
15157
|
+
},
|
|
15158
|
+
{
|
|
15159
|
+
id: "terraform-substring-contract",
|
|
15160
|
+
title: "Do not prove Terraform behavior with a regex",
|
|
15161
|
+
outcome: "match",
|
|
15162
|
+
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/); });" }],
|
|
15163
|
+
focusPath: "src/policy.test.ts",
|
|
15164
|
+
expectedCount: 1,
|
|
15165
|
+
public: true
|
|
15166
|
+
}
|
|
15167
|
+
]
|
|
15168
|
+
};
|
|
15169
|
+
var iac_source_coupled_test_default = createSourceCoupledRule(
|
|
15170
|
+
"iac-source-coupled-test",
|
|
15171
|
+
iacSourceCoupledTestDocumentation,
|
|
15172
|
+
IAC_SOURCE_SUFFIX_RE
|
|
15173
|
+
);
|
|
15127
15174
|
|
|
15128
15175
|
// src/rules/zod-naming-convention.ts
|
|
15129
15176
|
import {
|
|
@@ -15351,6 +15398,7 @@ var retiredRules = {
|
|
|
15351
15398
|
|
|
15352
15399
|
// src/index.ts
|
|
15353
15400
|
var rules = {
|
|
15401
|
+
"iac-source-coupled-test": iac_source_coupled_test_default,
|
|
15354
15402
|
"duplicate-test-body": duplicate_test_body_default,
|
|
15355
15403
|
"enforce-file-structure": enforce_file_structure_default,
|
|
15356
15404
|
"no-client-side-data-fetching": no_client_side_data_fetching_default,
|
|
@@ -15420,15 +15468,16 @@ var rules = {
|
|
|
15420
15468
|
};
|
|
15421
15469
|
var meta = {
|
|
15422
15470
|
name: "@sarj/eslint-plugin",
|
|
15423
|
-
version: "15.
|
|
15471
|
+
version: "15.7.0"
|
|
15424
15472
|
};
|
|
15425
15473
|
var applicationOnlyRules = [
|
|
15426
15474
|
"no-restricted-library-load",
|
|
15427
15475
|
"prefer-native-random-uuid",
|
|
15428
15476
|
"prefer-shadcn-primitives"
|
|
15429
15477
|
];
|
|
15430
|
-
var advisoryRules = ["no-vague-suppression-description", "source-coupled-test"];
|
|
15478
|
+
var advisoryRules = ["no-vague-suppression-description", "iac-source-coupled-test", "source-coupled-test"];
|
|
15431
15479
|
var recommendedRules = {
|
|
15480
|
+
"@sarj/iac-source-coupled-test": "warn",
|
|
15432
15481
|
"@sarj/duplicate-test-body": "error",
|
|
15433
15482
|
"@sarj/enforce-file-structure": "error",
|
|
15434
15483
|
"@sarj/no-client-side-data-fetching": "error",
|
|
@@ -15490,6 +15539,7 @@ var recommendedRules = {
|
|
|
15490
15539
|
"@sarj/zod-naming-convention": "error"
|
|
15491
15540
|
};
|
|
15492
15541
|
var strictRules = {
|
|
15542
|
+
"@sarj/iac-source-coupled-test": "warn",
|
|
15493
15543
|
"@sarj/duplicate-test-body": "error",
|
|
15494
15544
|
"@sarj/enforce-file-structure": "error",
|
|
15495
15545
|
"@sarj/no-client-side-data-fetching": "error",
|