Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
bump: minor
---

### Added

- **The parser reader bounds a computed key it cannot list.** Under `--reader=parser`, in a module with an import or export or a program whose first statement is `"use strict"`, a computed read whose key can spell neither `require` nor `constructor` no longer counts as a load the parser cannot name or a read that reaches `Function`. A computed write there counts only for the trusted names its key can spell, unless it can spell `__proto__`. A `+` makes a number or a string that starts with its left side and ends with its right side, so `e[n+1]` spells neither. A template starts and ends with its own text. A parameter holds what each call passes when it is never written and its function is bound to one name, never exported, only ever called directly, and out of reach of `arguments` and block hoisting. Code that may run sloppy, including the code a `Function` call folds to, is bounded only by known values, since `f.caller` can call a function with any argument. When the parser refuses a callback held in a parameter whose function is used other than by a call, the reason now reads `the parameter's function is used other than by a call`, so it no longer matches the refusal for a returning function.
23 changes: 18 additions & 5 deletions plugins/harness-ops/skills/inventory/reference/extraction.md
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,19 @@ an undeclared name that is a trusted one (`Promise=f`). A key is a name when it
template without substitutions, or `Symbol.x`. A computed key is known when every value it can
take is: a literal, a number (arithmetic, `++`, unary minus), a boolean or `typeof` result, or a
variable each write of which is one of those, so a loop counter (`o[i]=`) or a TypeScript enum
(`e[e.X=1]=`) writes no trusted name; any other key may write every one. A target is cleared only
(`e[e.X=1]=`) writes no trusted name. In code that runs strict (a module with an import or
export, or a program whose first statement is `"use strict"`), a key that is not known is still
bounded when it is a `+`, which makes a number or a string starting with its left side's value
and ending with its right side's (`o[n+1]=` ends in a digit), or a `+=`, by its right side the
same way; a template, which starts and ends with its own text; a `?:` or logical branch, a
sequence's last expression or an `=` assignment's value that is bounded; a variable each write
of which is bounded; or a parameter with no default, pattern or rest, never written, of a
function bound to one name, never exported, only ever called directly with no spread up to it,
not reading `arguments` and not declared in a block, which holds what each call passes. Sloppy
code, the code a `Function` call folds to included, can call a function through `f.caller` and
binds a block function's name outside its block, so there only a known key is bounded. A bounded
key that cannot spell `__proto__` writes only the trusted names it may spell; any other key may
write every one. A target is cleared only
when it is provably fresh: a literal, a function, or `new` of a class declared in the module with
no superclass and no constructor returning a value; `new` of a built-in constructor named by its
unshadowed global name (`Error` and the other native errors, `Map`, `Set`, `WeakMap`, `WeakSet`),
Expand Down Expand Up @@ -512,7 +524,7 @@ further member, past any call on the way, names another key, a computed one know
it, and the chain ends discarded, tested, compared or written, never held, passed on or the left
of `instanceof`) or a build cleared below, any member named `eval` or `Function`, a read of
`constructor` (by name, by a computed key that may spell it, as an unknown key does while a loop
counter does not, or as a destructured key; every
counter or `n+1` does not, or as a destructured key; every

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Advisory: the doc and the changelog fragment don't describe the behavior change from the round-1 fix.

maySpellAnywhereSink (parser_helper.cjs:2337) means a constructor read is exempt as inert only when its key also can't spell eval, Function, a vm runner or __defineGetter__/__defineSetter__. An unknown key, such as a for in key or any key in sloppy code, can spell all of them.

So a discarded or tested unknown-key read is now a function-constructor hit, in sloppy code too. Common bundle code like for(k in o)if(o[k]) used to be exempt and now counts.

This paragraph still says a constructor read that is "discarded, tested, compared…" does not count, and that "any member named eval or Function" counts, which reads as literal names only. The fragment says sloppy code "is bounded only by known values", which suggests its results are unchanged.

It is the safe direction, but the next host run's count of function-constructor hits will rise for a reason the doc doesn't give. Please say in this paragraph and in the fragment that a computed read whose key may spell one of those members counts wherever it stands, inert or not.

claude-lane: claude-review

function's `constructor` is `Function`, as in `(()=>0).constructor("...")()`) other than one that
is discarded, tested, compared with `===` or `!==`, the right of `instanceof`, or followed by a
`.name` read or a plain `.prototype` write, since coercing it or writing a key it lacks runs a
Expand Down Expand Up @@ -572,13 +584,14 @@ the literal does not hold. A promise settled with the namespace reads its `then`
file's module must be known and export no `then`. A load the scan cannot name (`import(x)` with a
specifier that is no literal; `require`, `import.meta.require` or `x.require` aliased, `.call`ed,
behind a comma, or called with no literal; a computed member or destructured key whose value, read
as the sink rule reads a computed key, is unknown or may be `require`, as in `module[k]`,
or bounded as the sink rule reads a computed key, may be `require`, as in `module[k]`,
`x["req"+"uire"]` and `{[k]:r}=globalThis`, other than the target of a plain `o[k]=v`) could
load any file whole, so any such module fails every export hop; `typeof require`,
`require.resolve`, a binding named `require`, and a `.require` member read that is only tested
(a `&&` operand other than the last, an `if`, loop or `?:` test, a `!` operand, as in lodash's
`w&&w.require&&w.require("util")`) do not count. A computed read whose key is a
parameter, a `for in` key or a string built with `+` is unknown, so a module holding one counts.
`w&&w.require&&w.require("util")`) do not count. A computed read whose key is a `for in` key, a
parameter the rule cannot bound, or a `+` whose sides may start and end `require` (`k+s` with
`k="re"`) counts; in strict code `e[n+1]` and `` e[`on${n}`] `` do not.
A template `import()` whose specifier starts with a bare package name
(`` import(`@withfig/autocomplete/build/${e}.js`) ``) counts too: no source read shows that a bare
specifier cannot reach a file embedded in the executable (pointer: Bun's
Expand Down
Loading
Loading