eslint-plugin-rulvar 1.38.0 → 1.40.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.d.ts CHANGED
@@ -12,6 +12,28 @@ interface RulvarLintDiagnostic {
12
12
  }
13
13
  declare function toJsonDiagnostics(messages: readonly Linter.LintMessage[]): RulvarLintDiagnostic[];
14
14
  //#endregion
15
+ //#region src/dialect-scan.d.ts
16
+ /** Where a finding sits in the ORIGINAL source (line and column counted from 1). */
17
+ interface DialectFinding {
18
+ kind: "eval" | "function-constructor" | "constructor-access";
19
+ line: number;
20
+ column: number;
21
+ }
22
+ /**
23
+ * Structural scan for compileScript: every dynamic code generation form the
24
+ * dialect rejects, as findings positioned in the original source. Covers bare
25
+ * `eval`/`Function` calls and `new`, `globalThis.eval`/`globalThis.Function`,
26
+ * and every constructor reconstruction form the shared predicates recognize.
27
+ * Member access on other objects (`response.eval`, `parser.Function`) and a
28
+ * property NAMED constructor in an object LITERAL are not code generation and
29
+ * are left alone.
30
+ *
31
+ * Takes a parsed ESTree Program, typed `unknown` so a caller needs neither the
32
+ * estree types nor a specific parser in its own public surface; any ESTree
33
+ * compatible parser (espree in the lint pass, acorn in compileScript) works.
34
+ */
35
+ declare function scanDialect(program: unknown): DialectFinding[];
36
+ //#endregion
15
37
  //#region src/index.d.ts
16
38
  declare const rules: Record<string, Rule.RuleModule>;
17
39
  declare const plugin: ESLint.Plugin;
@@ -21,4 +43,4 @@ declare const plugin: ESLint.Plugin;
21
43
  */
22
44
  declare const workflowsConfig: Linter.Config;
23
45
  //#endregion
24
- export { type RulvarLintDiagnostic, plugin as default, rules, toJsonDiagnostics, workflowsConfig };
46
+ export { type DialectFinding, type RulvarLintDiagnostic, plugin as default, rules, scanDialect, toJsonDiagnostics, workflowsConfig };
package/dist/index.js CHANGED
@@ -151,15 +151,159 @@ const noProcessEnv = {
151
151
  }
152
152
  };
153
153
  //#endregion
