Recently, I replaced an ESLint rule that was specific to arrays —
no-array-foreach — with a more general rule —
no-foreach — that also effected failures for the
no-foreach rule supports a
types option — so the that the types for which it is enforced can be configured by the developer — and I planned to leverage that by deprecating and re-implementing
no-array-foreach to use
Something like this:
options property is overwritten before it’s passed to the base rule’s
create function. However, that fails with an error:
To work around this, it’s necessary to create a new object that includes the
options, along with all of the other properties on
context. And there are a number of ways that can be done.
When the spread syntax is used, all of
context’s (own) properties are spread into the new object —
contextForBaseRule — along with the specified
options, like this:
There is a problem with this, though: it breaks the prototype chain. That’s not an issue with the
report properties — they’re own properties on
context — however,
context has a bunch of other properties that are on its prototype and, with that implementation, the rule fails with an error:
parserOptions is one of the properties that’s on
context’s prototype and it’s needed within the rule to retrieve the TypeScript node that corresponds to an ESLint node.
It’s possible to use a
Proxy to override an object’s property, like this:
get handler is specified and when a property is accessed, it checks the property name. If it’s
"options", it returns the options that need to be passed to the base rule. Otherwise, it returns the value of
However, that doesn’t work either. It fails with this error:
The reason for this is that the invariants outlined in the ECMAScript specification are enforced by the
Proxy objects maintain these invariants by means of runtime checks on the result of [handlers] invoked on the [[ProxyHandler]] object.
The value reported for a property must be the same as the value of the corresponding target object property if the target object property is a non-writable, non-configurable own data property.
The value reported for a property must be undefined if the corresponding target object property is a non-configurable own accessor property that has undefined as its [[Get]] attribute.
This is something that I’d run into before, but had forgotten. It’s a small comfort, though, to know that there are limits on the havoc that can be wreaked with proxies.
Object.create can be used to create a new object which has
context as its prototype, to which the
options can then be assigned, like this:
However, that won’t work and will fail with this error:
This surprised me. I expected the
options property to be added to the created object — regardless of the fact that there is an
options property on the prototype.
It turns out that that’s not how the language works.
The details are in the ECMAScript specification, but the gist of it is that when an object property is assigned a value, the runtime looks for a property descriptor to determine whether or not the assignment is permitted. First, it looks at the object’s own property descriptors, but if it doesn’t find a descriptor, it then looks at the object’s prototype’s descriptors.
So what’s happening here is:
- The runtime looks for a property descriptor for the
contextWithOption, but it doesn’t find one.
- It then looks for a property descriptor on
contextand finds one.
- However, the property descriptor indicates that the property is not writable, so an error is thrown.
Fortunately, the second parameter to
Object.create can be used to add properties to the created object — using property descriptors like those passed to
Object.defineProperty — like this:
Once that’s done, the rule works:
- When the base rule accesses the
optionsproperty, it finds the property on
contextForBaseRule— which overrides the
- When the base rule calls the
reportmethod, it finds the method on
context— which is the prototype for
- And when the base rule accesses the
parserOptionsproperty, it finds the property on
For something that I’d expected to be straightforward, a surprising number of attempts were needed to get this working. 😅 However, I did learn some things along the way.