Controlling DontEnum (was: ES4 draft: Object)
The use of a __-bracketed name satisfies the second goal, but it would be a problem for Caja or similar. This is an issue where I would appreciate Mark's feedback.
I can't speak for Mark, but it seems like it might actually be beneficial that unsecured code (regular ES4) would have the ability to set DontEnum and secured code (Secure ES, Cajoled Caja, ADsafe, or whatever) wouldn't have that capability. +1 Kris
On Mar 11, 2008, at 2:45 PM, Brendan Eich wrote:
Proposal: Add Object.prototype.makePropertyNonEnumerable(name), defined as if by:
Object.prototype.__makePropertyNonEnumerable__ = function
(name) { if (this has a direct property identified by name) { set the DontEnum attribute for this[name]; return true; } return false; }
Important missing statement after this one (I was getting on a plane
and realized it was omitted just after shutting down :-/):
Object.prototype.makePropertyNonEnumerable ('makePropertyNonEnumerable');
In ES4 terms, the spec would look like this (including reversion of
propertyIsEnumerable to ES3 form):
Obvious changes to "Synopsis" left as exercise for reader ;-).
Under "Methods on Object instances":
intrinsic function propertyIsEnumerable(name: EnumerableId): boolean private::propertyIsEnumerable(name);
private function propertyIsEnumerable(name) { if (!magic::hasOwnProperty(this, name)) return false;
return !magic::getPropertyIsDontEnum(this, name);
}
intrinsic function makePropertyNonEnumerable(name: EnumerableId):
boolean
private::makePropertyNonEnumerable(name);
private function makePropertyNonEnumerable(name) { if (!magic::hasOwnProperty(this, name)) return false;
magic::setPropertyIsDontEnum(this, name, true);
return true;
}
Under "Methods on the Object prototype object":
prototype function propertyIsEnumerable(name) this.private::propertyIsEnumerable(helper::toEnumerableId(name));
prototype function makePropertyNonEnumerable(name) this.private::makePropertyNonEnumerable(name);
If there is no method to make a property enumerable again (which I now agree is better not to exist) it feels somehow bad to be able to make a property non-enumerable after its declaration.
Consider:
let a = {b:2, c:3}; for(let s in a){ print(s); // b, c } a.makePropertyNonEnumerable("b"); for(let s in a){ print(s); // c }
It seems wrong that the code should run once one way, and then another way, but there be no way back. It's not so much that I think there should be a way back, but I'd rather that than this, which I consider a weird situation. Also the double underscores are really ugly syntax...
Perhaps you can't achieve this without new syntax, but the declarative approach seems cleanest and not prone to this sort of anomaly. If new syntax could be considered, an |enumerable| attribute, complemented by a |!enumerable| attribute seems very clean.
type T = {enumerable a:int, b:int}; let t:T = {a:1, b:2}:T; // a is enumerable, b is not
let s = {a:1, !enumerable b:1}; // a is enumerable, b is not
class C { enumerable var p; }
Peter
[+google-caja-discuss]
On Tue, Mar 11, 2008 at 12:53 PM, Kris Zyp <kris at sitepen.com> wrote:
The use of a __-bracketed name satisfies the second goal, but it would be a problem for Caja or similar. This is an issue where I would appreciate Mark's feedback.
I can't speak for Mark, but it seems like it might actually be beneficial that unsecured code (regular ES4) would have the ability to set DontEnum and secured code (Secure ES, Cajoled Caja, ADsafe, or whatever) wouldn't have that capability. +1 Kris
I think Kris is right, but I'm not yet sure. I've forwarded this thread to google-caja-discuss at googlegroups.com for discussion of what would be the best ways to support Caja's needs.
Nono of these issues are specific to DontEnum. They affect all other integrity restriction attributes, such as ReadOnly, DontDelete, and fixture (i.e., "non-dynamic").
On Tue, Mar 11, 2008 at 2:32 PM, Peter Hall <peter.hall at memorphic.com> wrote:
Perhaps you can't achieve this without new syntax, but the declarative approach seems cleanest and not prone to this sort of anomaly. If new syntax could be considered, an |enumerable| attribute, complemented by a |!enumerable| attribute seems very clean.
type T = {enumerable a:int, b:int}; let t:T = {a:1, b:2}:T; // a is enumerable, b is not
let s = {a:1, !enumerable b:1}; // a is enumerable, b is not
class C { enumerable var p; }
Hi Peter, I like the idea of a more declarative approach as well, if one can be found. However, this proposal has two problems from our perspective:
-
It introduces new syntax, as you mention.
-
It depends on the proposed ES4 type system which is unacceptable to the ES3.1 effort. ES3.1 needs a way to express these integrity constraints in the absence of ES4 types.
It seems wrong that the code should run once one way, and then another way, but there be no way back. It's not so much that I think there should be a way back, but I'd rather that than this, which I consider a weird situation.
Declarative is nice, but as mentioned before, it ignores one of the primary use cases: addition of non-enumerable properties to built-ins (by libraries, primarily).
Also the double underscores are really ugly syntax...
That's the idea, no one is currently using such an ugly method ;).
Kris
On Mar 11, 2008, at 5:16 PM, Kris Zyp wrote:
Declarative is nice, but as mentioned before, it ignores one of the
primary use cases: addition of non-enumerable properties to built-ins (by
libraries, primarily).
I've read mention of the weirdness of the timing window between the
property definition and it's marking as non-enumerable. That combined
with the above observation makes me wonder if this should really be
obj.setNonEnumerableProperty(name, value);
On Mar 11, 2008, at 3:21 PM, Neil Mix wrote:
On Mar 11, 2008, at 5:16 PM, Kris Zyp wrote:
Declarative is nice, but as mentioned before, it ignores one of the primary use cases: addition of non-enumerable properties to built-ins (by libraries, primarily).
I've read mention of the weirdness of the timing window between the property definition and it's marking as non-enumerable. That combined with the above observation makes me wonder if this should really be obj.setNonEnumerableProperty(name, value);
+1
I've read mention of the weirdness of the timing window between the property definition and it's marking as non-enumerable. That combined with the above observation makes me wonder if this should really be obj.setNonEnumerableProperty(name, value);
+1
I like it too. Any chance that by setting the attribute at the same time of setting the property value, we could resurrect the possibility of setting other attributes as well (with a single method):
Object.prototype.setPropertyWithAttributes(name, value, dontEnum: boolean, readOnly: boolean, dontDelete: boolean);
And various proposed syntax like: obj = {dontenum a: 1, const b: 2, var c: 3};
could be written: obj = {}; obj.setPropertyWithAttributes("a",1,true); // property a is not enumerable
obj.setPropertyWithAttributes("b",2,false,true); // property b is readonly
obj.setPropertyWithAttributes("c",3,false,false,true); // propery c is permanent
I wonder if readonly and dontdelete are other attributes where there might be benefits derived from trusted code (regular ES3/4) being able to control, and untrusted code (ses, caja) not being able to control. I can't think of what those benefits might be, maybe another question for Mark or Doug to think on.
Kris
On Tue, Mar 11, 2008 at 8:32 PM, Kris Zyp <kris at sitepen.com> wrote:
I've read mention of the weirdness of the timing window between the property definition and it's marking as non-enumerable. That combined with the above observation makes me wonder if this should really be obj.setNonEnumerableProperty(name, value);
+1 I like it too. Any chance that by setting the attribute at the same time of setting the property value, we could resurrect the possibility of setting other attributes as well (with a single method):
Object.prototype.setPropertyWithAttributes(name, value, dontEnum: boolean, readOnly: boolean, dontDelete: boolean);
And various proposed syntax like: obj = {dontenum a: 1, const b: 2, var c: 3};
could be written: obj = {}; obj.setPropertyWithAttributes("a",1,true); // property a is not enumerable
obj.setPropertyWithAttributes("b",2,false,true); // property b is readonly
obj.setPropertyWithAttributes("c",3,false,false,true); // propery c is permanent
The order of the boolean parameters is kind of annoying to have to remember.
Would it be OK to shorten the method?
obj.setProperty("c"); // undefined value obj.setProperty("c", 2); obj.setProperty("c", undefined, "dontenum", "readonly"); obj.setProperty("c", undefined, "dontdelete");
-- or --
obj.setProperty("c", undefined). // Reference type. dontEnum(). readOnly(). dontDelete();
On Mar 11, 2008, at 8:32 PM, Kris Zyp wrote:
I like it too. Any chance that by setting the attribute at the same
time of setting the property value, we could resurrect the possibility of
setting other attributes as well (with a single method):Object.prototype.setPropertyWithAttributes(name, value, dontEnum: boolean, readOnly: boolean, dontDelete: boolean);
Only if you cannot change an existing properties attribute, or delete
an existing property and create a new one with the same name. Those
cases would have to fail. A false return is not a fail-stop
condition, so an exception might be better. Mark probably has
thoughts on this -- he referred to "defensive consistency" as defined
in his thesis when we discussed the silence-is-deadly ES1-era
handling of assignment to read-only properties, and the almost
useless boolean result of the delete operator.
I wonder if readonly and dontdelete are other attributes where
there might be benefits derived from trusted code (regular ES3/4) being able to
control, and untrusted code (ses, caja) not being able to control. I can't
think of what those benefits might be, maybe another question for Mark or
Doug to think on.
You can't implement JS in JS without something like this. Narcissus
has almost exactly what you describe here, only SpiderMonkey js
shells built with NARCISSUS=1:
lxr.mozilla.org/mozilla/search?string=defineProperty
But this, as its name suggests, is unsafe: it will redefine a
property, clobbering any existing attributes and value. And yeah, the
lack of named parameters hurts call-site readability.
Object.prototype.setPropertyWithAttributes(name, value, dontEnum: boolean, readOnly: boolean, dontDelete: boolean);
Only if you cannot change an existing properties attribute, or delete an existing property and create a new one with the same name. Those cases would have to fail. A false return is not a fail-stop condition, so an exception might be better.
Yes, I agree.
And yeah, the lack of named parameters hurts call-site readability.
Yeah, I took ugly and maybe it even uglier, didn't I :). Obviously one could do better...
Kris
Trying to sum up in order to make progress. Let me all know what you think.
Since our method is creating a new property and not setting one, I suggest createProperty as the most appropriate name, so I'm going to use that below.
I still think it may be right that properties in non-public namespaces should not be enumerated, but I also think that's orthogonal to the discussion that's going on here, about dynamic properties and their attributes. We've pretty much decided (ticket #233 has some of the discussion) that for future compatibility there ought to be a difference between fixture properties and DontDelete "dynamic" properties. So dynamic properties have at least one attribute bit like they do in ES3 (for deletability). Consequently, they might as well have all the ES3 bits: for enumerability and writability.
Ergo, let's assume that we are not finessing more than we have to, and dynamic properties have all these attribute bits. I don't know why createProperty should not be able to set all of these.
(They're not independent, ReadOnly implies DontDelete.)
createProperty should throw an exception (TypeError?) if the property already exists on the object or would shadow a ReadOnly property, a la [[CanPut]], or if the object is not dynamic. It should probably throw an exception if its arguments are not consistent (ReadOnly && !DontDelete).
As for an interface, there are four options. We can have three independent arguments (booleans that default to false):
o.createProperty("foo",true,true,true)
or we can have a bitmask of flags, C-style (with the flag constants defined on Object, probably -- obviously a danger of introducing new names):
o.createProperty("foo",Object.DontEnum|Object.DontDelete)
or we can pass flags as strings:
o.createProperty("foo","DontEnum","DontDelete")
or we can pass an object with named properties:
o.createProperty("foo",{dontEnum:true,dontDelete:true})
(Let's avoid restarting the discussion about named arguments in this thread, even if we'd obviously benefit from such a mechanism.)
Pesonally I like the strings approach, but the only interface among these four that has good compile-time checking is the first one, so I'm going to propose that we use that one, with dontEnum as the first flag, dontDelete as the second, and readOnly as the third (based on a guess about the frequency of use). Thus on Object.prototype and as an intrinsic propety on Object instances:
function createProperty(name:EnumerableId, dontEnum:boolean=false, dontDelete:boolean=false, readOnly:boolean=false): void
Notes.
-
The above does not preclude complementary declarative mechanisms in ES4 (but not in ES3.1 obviously)
-
The above has no obvious bearing on what's done about enumerating namespaced properties.
-
The above morphs easily to Object.createProperty or similar, if that's desired; I'm entirely agnostic.
-
The updated form of propertyIsEnumerable is removed from the Object class in any case.
the frequency of use). Thus on Object.prototype and as an intrinsic propety on Object instances:
function createProperty(name:EnumerableId, dontEnum:boolean=false, dontDelete:boolean=false, readOnly:boolean=false): void
I thought that this only made sense in conjunction with Neil's suggestion of providing the value at the same time. If you create a readonly property, how are you supposed to set the value otherwise? Shouldn't it be: function createProperty(name:EnumerableId, value, dontEnum:boolean=false, dontDelete:boolean=false, readOnly:boolean=false): void
And if readOnly implied dontDelete, I would have guessed readOnly to be a little more frequent than dontDelete, but of course that is just a guess. Kris
-----Original Message----- From: Kris Zyp [mailto:kris at sitepen.com] Sent: 13. mars 2008 11:27 To: Lars Hansen; es4-discuss at mozilla.org Subject: Re: Controlling DontEnum (was: ES4 draft: Object)
the frequency of use). Thus on Object.prototype and as an intrinsic propety on Object instances:
function createProperty(name:EnumerableId, dontEnum:boolean=false, dontDelete:boolean=false, readOnly:boolean=false): void I thought that this only made sense in conjunction with Neil's suggestion of providing the value at the same time. If you create a readonly property, how are you supposed to set the value otherwise? Shouldn't it be: function createProperty(name:EnumerableId, value, dontEnum:boolean=false, dontDelete:boolean=false, readOnly:boolean=false): void
Simple oversight. You are right.
And if readOnly implied dontDelete, I would have guessed readOnly to be a little more frequent than dontDelete, but of course that is just a guess.
ReadOnly implies DontDelete because ReadOnly doesn't make sense without DontDelete, but my guess (based on no data at all, just how I write code) is that undeletable mutable state is desired slightly more often than undeletable immutable state. (Also, if you're asking for ReadOnly=true then you're forced to state DontDelete=true, reminding you of this fact. My hunch is that this order slightly reduces the scope for error, which would be greater if you could state ReadOnly=true without being forced to think about DontDelete in the last argument position.)
All that said, I like Yuh-Ruey Chen's variation, where flags are tokens extracted from Object but passed individually following the value; if we ever end up with strong type checks for rest arguments (a feature that is a little wobbly at present) then the API would look like this:
class Object { static const READ_ONLY : AttrToken = new AttrToken; static const DONT_ENUM : AttrToken = new AttrToken; static const DONT_DELETE : AttrToken = new AttrToken;
intrinsic function __createProperty__(name, value, ...bits :
[AttrToken]) ... }
for some opaque non-user-accessible class AttrToken.
Is there enough value in complicating matters by adding the ability to set dontDelete and readOnly? You can create non-deletable properties by using a class or record type, and you can create read-only properties by adding a get function without a corresponding set...
Are there any use cases where these solutions won't cover it?
Peter
-----Original Message----- From: peterjoel at gmail.com [mailto:peterjoel at gmail.com] On Behalf Of Peter Hall Sent: 13. mars 2008 12:38 To: Lars Hansen Cc: Kris Zyp; es4-discuss at mozilla.org Subject: Re: Controlling DontEnum (was: ES4 draft: Object)
Is there enough value in complicating matters by adding the ability to set dontDelete and readOnly? You can create non-deletable properties by using a class or record type, and you can create read-only properties by adding a get function without a corresponding set...
Are there any use cases where these solutions won't cover it?
Depends on your point of view. I wrote a paper on evolutionary programming in ES4 (available from ecmascript.org), which demonstrates a progression from ES3 style code to ES4 style code with various stops along the way. One of the holes in that story is that it's not possible to make the properties introduced in a function-style constructor anything but enumerable, deletable, and writable:
function Server(host) { this.host = host this.database = [] }
Both these fields are supposed to be constant, and there's no reason for them to be enumerable. The paper shows how you can use a user-defined namespace to make them inaccessible (non-public namespaced properties are not enumerated, and if the namespace is hidden from client code -- not always easy -- then the field can't be accessed or deleted), but it's clearly a bit of a hack in that it is a consequence of other design choices in the language. The direct approach would set the properties explicitly:
function Server(host) { this.createProperty("host", host, true, true, true); this.createProperty("database", [], true, true, true); }
So this is a use case if you believe that relatively fine-grained evolution from ES3 style to ES4 style is a feature of the language. (I do.)
There are declarative ways of solving the problem -- essentially annotating the function, saying "this is a constructor function and the fields named such and so are going to have these and those attributes", but they add yet more features to an already fairly feature-laden language.
(One proposal goes like this:
constructor function Server(host) : { const host: string, const database: array } { this.host = host this.database = [] }
and it's not all that heavyweight, but it's a new, ad hoc feature all the same.)
I'm mostly agnostic about createProperty being able to set DontDelete and ReadOnly; DontEnum is the really important one. At the same time, it seems silly not to just solve the general problem when we have a chance. Just because you can't enumerate a property doesn't mean that you -- or a library you import -- can't accidentally -- or maliciously -- overwrite it.
set dontDelete and readOnly? You can create non-deletable properties by using a class or record type
They can't be added after an object is created.
and you can create read-only properties by adding a get function without a corresponding set...
Unless behavior is different than ES3, setters (and lack thereof) are not dontDelete, therefore still easily mutated:
obj = {get foo() { return 'hi' }}
Object foo=hi
obj.foo "hi"
delete obj.foo
true
obj.foo = 'goodbye' "goodbye"
obj.foo "goodbye"
On 13/03/2008, Lars Hansen <lhansen at adobe.com> wrote:
Since our method is creating a new property and not setting one, I suggest createProperty as the most appropriate name, so I'm going to use that below.
Sounds perfectly reasonable to me.
I still think it may be right that properties in non-public namespaces should not be enumerated, but I also think that's orthogonal to the discussion that's going on here, about dynamic properties and their attributes. We've pretty much decided (ticket #233 has some of the discussion) that for future compatibility there ought to be a difference between fixture properties and DontDelete "dynamic" properties. So dynamic properties have at least one attribute bit like they do in ES3 (for deletability). Consequently, they might as well have all the ES3 bits: for enumerability and writability.
For ES4, shouldn't this function also take a type binding?
Ergo, let's assume that we are not finessing more than we have to, and dynamic properties have all these attribute bits. I don't know why createProperty should not be able to set all of these.
Neither can I.
(They're not independent, ReadOnly implies DontDelete.)
createProperty should throw an exception (TypeError?) if the property already exists on the object or would shadow a ReadOnly property, a la [[CanPut]], or if the object is not dynamic. It should probably throw an exception if its arguments are not consistent (ReadOnly && !DontDelete).
If ReadOnly is specified, is there even a reason to look at DontDelete?
Pesonally I like the strings approach, but the only interface among these four that has good compile-time checking is the first one, so I'm going to propose that we use that one, with dontEnum as the first flag, dontDelete as the second, and readOnly as the third (based on a guess about the frequency of use). Thus on Object.prototype and as an intrinsic propety on Object instances:
function createProperty(name:EnumerableId, dontEnum:boolean=false, dontDelete:boolean=false, readOnly:boolean=false): void
ReadOnly would need to be instanciated at the same time, no? And you probably want to be able to specify a type binding for ES4.
- The above does not preclude complementary declarative mechanisms in ES4 (but not in ES3.1 obviously)
Good to hear, because I still want to see something that can be used in object literals and property declarations.
-----Original Message----- From: es4-discuss-bounces at mozilla.org [mailto:es4-discuss-bounces at mozilla.org] On Behalf Of liorean Sent: 13. mars 2008 14:31 To: es4-discuss at mozilla.org Subject: Re: Controlling DontEnum (was: ES4 draft: Object)
I still think it may be right that properties in non-public
namespaces should not be enumerated, but I also think that's
orthogonal to the discussion that's going on here, about dynamic properties and their attributes. We've pretty much decided (ticket #233 has some of the discussion) that for future compatibility
there
ought to be a difference between fixture properties and DontDelete "dynamic" properties. So dynamic properties have at least one attribute bit like they do in ES3 (for deletability).
Consequently,
they might as well have all the ES3 bits: for enumerability and writability.
For ES4, shouldn't this function also take a type binding?
Let people who really want to use type-constrained properties migrate to a class, or find some way to use an object literal with a structural type.
At present the language does not allow dynamic, typed properties, unless I'm mistaken. (The 'const' and 'var' keywords in object literals introduce fixtures.) I'm not saying they can't be useful, but we're now far afield from controlling 'dontEnum'.
(They're not independent, ReadOnly implies DontDelete.)
createProperty should throw an exception (TypeError?) if the property already exists on the object or would shadow a ReadOnly property, a la [[CanPut]], or if the object is not dynamic. It should probably throw an exception if its arguments are not consistent (ReadOnly && !DontDelete).
If ReadOnly is specified, is there even a reason to look at DontDelete?
I think it's polite to notify the programmer that he's done something silly and I find special case "smart" rules to be annoying unless they solve real problems (and I don't see this as one of those). In this case it's not a deeply held conviction on my part, just the way I see it.
function createProperty(name:EnumerableId, dontEnum:boolean=false, dontDelete:boolean=false, readOnly:boolean=false): void
ReadOnly would need to be instanciated at the same time, no?
I assume you're referring to my having left the value argument out by mistake (second argument).
Or was there something else?
On Mar 13, 2008, at 11:47 AM, Lars Hansen wrote:
function createProperty(name:EnumerableId, dontEnum:boolean=false, dontDelete:boolean=false, readOnly:boolean=false): void
I like where this is going. May I (in addition to Kris's suggestion
for adding a value parameter) kindly nit on the parameter names?
function createProperty(name:EnumerableId, value:*, enumerable:boolean=true, removable:boolean=true, writable:boolean=true): void
Or some reasonable variant therein? I have no strong opinions on the
form the parameters eventually take, but trying to parse what
dontenum=false means tends to give me headaches. ;)
[mailto:es4-discuss-bounces at mozilla.org] On Behalf Of liorean ReadOnly would need to be instanciated at the same time, no?
On 13/03/2008, Lars Hansen <lhansen at adobe.com> wrote:
I assume you're referring to my having left the value argument out by mistake (second argument).
Or was there something else?
Value and type, though you addressed types earlier in your reply.
-----Original Message----- From: Neil Mix [mailto:nmix at pandora.com] Sent: 13. mars 2008 14:47 To: Lars Hansen Cc: es4-discuss at mozilla.org Subject: Re: Controlling DontEnum (was: ES4 draft: Object)
On Mar 13, 2008, at 11:47 AM, Lars Hansen wrote:
function createProperty(name:EnumerableId, dontEnum:boolean=false, dontDelete:boolean=false, readOnly:boolean=false): void
I like where this is going. May I (in addition to Kris's suggestion for adding a value parameter) kindly nit on the parameter names?
function createProperty(name:EnumerableId, value:*, enumerable:boolean=true, removable:boolean=true, writable:boolean=true): void
Or some reasonable variant therein? I have no strong opinions on the form the parameters eventually take, but trying to parse what dontenum=false means tends to give me headaches. ;)
Well, it had to come up at some point ;)
I suspect what you're proposing is the better UI. Obviously JS1/ES3 shows a bias for enumerable, removable, writable properties -- the attribute bits flag exceptions from the general rule. The more general design has attribute bits that simply control those property aspects, and the less biased names feel like an improvement.
Brendan, opinions?
On Mar 13, 2008, at 2:07 PM, Lars Hansen wrote:
-----Original Message----- From: Neil Mix [mailto:nmix at pandora.com]
function createProperty(name:EnumerableId, value:*, enumerable:boolean=true, removable:boolean=true, writable:boolean=true): void
Or some reasonable variant therein? I have no strong opinions on the form the parameters eventually take, but trying to parse what dontenum=false means tends to give me headaches. ;)
Well, it had to come up at some point ;)
I suspect what you're proposing is the better UI. Obviously JS1/ES3 shows a bias for enumerable, removable, writable properties -- the attribute bits flag exceptions from the general rule. The more
general design has attribute bits that simply control those property aspects, and the less biased names feel like an improvement.Brendan, opinions?
Neil's names are much better. For the record, I didn't come up with
DontDelete and DontEnum in ES1 daze. SpiderMonkey internally uses
PERMANENT for the former and ENUMERATE for the inverse of the latter
(since the native-biased API finds callers wanting enumerable pre-
defined properties to be the exception).
This feels like convergence. I'll hook this into the RI and write it up in the next couple of days (barring further discussion, of course).
From: Neil Mix [mailto:nmix at pandora.com]
function createProperty(name:EnumerableId, value:*, enumerable:boolean=true, removable:boolean=true, writable:boolean=true): void
On Thu, Mar 13, 2008 at 2:21 PM, Lars Hansen <lhansen at adobe.com> wrote:
This feels like convergence. I'll hook this into the RI and write it up in the next couple of days (barring further discussion, of course).
I've been following this discussion and am quite happy with the overall direction! With these changes, ES3.1 should be a much friendlier base to subset into a Caja-like secure language. I'm hopeful that the resulting Caja-like language could safely be a much larger subset of ES3.1 than with current Caja vs ES3. I haven't yet thought these issues through well, but I'll offer some half baked suggestions for now.
-
As someone pointed out, using positional boolean flag parameters (..., false, true, false, ...) makes call-site readability too hard. This point was raised earlier but seems to have been dropped. IIRC, all the other parameterization suggestions had better call site readability. My favorite was simply a list of strings. OTOH, an advantage of the bit-mask approach is that sensible combinations of flags can be named and used simply.
-
I love Neil's suggestion of moving towards names saying what's allowed, rather than saying what's denied. I'd like to go further and suggest that createProperty adopt the security best-practice of default-deny: When a property is explicitly created by this mechanism, only those operations that are explicitly allowed are permitted. All others attributes default to denying permission. To maintain compatibility of course, a property created the old fashioned way is implicitly fully permissive.
-
As Brendan reminds us, an erroneous createProperty request should throw rather than failing silently or merely returning false. And a property access that fails because createProperty did not allow it should also fail with a throw rather than silently. If we want to be strictly legacy compatible, then these throws should only happen for properties created with createProperty. Unfortunately, that means the semantics and implementations would need to distinguish, for example, legacy silently non-writable properties from new noisily non-writable properties. Can we instead specify that all these failures will be noisy?
-
Kris raises, we need to think about the interaction of these attributes with the notion of virtual properties defined using getters and setters. In particular, how does one create a non-removable virtual property? Should virtual properties have deleters in addition to getters and setters?
-
Might there be a sensible way to extend this mechanism to distinguish public from non-public properties? Based on the Caja design, might we adopt the rule that non-public property foo can only be addressed as this.foo or this['foo']. In other words, x.foo would only work is foo is public or unrestricted.
The general approach we're following here for properties is: default to legacy-compatible overly-permissive behavior, but allow restriction to a fail-stop subset of this behavior. We should prefer to make expressible those restrictions that contribute to reasoning about integrity. (This is the right criteria whether on not one is trying to define a secure subset.)
We should apply this general approach to objects as well as individual properties of objects. The most important, borrowed from ES4, is fixture vs dynamic. The Caja concept "frozen" then becomes exactly: fixture + all existing properties are non-writable and non-removable.
We can even use this approach to clean up the main source of confusion in ES3 while preserving compatibility: What is the intended behavior of a function? It would be great to explicitly restrict a function to be callable only as a function, or only as a method, or only as a constructor, or only as a final constructor. An unrestricted function could continue to be callable in all these ways, as with ES3 functions. Moving these restrictions into the language would clear up a major source of vulnerability in the current Caja design: confused deputy dangers at the taming boundary between cajoled code and uncajoled code. If there's interest, I can expand on this issue.
Speaking for myself, I should say from the outset that I am not in favor of taking this much further than it has been taken. The base language is ES3; we're trying to fix a small problem in that language in the cheapest possible way. I would rather backtrack to a solution that only fixes the DontEnum problem than to go forward into an elaborate design that attempts to fix various dodgy (or perceived as dodgy) features in the base language.
Getters and setters should not be an issue. There is no way in ES4 to add a getter or setter to an object after the fact; getters and setters defined by an object initializer or in a class are fixtures and hence not deletable.
(Some browsers have functions defineGetter and defineSetter that allow getters and setters to be added after the fact. Those are normally deletable. I suggest that these browsers expand their API to match that of createProperty.)
[Coming late to the party]
On 2008-03-13, at 12:47 EDT, Lars Hansen wrote:
function createProperty(name:EnumerableId, dontEnum:boolean=false, dontDelete:boolean=false, readOnly:boolean=false): void
If I did my math right there are 8 possible flag combinations, but
only 6 make sense. Why not a single parameter that is a member of an
enumeration? Maybe the enumeration values could be named mnemonically
to match the ES4 declarative syntax?
Here is a modest proposal based on Mark's and Maciej's messages:
Goal: A detectable method requiring no new syntax to set the DontEnum
attribute on an existing property.
Goal: Avoid breaking any existing ES3 code that might depend on a
plainly named method not being predefined.
Anti-goal: The ability to turn off DontEnum where it has been set on
predefined properties.
Proposal: Add Object.prototype.makePropertyNonEnumerable(name),
defined as if by:
Note that if this has a direct property identified by name, but this [name] already has the DontEnum attribute, then the function returns
true. The specification with prose of setting DontEnum would occur
redundantly in this case, but it is not observable in ES3 or ES4 as
proposed, so for simplicity I left out a test of DontEnum to avoid a
redundant set.
The use of a __-bracketed name satisfies the second goal, but it
would be a problem for Caja or similar. This is an issue where I
would appreciate Mark's feedback.
Comments welcome.