154
+ //#region src/dialect-scan.ts
155
+ function positionOf(node) {
156
+ const start = node.loc?.start;
157
+ return {
158
+ line: start?.line ?? 1,
159
+ column: (start?.column ?? 0) + 1
160
+ };
161
+ }
162
+ /**
163
+ * Folds an expression that evaluates to a string to its constant value, or
164
+ * undefined when it is not statically constant. Handles string literals, templates whose
165
+ * every interpolation folds, and `+` concatenations of foldable operands.
166
+ * This is deliberately conservative: anything it cannot prove constant (a
167
+ * variable, a call, an index) folds to undefined and is left to the runtime.
168
+ */
169
+ function foldStaticString(node) {
170
+ if (node === null || node === void 0) return;
171
+ if (node.type === "Literal") return typeof node.value === "string" ? node.value : void 0;
172
+ if (node.type === "TemplateLiteral") {
173
+ let out = "";
174
+ for (let i = 0; i < node.quasis.length; i += 1) {
175
+ out += node.quasis[i].value.cooked ?? node.quasis[i].value.raw;
176
+ if (i < node.expressions.length) {
177
+ const piece = foldStaticString(node.expressions[i]);
178
+ if (piece === void 0) return;
179
+ out += piece;
180
+ }
181
+ }
182
+ return out;
183
+ }
184
+ if (node.type === "BinaryExpression" && node.operator === "+") {
185
+ const left = node.left.type === "PrivateIdentifier" ? void 0 : foldStaticString(node.left);
186
+ const right = foldStaticString(node.right);
187
+ return left === void 0 || right === void 0 ? void 0 : left + right;
188
+ }
189
+ }
190
+ /** `fn.constructor` or `fn["constructor"]` (any statically constant key). */
191
+ function isConstructorMemberExpression(node) {
192
+ if (!node.computed) return node.property.type === "Identifier" && node.property.name === "constructor";
193
+ return foldStaticString(node.property) === "constructor";
194
+ }
195
+ /** A destructuring property that binds the `constructor` slot of its source. */
196
+ function isConstructorPatternProperty(node) {
197
+ if (!node.computed) {
198
+ if (node.key.type === "Identifier") return node.key.name === "constructor";
199
+ if (node.key.type === "Literal") return node.key.value === "constructor";
200
+ return false;
201
+ }
202
+ return foldStaticString(node.key) === "constructor";
203
+ }
204
+ /** `Reflect.get(fn, "constructor")` with a statically constant key. */
205
+ function isReflectConstructorGet(node) {
206
+ const callee = node.callee;
207
+ if (callee.type !== "MemberExpression" || callee.computed || callee.object.type !== "Identifier" || callee.object.name !== "Reflect" || callee.property.type !== "Identifier" || callee.property.name !== "get") return false;
208
+ const keyArg = node.arguments[1];
209
+ return keyArg !== void 0 && keyArg.type !== "SpreadElement" && foldStaticString(keyArg) === "constructor";
210
+ }
211
+ /** A bare global `eval`/`Function` used as a call or `new` target. */
212
+ function bareCodegenCallee(callee) {
213
+ if (callee.type === "Identifier") {
214
+ if (callee.name === "eval") return "eval";
215
+ if (callee.name === "Function") return "function-constructor";
216
+ return null;
217
+ }
218
+ if (callee.type === "MemberExpression" && !callee.computed && callee.object.type === "Identifier" && callee.object.name === "globalThis" && callee.property.type === "Identifier") {
219
+ if (callee.property.name === "eval") return "eval";
220
+ if (callee.property.name === "Function") return "function-constructor";
221
+ }
222
+ return null;
223
+ }
224
+ const SKIP_KEYS = /* @__PURE__ */ new Set([
225
+ "loc",
226
+ "range",
227
+ "start",
228
+ "end",
229
+ "parent",
230
+ "comments",
231
+ "tokens",
232
+ "leadingComments",
233
+ "trailingComments"
234
+ ]);
235
+ function walk(root, visit) {
236
+ const stack = [root];
237
+ while (stack.length > 0) {
238
+ const current = stack.pop();
239
+ if (current === null || typeof current !== "object") continue;
240
+ if (Array.isArray(current)) {
241
+ for (const item of current) stack.push(item);
242
+ continue;
243
+ }
244
+ const node = current;
245
+ if (typeof node.type !== "string") continue;
246
+ visit(node);
247
+ for (const [key, value] of Object.entries(node)) if (!SKIP_KEYS.has(key)) stack.push(value);
248
+ }
249
+ }
250
+ /**
251
+ * Structural scan for compileScript: every dynamic code generation form the
252
+ * dialect rejects, as findings positioned in the original source. Covers bare
253
+ * `eval`/`Function` calls and `new`, `globalThis.eval`/`globalThis.Function`,
254
+ * and every constructor reconstruction form the shared predicates recognize.
255
+ * Member access on other objects (`response.eval`, `parser.Function`) and a
256
+ * property NAMED constructor in an object LITERAL are not code generation and
257
+ * are left alone.
258
+ *
259
+ * Takes a parsed ESTree Program, typed `unknown` so a caller needs neither the
260
+ * estree types nor a specific parser in its own public surface; any ESTree
261
+ * compatible parser (espree in the lint pass, acorn in compileScript) works.
262
+ */
263
+ function scanDialect(program) {
264
+ const findings = [];
265
+ walk(program, (node) => {
266
+ switch (node.type) {
267
+ case "CallExpression":
268
+ case "NewExpression": {
269
+ const codegen = bareCodegenCallee(node.callee);
270
+ if (codegen !== null) findings.push({
271
+ kind: codegen,
272
+ ...positionOf(node)
273
+ });
274
+ if (node.type === "CallExpression" && isReflectConstructorGet(node)) findings.push({
275
+ kind: "constructor-access",
276
+ ...positionOf(node)
277
+ });
278
+ break;
279
+ }
280
+ case "MemberExpression":
281
+ if (isConstructorMemberExpression(node)) findings.push({
282
+ kind: "constructor-access",
283
+ ...positionOf(node)
284
+ });
285
+ break;
286
+ case "ObjectPattern":
287
+ for (const property of node.properties) if (property.type === "Property" && isConstructorPatternProperty(property)) findings.push({
288
+ kind: "constructor-access",
289
+ ...positionOf(property)
290
+ });
291
+ break;
292
+ default: break;
293
+ }
294
+ });
295
+ return findings;
296
+ }
297
+ //#endregion
154
298
  //#region src/rules/dialect.ts
