# Why can't any tool see calls made through eval?

> Rule AR-10: No eval/exec/Function() to call code.

`eval`, `exec` and `new Function(...)` take source code as a string and run it. Whatever they call is written inside a string value, so a search finds a string, a type checker sees an argument of type `str`, and a rename touches neither. They are also the one place where a data value can become code, which is why the standard linters flag them. Use a dispatch table (AR-08) to call a function, and a parser to read data.

## Why it happens
**Python.** The built-in reference documents `eval(source, /, globals=None, locals=None)` as parsing and evaluating `source` as a Python expression, where `source` is a string or a code object. `exec` runs a string or code object as Python code and returns `None`. Both carry the same warning in the docs: they execute arbitrary code, and calling them with untrusted input leads to security vulnerabilities. The docs add that overriding `__builtins__` can restrict or change available names but is not a security mechanism. For strings that contain only literals, the docs point to the literal evaluator in Python's syntax-tree module.

**TypeScript and JavaScript.** `eval` runs a string as script. ESLint's `no-eval` page lists the forms it reports: direct calls, indirect forms such as `(0, eval)(...)` and `const foo = eval; foo(...)`, and `window.eval(...)` or `this.eval(...)` on the global object. The ECMAScript spec defines `eval` through an abstract operation `PerformEval` and a host hook, `HostEnsureCanCompileStrings`, that lets a host refuse to compile strings (section 19.2.1, `eval ( source )`, has one step: return `PerformEval(source, false, false)`; `PerformEval` is 19.2.1.1, and its step 5 performs `HostEnsureCanCompileStrings`: https://tc39.es/ecma262/multipage/global-object.html#sec-eval-x). The `Function` constructor builds a function from strings. ESLint's `no-new-func` reports `new Function(...)`, `Function(...)` without `new`, and the `.call`, `.apply` and `.bind` forms. `setTimeout("code", n)` and `setInterval("code", n)` with a string first argument are an implied `eval`, and `no-implied-eval` reports them.

For a type checker the consequence is simple. The string's content is a value, not syntax. `eval(f"{name}(order)")` calls a function named by `name`, and no tool can tell which one without knowing every value `name` takes. That is the AR-07 problem with no way to narrow it.

## Bad and good
A rules engine where each rule is stored as a text expression.

```python tab
# bad: the callee is inside a string
def apply_rule(rule: dict, order: Order):
    return eval(rule["expr"], {"order": order, "discount": discount, "tax": tax})
# rule["expr"] == "discount(order, 0.1) + tax(order)"
```

```ts tab
// bad: the callee is inside a string
function applyRule(rule: { expr: string }, order: Order): number {
  const fn = new Function("order", "discount", "tax", `return ${rule.expr};`);
  return fn(order, discount, tax);
}
```

```python tab
# good: rules name a known operation; the code lives in source
from typing import Callable, Literal

Op = Literal["discount", "tax"]

OPS: dict[Op, Callable[[Order, float], float]] = {
    "discount": lambda o, rate: discount(o, rate),
    "tax": lambda o, rate: tax(o),
}

def apply_rule(rule: "Rule", order: Order) -> float:
    return sum(OPS[step.op](order, step.arg) for step in rule.steps)
```

```ts tab
// good: rules name a known operation; the code lives in source
type Op = "discount" | "tax";

const ops = {
  discount: (o: Order, rate: number) => discount(o, rate),
  tax: (o: Order, _rate: number) => tax(o),
} satisfies Record<Op, (o: Order, arg: number) => number>;

function applyRule(rule: Rule, order: Order): number {
  return rule.steps.reduce((sum, s) => sum + ops[s.op](order, s.arg), 0);
}
```

The rule data becomes `{op: "discount", arg: 0.1}`, a value the program validates. Every operation a rule can name appears in source, so an agent that changes `discount` finds the table entry and the type checker checks the call.

## Trade-offs and when it is acceptable
- **Eval is the shortest path to a calculator or a REPL.** If the product is code execution (a notebook, a plugin sandbox, a build tool that loads user scripts), it is the right tool. Keep it in one small module with a comment, and keep agents' edits away from it.
- **A table is less flexible.** Users can only combine operations you listed. For most configuration-driven behaviour, that limit is the feature.
- **Python's literal evaluator (in the syntax-tree module) is the safe half.** It reads literals (numbers, strings, lists, dicts) and raises on anything else, per the Python docs. It does not call functions.
- **For JSON-shaped data, use a JSON parser.** It never runs the content.
- **Sandboxes don't make eval traceable.** Node's `vm` page says in its introduction that the module is not a security mechanism and should not be used to run untrusted code. A sandbox changes what the code may touch. It doesn't make its calls visible to analysis.
- **Code generation** (templates that write source files) is a separate pattern. Commit the output, or commit the generator and the command that runs it (AR-23).

