From fb1698fca5354ccac4fb338c468ca68f1fb463e6 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Wed, 9 Aug 2017 11:26:29 +0800 Subject: [PATCH] Rework interface analysis --- lib/constructs/interface.js | 340 ++++++++++++++++++++------------ lib/constructs/iterable.js | 10 +- lib/context.js | 3 +- lib/transformer.js | 27 +-- test/__snapshots__/test.js.snap | 105 ++++++---- test/cases/UnforgeableMap.idl | 2 +- 6 files changed, 309 insertions(+), 178 deletions(-) diff --git a/lib/constructs/interface.js b/lib/constructs/interface.js index 0656edbc..67f61626 100644 --- a/lib/constructs/interface.js +++ b/lib/constructs/interface.js @@ -10,34 +10,16 @@ const Overloads = require("../overloads"); const Parameters = require("../parameters"); const keywords = require("../keywords"); -function isPropGetter(idl) { - return idl.type === "operation" && idl.getter && Array.isArray(idl.arguments) && idl.arguments.length === 1; -} - -function isPropSetter(idl) { - return idl.type === "operation" && idl.setter && Array.isArray(idl.arguments) && idl.arguments.length === 2; -} - -function isPropDeleter(idl) { - return idl.type === "operation" && idl.deleter && Array.isArray(idl.arguments) && idl.arguments.length === 1; -} +// Used as a sentinel in inheritedMembers to signify that the following members are inherited. +const inherited = Symbol("inherited"); function isNamed(idl) { return idl.arguments[0].idlType.idlType === "DOMString"; } - function isIndexed(idl) { return idl.arguments[0].idlType.idlType === "unsigned long"; } -function isValueIterable(idl) { - return idl.type === "iterable" && !Array.isArray(idl.idlType); -} - -function isPairIterable(idl) { - return idl.type === "iterable" && Array.isArray(idl.idlType) && idl.idlType.length === 2; -} - class Interface { constructor(ctx, idl, opts) { this.ctx = ctx; @@ -50,28 +32,175 @@ class Interface { this.requires = new utils.RequiresMap(ctx); this.mixins = []; - this.indexedGetter = this.inheritedMembers.find(inner => isPropGetter(inner) && isIndexed(inner)); - this.indexedSetter = this.inheritedMembers.find(inner => isPropSetter(inner) && isIndexed(inner)); - this.namedGetter = this.inheritedMembers.find(inner => isPropGetter(inner) && isNamed(inner)); - this.namedSetter = this.inheritedMembers.find(inner => isPropSetter(inner) && isNamed(inner)); - this.namedDeleter = this.inheritedMembers.find(inner => isPropDeleter(inner) && isNamed(inner)); - } + this.operations = new Map(); + this.attributes = new Map(); + this.constants = new Map(); - get supportsIndexedProperties() { - return Boolean(this.indexedGetter); + this.indexedGetter = null; + this.indexedSetter = null; + this.namedGetter = null; + this.namedSetter = null; + this.namedDeleter = null; + + this.iterable = null; + this._analyzed = false; } - get supportsNamedProperties() { - return Boolean(this.namedGetter); + _analyzeMembers() { + let definingInterface = null; + for (const member of this.inheritedMembers()) { + if (member[0] === inherited) { + definingInterface = member[1]; + continue; + } + if (!definingInterface) { + switch (member.type) { + case "operation": { + let name = member.name; + if (name === null && member.stringifier) { + name = "toString"; + } + if (name !== null && !this.operations.has(name)) { + this.operations.set(name, new Operation(this.ctx, this, this.idl, member)); + } + break; + } + case "attribute": + this.attributes.set(member.name, new Attribute(this.ctx, this, this.idl, member)); + break; + case "const": + this.constants.set(member.name, new Constant(this.ctx, this, this.idl, member)); + break; + case "iterable": + if (this.iterable) { + throw new Error(`Interface ${this.name} has more than one iterable declaration`); + } + this.iterable = new Iterable(this.ctx, this, this.idl, member); + break; + default: + if (!this.ctx.options.suppressErrors) { + throw new Error(`Unknown IDL member type "${member.type}" in interface ${this.name}`); + } + } + } else { + switch (member.type) { + case "iterable": + if (this.iterable) { + throw new Error(`Iterable interface ${this.name} inherits from another iterable interface ` + + `${definingInterface}`); + } + break; + } + } + if (member.type === "operation") { + if (member.getter) { + let msg = `Invalid getter ${member.name ? `"${member.name}" ` : ""}on interface ${this.name}`; + if (definingInterface) { + msg += ` (defined in ${definingInterface})`; + } + msg += ": "; + if (member.arguments.length < 1 || + (!this.ctx.options.suppressErrors && member.arguments.length !== 1)) { + throw new Error(msg + `1 argument should be present, found ${member.arguments.length}`); + } + if (isIndexed(member)) { + if (!this.ctx.options.suppressErrors && this.indexedGetter) { + throw new Error(msg + "duplicated indexed getter"); + } + this.indexedGetter = member; + } else if (isNamed(member)) { + if (!this.ctx.options.suppressErrors && this.namedGetter) { + throw new Error(msg + "duplicated named getter"); + } + this.namedGetter = member; + } else { + throw new Error(msg + "getter is neither indexed nor named"); + } + } + if (member.setter) { + let msg = `Invalid setter ${member.name ? `"${member.name}" ` : ""}on interface ${this.name}`; + if (definingInterface) { + msg += ` (defined in ${definingInterface})`; + } + msg += ": "; + + if (member.arguments.length < 2 || + (!this.ctx.options.suppressErrors && member.arguments.length !== 2)) { + throw new Error(msg + `2 arguments should be present, found ${member.arguments.length}`); + } + if (isIndexed(member)) { + if (!this.ctx.options.suppressErrors && this.indexedSetter) { + throw new Error(msg + "duplicated indexed setter"); + } + this.indexedSetter = member; + } else if (isNamed(member)) { + if (!this.ctx.options.suppressErrors && this.namedSetter) { + throw new Error(msg + "duplicated named setter"); + } + this.namedSetter = member; + } else { + throw new Error(msg + "setter is neither indexed nor named"); + } + } + if (member.deleter) { + let msg = `Invalid deleter ${member.name ? `"${member.name}" ` : ""}on interface ${this.name}`; + if (definingInterface) { + msg += ` (defined in ${definingInterface})`; + } + msg += ": "; + + if (member.arguments.length < 1 || + (!this.ctx.options.suppressErrors && member.arguments.length !== 1)) { + throw new Error(msg + `1 arguments should be present, found ${member.arguments.length}`); + } + if (isNamed(member)) { + if (!this.ctx.options.suppressErrors && this.namedDeleter) { + throw new Error(msg + "duplicated named deleter"); + } + this.namedDeleter = member; + } else { + throw new Error(msg + "deleter is not named"); + } + } + } + } + + const forbiddenMembers = new Set(); + if (this.iterable) { + if (this.iterable.isValue) { + if (!this.supportsIndexedProperties) { + throw new Error(`A value iterator cannot be declared on ${this.name} which does not support indexed ` + + "properties"); + } + } else if (this.iterable.isPair && this.supportsIndexedProperties) { + throw new Error(`A pair iterator cannot be declared on ${this.name} which supports indexed properties`); + } + for (const n of ["entries", "forEach", "keys", "values"]) { + forbiddenMembers.add(n); + } + } + definingInterface = null; + for (const member of this.inheritedMembers()) { + if (member[0] === inherited) { + definingInterface = member[1]; + continue; + } + if (forbiddenMembers.has(member.name)) { + let msg = `${member.name} is forbidden in interface ${this.name}`; + if (definingInterface) { + msg += ` (defined in ${definingInterface})`; + } + throw new Error(msg); + } + } } - // TODO: Figure out how iterable<> declarations work with inheritance. - get hasValueIterator() { - return this.supportsIndexedProperties && this.idl.members.some(isValueIterable); + get supportsIndexedProperties() { + return this.indexedGetter !== null; } - get hasPairIterator() { - return !this.supportsIndexedProperties && this.idl.members.some(isPairIterable); + get supportsNamedProperties() { + return this.namedGetter !== null; } get isLegacyPlatformObj() { @@ -83,7 +212,7 @@ class Interface { } generateIterator() { - if (this.hasPairIterator) { + if (this.iterable && this.iterable.isPair) { this.str += ` const IteratorPrototype = Object.create(utils.IteratorPrototype, { next: { @@ -194,12 +323,14 @@ class Interface { `; } - get inheritedMembers() { - if (!this.idl.inheritance || !this.ctx.interfaces.has(this.idl.inheritance)) { - return [...this.idl.members]; + * inheritedMembers() { + yield* this.idl.members; + for (const iface of [...this.mixins, this.idl.inheritance]) { + if (this.ctx.interfaces.has(iface)) { + yield [inherited, iface]; + yield* this.ctx.interfaces.get(iface).inheritedMembers(); + } } - const parent = this.ctx.interfaces.get(this.idl.inheritance); - return [...this.idl.members, ...parent.inheritedMembers]; } generateRequires() { @@ -271,7 +402,7 @@ class Interface { }, `; - if (this.hasPairIterator) { + if (this.iterable && this.iterable.isPair) { this.str += ` createDefaultIterator(target, kind) { const iterator = Object.create(IteratorPrototype); @@ -288,9 +419,9 @@ class Interface { } generateLegacyProxy() { - const hasIndexedSetter = this.indexedSetter !== undefined; - const hasNamedSetter = this.namedSetter !== undefined; - const hasNamedDeleter = this.namedDeleter !== undefined; + const hasIndexedSetter = this.indexedSetter !== null; + const hasNamedSetter = this.namedSetter !== null; + const hasNamedDeleter = this.namedDeleter !== null; const overrideBuiltins = Boolean(utils.getExtAttr(this.idl.extAttrs, "OverrideBuiltins")); const supportsPropertyIndex = (O, index, indexedValue) => { @@ -304,7 +435,7 @@ class Interface { return `${value} !== ${unsupportedValue}`; } return `${O}[impl][utils.supportsPropertyIndex](${index})`; - } + }; const supportsPropertyName = (O, P, namedValue) => { let unsupportedValue = utils.getExtAttr(this.namedGetter.extAttrs, "WebIDL2JSValueAsUnsupported"); @@ -317,7 +448,7 @@ class Interface { return `${value} !== ${unsupportedValue}`; } return `${O}[impl][utils.supportsPropertyName](${P})`; - } + }; // "named property visibility algorithm" // If `supports` is true then skip the supportsPropertyName check. @@ -688,10 +819,19 @@ class Interface { } let needFallback = false; if (this.supportsNamedProperties && !utils.isGlobal(this.idl)) { - const unforgeable = this.idl.members.filter(m => utils.getExtAttr(m.extAttrs, "Unforgeable")).map(m => m.name); - if (unforgeable.length > 0) { + const unforgeable = new Set(); + for (const m of this.inheritedMembers()) { + if (m[0] === inherited) { + continue; + } + if ((m.type === "attribute" || m.type === "operation") && !m.static && + utils.getExtAttr(m.extAttrs, "Unforgeable")) { + unforgeable.add(m.name); + } + } + if (unforgeable.size > 0) { needFallback = true; - this.str += `if (!${JSON.stringify(unforgeable)}.includes(P)) {`; + this.str += `if (!${JSON.stringify([...unforgeable])}.includes(P)) {`; } if (!overrideBuiltins) { needFallback = true; @@ -719,7 +859,7 @@ class Interface { if (!overrideBuiltins) { this.str += "}"; } - if (unforgeable.length > 0) { + if (unforgeable.size > 0) { this.str += "}"; } } else { @@ -826,8 +966,6 @@ class Interface { exposers.push(keys[i] + ": { " + exposedMap[keys[i]].join(", ") + " }"); } - // since we don't have spread arg calls, we can't do new Interface(...arguments) yet - // add initialized symbol as to not destroy the object shape and cause deopts this.str += ` create(constructorArgs, privateData) { let obj = Object.create(${this.name}.prototype); @@ -848,25 +986,11 @@ class Interface { `; } - for (let i = 0; i < this.idl.members.length; ++i) { - const memberIdl = this.idl.members[i]; - if (utils.isOnInstance(memberIdl, this.idl)) { - let member; - switch (memberIdl.type) { - case "operation": { - member = new Operation(this.ctx, this, this.idl, memberIdl); - break; - } - case "attribute": { - member = new Attribute(this.ctx, this, this.idl, memberIdl); - break; - } - default: { - throw new Error("Cannot handle on-instance members that are not operations or attributes"); - } - } - - this.str += member.generate().body; + for (const member of [...this.operations.values(), ...this.attributes.values()]) { + if (utils.isOnInstance(member.idl, this.idl)) { + const data = member.generate(); + this.requires.merge(data.requires); + this.str += data.body; } } @@ -920,7 +1044,7 @@ class Interface { // TODO maplike setlike // Don't bother checking "length" attribute as interfaces that support indexed properties must implement one. // "Has value iterator" implies "supports indexed properties". - if (this.supportsIndexedProperties || this.hasPairIterator) { + if (this.supportsIndexedProperties || this.iterable && this.iterable.isPair) { let expr; if (this.supportsIndexedProperties) { @@ -946,10 +1070,10 @@ class Interface { `; } - if (this.hasValueIterator || this.hasPairIterator) { + if (this.iterable) { let expr; - if (this.hasValueIterator) { + if (this.iterable.isValue) { expr = "Array.prototype.forEach"; } else { expr = ` @@ -981,64 +1105,28 @@ class Interface { this.str += `${this.name}.prototype.forEach = ${expr};`; } - const done = {}; - - for (let i = 0; i < this.idl.members.length; ++i) { - const memberIdl = this.idl.members[i]; - let member = null; - - switch (memberIdl.type) { - case "operation": - if (utils.isOnInstance(memberIdl, this.idl)) { - break; - } - member = new Operation(this.ctx, this, this.idl, memberIdl); - if (done[member.name]) { - continue; - } - done[member.name] = true; - break; - case "iterable": - member = new Iterable(this.ctx, this, this.idl, memberIdl); - break; - default: - // throw new Error("Can't handle member of type '" + memberIdl.type + "'"); - break; - } - - if (member !== null) { + for (const member of this.operations.values()) { + if (!utils.isOnInstance(member.idl, this.idl)) { const data = member.generate(); this.requires.merge(data.requires); this.str += data.body; } } + if (this.iterable) { + const data = this.iterable.generate(); + this.requires.merge(data.requires); + this.str += data.body; + } } generateAttributes() { - for (let i = 0; i < this.idl.members.length; ++i) { - const memberIdl = this.idl.members[i]; - let member = null; - - switch (memberIdl.type) { - case "attribute": - if (utils.isOnInstance(memberIdl, this.idl)) { - break; - } - member = new Attribute(this.ctx, this, this.idl, memberIdl); - break; - case "const": - member = new Constant(this.ctx, this, this.idl, memberIdl); - break; - default: - // throw new Error("Can't handle member of type '" + memberIdl.type + "'"); - break; - } - - if (member !== null) { - const data = member.generate(); - this.requires.merge(data.requires); - this.str += data.body; + for (const member of [...this.attributes.values(), ...this.constants.values()]) { + if (member instanceof Attribute && utils.isOnInstance(member.idl, this.idl)) { + continue; } + const data = member.generate(); + this.requires.merge(data.requires); + this.str += data.body; } } @@ -1117,6 +1205,10 @@ class Interface { toString() { this.str = ""; + if (!this._analyzed) { + this._analyzed = true; + this._analyzeMembers(); + } this.generate(); return this.str; } diff --git a/lib/constructs/iterable.js b/lib/constructs/iterable.js index d3ecd041..24e30c74 100644 --- a/lib/constructs/iterable.js +++ b/lib/constructs/iterable.js @@ -12,6 +12,14 @@ class Iterable { this.name = idl.type; } + get isValue() { + return !Array.isArray(this.idl.idlType); + } + + get isPair() { + return Array.isArray(this.idl.idlType) && this.idl.idlType.length === 2; + } + generateFunction(key, kind, keyExpr, fnName) { if (fnName === undefined) { if (typeof key === "symbol") { @@ -36,7 +44,7 @@ class Iterable { generate() { let str = ""; - if (this.obj.hasPairIterator) { + if (this.isPair) { str += ` ${this.obj.name}.prototype.entries = ${this.obj.name}.prototype[Symbol.iterator]; ${this.generateFunction("keys", "key")} diff --git a/lib/context.js b/lib/context.js index 00c60281..68079e87 100644 --- a/lib/context.js +++ b/lib/context.js @@ -11,8 +11,9 @@ const builtinTypedefs = webidl.parse(` `); class Context { - constructor({ implSuffix = "" } = {}) { + constructor({ implSuffix = "", options } = {}) { this.implSuffix = implSuffix; + this.options = options; this.initialize(); } diff --git a/lib/transformer.js b/lib/transformer.js index 4d43fccf..2a3b0161 100644 --- a/lib/transformer.js +++ b/lib/transformer.js @@ -15,13 +15,14 @@ const Dictionary = require("./constructs/dictionary"); class Transformer { constructor(opts = {}) { this.ctx = new Context({ - implSuffix: opts.implSuffix + implSuffix: opts.implSuffix, + options: { + suppressErrors: Boolean(opts.suppressErrors) + } }); - this.options = Object.assign({ - suppressErrors: false - }, opts); this.sources = []; + this.utilPath = null; } addSource(idl, impl) { @@ -112,7 +113,7 @@ class Transformer { typedefs.set(obj.name, obj); break; default: - if (!this.options.suppressErrors) { + if (!this.ctx.options.suppressErrors) { throw new Error("Can't convert type '" + instruction.type + "'"); } } @@ -130,7 +131,7 @@ class Transformer { break; } - if (this.options.suppressErrors && !interfaces.has(instruction.name)) { + if (this.ctx.options.suppressErrors && !interfaces.has(instruction.name)) { break; } oldMembers = interfaces.get(instruction.name).idl.members; @@ -142,7 +143,7 @@ class Transformer { if (!instruction.partial) { break; } - if (this.options.suppressErrors && !dictionaries.has(instruction.name)) { + if (this.ctx.options.suppressErrors && !dictionaries.has(instruction.name)) { break; } oldMembers = dictionaries.get(instruction.name).idl.members; @@ -151,7 +152,7 @@ class Transformer { extAttrs.push(...instruction.extAttrs); break; case "implements": - if (this.options.suppressErrors && !interfaces.has(instruction.target)) { + if (this.ctx.options.suppressErrors && !interfaces.has(instruction.target)) { break; } interfaces.get(instruction.target).implements(instruction.implements); @@ -163,7 +164,7 @@ class Transformer { * _writeFiles(outputDir) { const utilsText = yield fs.readFile(path.resolve(__dirname, "output/utils.js")); - yield fs.writeFile(this.options.utilPath, utilsText); + yield fs.writeFile(this.utilPath, utilsText); const { interfaces, dictionaries } = this.ctx; @@ -176,7 +177,7 @@ class Transformer { implFile = "./" + implFile; } - let relativeUtils = path.relative(outputDir, this.options.utilPath).replace(/\\/g, "/"); + let relativeUtils = path.relative(outputDir, this.utilPath).replace(/\\/g, "/"); if (relativeUtils[0] !== ".") { relativeUtils = "./" + relativeUtils; } @@ -198,7 +199,7 @@ class Transformer { for (const obj of dictionaries.values()) { let source = obj.toString(); - let relativeUtils = path.relative(outputDir, this.options.utilPath).replace(/\\/g, "/"); + let relativeUtils = path.relative(outputDir, this.utilPath).replace(/\\/g, "/"); if (relativeUtils[0] !== ".") { relativeUtils = "./" + relativeUtils; } @@ -224,8 +225,8 @@ class Transformer { } generate(outputDir) { - if (!this.options.utilPath) { - this.options.utilPath = path.join(outputDir, "utils.js"); + if (!this.utilPath) { + this.utilPath = path.join(outputDir, "utils.js"); } return co(function* () { diff --git a/test/__snapshots__/test.js.snap b/test/__snapshots__/test.js.snap index cec22575..8c992727 100644 --- a/test/__snapshots__/test.js.snap +++ b/test/__snapshots__/test.js.snap @@ -3080,6 +3080,14 @@ URLSearchParams.prototype.sort = function sort() { return this[impl].sort(); }; +URLSearchParams.prototype.toString = function toString() { + if (!this || !module.exports.is(this)) { + throw new TypeError(\\"Illegal invocation\\"); + } + + return this[impl].toString(); +}; + URLSearchParams.prototype.entries = URLSearchParams.prototype[Symbol.iterator]; URLSearchParams.prototype.keys = function keys() { @@ -3096,14 +3104,6 @@ URLSearchParams.prototype.values = function values() { return module.exports.createDefaultIterator(this, \\"value\\"); }; -URLSearchParams.prototype.toString = function toString() { - if (!this || !module.exports.is(this)) { - throw new TypeError(\\"Illegal invocation\\"); - } - - return this[impl].toString(); -}; - Object.defineProperty(URLSearchParams.prototype, Symbol.toStringTag, { value: \\"URLSearchParams\\", writable: false, @@ -3970,6 +3970,32 @@ const iface = { return utils.implForWrapper(obj); }, _internalSetup(obj) { + obj.assign = function assign(url) { + if (!this || !module.exports.is(this)) { + throw new TypeError(\\"Illegal invocation\\"); + } + + if (arguments.length < 1) { + throw new TypeError( + \\"Failed to execute 'assign' on 'Unforgeable': 1 \\" + + \\"argument required, but only \\" + + arguments.length + + \\" present.\\" + ); + } + + const args = []; + for (let i = 0; i < arguments.length && i < 1; ++i) { + args[i] = arguments[i]; + } + + args[0] = conversions[\\"USVString\\"](args[0], { + context: \\"Failed to execute 'assign' on 'Unforgeable': parameter 1\\" + }); + + return this[impl].assign(...args); + }; + Object.defineProperty(obj, \\"href\\", { get() { if (!this || !module.exports.is(this)) { @@ -4044,32 +4070,6 @@ const iface = { enumerable: true, configurable: false }); - - obj.assign = function assign(url) { - if (!this || !module.exports.is(this)) { - throw new TypeError(\\"Illegal invocation\\"); - } - - if (arguments.length < 1) { - throw new TypeError( - \\"Failed to execute 'assign' on 'Unforgeable': 1 \\" + - \\"argument required, but only \\" + - arguments.length + - \\" present.\\" - ); - } - - const args = []; - for (let i = 0; i < arguments.length && i < 1; ++i) { - args[i] = arguments[i]; - } - - args[0] = conversions[\\"USVString\\"](args[0], { - context: \\"Failed to execute 'assign' on 'Unforgeable': parameter 1\\" - }); - - return this[impl].assign(...args); - }; }, setup(obj, constructorArgs, privateData) { if (!privateData) privateData = {}; @@ -4264,7 +4264,7 @@ const iface = { const namedValue = target[impl][utils.namedGet](P); return { - writable: false, + writable: true, enumerable: true, configurable: true, value: utils.tryWrapperForImpl(namedValue) @@ -4279,7 +4279,22 @@ const iface = { return Reflect.set(target, P, V, receiver); } if (target === receiver) { - typeof P === \\"string\\" && !utils.isArrayIndexPropName(P); + if (typeof P === \\"string\\" && !utils.isArrayIndexPropName(P)) { + let namedValue = V; + + namedValue = conversions[\\"DOMString\\"](namedValue, { + context: \\"Failed to set the '\\" + P + \\"' property on 'UnforgeableMap': The provided value\\" + }); + + const creating = !target[impl][utils.supportsPropertyName](P); + if (creating) { + target[impl][utils.namedSetNew](P, namedValue); + } else { + target[impl][utils.namedSetExisting](P, namedValue); + } + + return true; + } } let ownDesc; @@ -4321,10 +4336,24 @@ const iface = { } if (![\\"a\\"].includes(P)) { if (!Object.prototype.hasOwnProperty.call(target, P)) { - const creating = !target[impl][utils.supportsPropertyName](P); - if (!creating) { + if (desc.get || desc.set) { return false; } + + let namedValue = desc.value; + + namedValue = conversions[\\"DOMString\\"](namedValue, { + context: \\"Failed to set the '\\" + P + \\"' property on 'UnforgeableMap': The provided value\\" + }); + + const creating = !target[impl][utils.supportsPropertyName](P); + if (creating) { + target[impl][utils.namedSetNew](P, namedValue); + } else { + target[impl][utils.namedSetExisting](P, namedValue); + } + + return true; } } return Reflect.defineProperty(target, P, desc); diff --git a/test/cases/UnforgeableMap.idl b/test/cases/UnforgeableMap.idl index 233d0b2e..c9ba22d2 100644 --- a/test/cases/UnforgeableMap.idl +++ b/test/cases/UnforgeableMap.idl @@ -1,5 +1,5 @@ interface UnforgeableMap { [Unforgeable] readonly attribute DOMString a; getter DOMString (DOMString x); - setter DOMString (DOMString x); + setter DOMString (DOMString x, DOMString y); };