155
299
  const noCodeGeneration = {
156
300
  meta: {
157
301
  type: "problem",
158
- docs: { description: "ban dynamic code generation (eval, the Function constructor, and .constructor access) in workflow modules; it reopens the import allowlist and the ambient capability surface the sandbox dialect closes" },
302
+ docs: { description: "ban dynamic code generation (eval, the Function constructor, and constructor reconstruction) in workflow modules; it reopens the import allowlist and the ambient capability surface the sandbox dialect closes" },
159
303
  messages: {
160
304
  noEval: "eval is not part of the workflow dialect; it compiles code in a fresh scope that reopens import() and ambient capability. Stay on the ctx surface.",
161
305
  noFunctionConstructor: "the Function constructor is not part of the workflow dialect; it compiles arbitrary code that bypasses the import allowlist. Stay on the ctx surface.",
162
- noConstructorAccess: ".constructor access reaches the Function constructor from any function value and reopens dynamic code generation; it is not part of the workflow dialect."
306
+ noConstructorAccess: "constructor access reaches the Function constructor from any function value and reopens dynamic code generation; it is not part of the workflow dialect."
163
307
  },
164
308
  schema: []
165
309
  },
@@ -196,23 +340,25 @@ const noCodeGeneration = {
196
340
  return {
197
341
  CallExpression(node) {
198
342
  checkCallee(node, node.callee);
343
+ if (isReflectConstructorGet(node)) context.report({
344
+ node,
345
+ messageId: "noConstructorAccess"
346
+ });
199
347
  },
200
348
  NewExpression(node) {
201
349
  checkCallee(node, node.callee);
202
350
  },
203
351
  MemberExpression(node) {
204
- const { property, computed } = node;
205
- if (!computed && property.type === "Identifier" && property.name === "constructor") {
206
- context.report({
207
- node,
208
- messageId: "noConstructorAccess"
209
- });
210
- return;
211
- }
212
- if (computed && property.type === "Literal" && property.value === "constructor") context.report({
352
+ if (isConstructorMemberExpression(node)) context.report({
213
353
  node,
214
354
  messageId: "noConstructorAccess"
215
355
  });
356
+ },
357
+ ObjectPattern(node) {
358
+ for (const property of node.properties) if (property.type === "Property" && isConstructorPatternProperty(property)) context.report({
359
+ node: property,
360
+ messageId: "noConstructorAccess"
361
+ });
216
362
  }
217
363
  };
218
364
  }
@@ -339,4 +485,4 @@ const workflowsConfig = {
339
485
  };
340
486
  plugin.configs = { workflows: workflowsConfig };
341
487
  //#endregion
342
- export { plugin as default, rules, toJsonDiagnostics, workflowsConfig };
488
+ export { plugin as default, rules, scanDialect, toJsonDiagnostics, workflowsConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-rulvar",
3
- "version": "1.38.0",
3
+ "version": "1.40.0",
4
4
  "description": "Rulvar determinism lint rules with structural JSON diagnostics for the planner self-repair loop.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",