## Enforce it
| Tool | Setting | What it catches | Checked |
|---|---|---|---|
| ruff | `S307` suspicious-eval-usage | any use of the builtin `eval()` (suggests the standard library's literal evaluator) | 2026-10-30 |
| ruff | `S102` exec-builtin | calls to the builtin `exec` | 2026-10-30 |
| eslint | `no-eval` (option `allowIndirect`, default `false`) | direct and indirect `eval`, `window.eval`, `this.eval`; user-defined methods named `eval` are not flagged | 2026-10-30 |
| eslint | `no-new-func` | `new Function(...)`, `Function(...)`, and `.call`, `.apply`, `.bind` forms | 2026-10-30 |
| eslint | `no-implied-eval` | `setTimeout`, `setInterval` and `execScript` with a string as first argument | 2026-10-30 |

Both ruff rules come from the flake8-bandit set; enable them with `extend-select = ["S102", "S307"]` in `[tool.ruff.lint]`. Whether ruff's `S` set is on by default is not stated on those rule pages, so list the two codes.

## Rule block for AGENTS.md / CLAUDE.md
```markdown
- Never use eval, exec, `new Function(...)` or string-argument setTimeout/setInterval to call code.
- To call a function chosen at runtime, use a typed table keyed by a literal type (see dispatch rules).
- To read data, use json.loads / JSON.parse.
- If a task seems to need eval, stop and ask; don't add it.
```

## Edge cases
- **`eval` of expressions the user types** (a formula cell, a filter box): use a parser for a small grammar. A table of operations plus a tokenizer is a small amount of code and never executes user text.
- **`compile()` then `exec`** has the same blind spot as the string forms; the source is still a value.
- **Dynamic `import()` and `importlib`** load modules by string. That is the module-level version of AR-07; see AR-37 for components.
- **Tests that `eval` expected values** hide what the test asserts. Write the value as a literal.
- **The literal evaluator accepts only literals.** The docs list the allowed structures (strings, bytes, numbers, tuples, lists, dicts, sets, booleans, `None` and `Ellipsis`) and say it "is not capable of evaluating arbitrarily complex expressions, for example involving operators or indexing", so an expression like `1 + 2` is rejected (Python standard library docs, syntax-tree module). The docs do not say whether a complex literal such as `1 + 2j` is accepted.

## What LLMGRAPH shows
For callees named only inside an `eval` string, LLMGRAPH prints `no calls to it found in this repo` and `plus 1 dynamic call nearby that could reach anything`. With a table of lambdas that name them in source, both print `certain`.

## References
- Python library reference, built-in functions (`eval`, `exec`): https://docs.python.org/3/library/functions.html
- Ruff S307, S102: https://docs.astral.sh/ruff/rules/suspicious-eval-usage/ · https://docs.astral.sh/ruff/rules/exec-builtin/
- ESLint: https://eslint.org/docs/latest/rules/no-eval · https://eslint.org/docs/latest/rules/no-new-func · https://eslint.org/docs/latest/rules/no-implied-eval
- ECMAScript spec, global object (`eval`, `PerformEval`, `HostEnsureCanCompileStrings` in the section list): https://tc39.es/ecma262/multipage/global-object.html
- Node.js `vm` module: https://nodejs.org/api/vm.html
- TypeScript utility types (`Record`) and `satisfies`: https://www.typescriptlang.org/docs/handbook/utility-types.html · https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html

## Changelog
- 2026-10-30: draft 1.
- 2026-10-30: verify items resolved (docs check)
- 2026-10-30: copy-gate wording fixes

## Related

- [AR-07: Why does my coding agent miss calls made through getattr?](https://beta.llmgraph.in/agent-readable/ar-07.md)
- [AR-08: How do I write dispatch that type checkers can follow?](https://beta.llmgraph.in/agent-readable/ar-08.md)
- [AR-23: How should generated code be handled for coding agents?](https://beta.llmgraph.in/agent-readable/ar-23.md)

Last verified 2026-10-30
