-
Notifications
You must be signed in to change notification settings - Fork 80
/
Copy pathconsistency.js
334 lines (302 loc) · 13.2 KB
/
consistency.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
const assert = require('assert').strict;
const idl = require('@webref/idl');
// Helper to get a map of unique definitions in |dfns|, for convenience and to
// avoid O(n^2) in the common `for (dfn of dfns) { dfns.find(...) }`.
function nameMap(dfns) {
assert(Array.isArray(dfns));
const map = new Map();
for (const dfn of dfns) {
assert(!map.has(dfn.name), `duplicate definition of ${dfn.name}`);
map.set(dfn.name, dfn);
}
return map;
}
function getExtAttr(node, name) {
return node.extAttrs && node.extAttrs.find((attr) => attr.name === name);
}
// Helper to test if two members define the same thing, such as the same
// attribute or the same method. Should match requirements in spec:
// https://heycam.github.io/webidl/#idl-overloading
function isOverloadedOperation(a, b) {
if (a.type !== 'constructor' && a.type !== 'operation') {
return false;
}
if (a.type !== b.type) {
return false;
}
// Note that |name| or |special| could be null/undefined, but even then
// they have to be the same for both members.
if (a.name !== b.name) {
return false;
}
if (a.special !== b.special) {
return false;
}
return true;
}
function describeDfn(dfn) {
let desc = dfn.type;
if (dfn.name) {
desc += ' ' + dfn.name;
}
if (dfn.partial) {
desc = 'partial ' + desc;
}
return desc;
}
function describeMember(member) {
let desc = member.type;
if (member.name) {
desc += ' ' + member.name;
}
if (member.special) {
desc = member.special + ' ' + desc;
}
return desc;
}
function mergeMembers(target, source) {
// Check for overloaded operation across partials/mixins. This is O(n^2) and
// could be improved, but it's fast enough (and simple) for testing.
for (const targetMember of target.members) {
for (const sourceMember of source.members) {
assert(!isOverloadedOperation(targetMember, sourceMember),
`invalid overload of ${describeMember(targetMember)} from ${describeDfn(source)}`);
}
}
// Now merge members.
target.members.push(...source.members);
}
// This could be useful part of the public API, but for now just run the
// merging in order to find problems. Modifies some definitions in place,
// and returns a list of only the merged definitions.
function merge(dfns, partials, includes) {
assert(Array.isArray(dfns));
assert(Array.isArray(partials));
assert(Array.isArray(includes));
dfns = nameMap(dfns); // replace |dfns| to avoid using it
// merge partials (including partial mixins)
for (const partial of partials) {
const target = dfns.get(partial.name);
assert(target, `target definition of partial ${partial.type} ${partial.name} not found`);
assert.equal(partial.type, target.type, `${partial.name} inherits from wrong type: ${target.type}`);
// TODO: account for extended attributes on the partial definition
mergeMembers(target, partial);
}
// mix in the mixins
for (const include of includes) {
assert(include.target);
const target = dfns.get(include.target);
assert(target, `missing target of includes statement: ${include.target}`);
assert.equal(target.type, 'interface');
assert(include.includes);
const mixin = dfns.get(include.includes);
assert(mixin, `missing source of includes statement: ${include.includes}`);
assert.equal(mixin.type, 'interface mixin');
mergeMembers(target, mixin);
}
// remove all mixins, whether used or not
for (const [name, dfn] of dfns) {
if (dfn.type === 'interface mixin') {
dfns.delete(name);
}
}
// now check for duplicate members
for (const dfn of dfns.values()) {
if (!dfn.members) {
continue;
}
const namedMembers = new Map();
for (const member of dfn.members) {
if (!member.name) {
continue;
}
const firstMember = namedMembers.get(member.name);
if (firstMember) {
// Overloaded operations are OK.
if (isOverloadedOperation(member, firstMember)) {
continue;
}
// Non-overlapping exposure sets are OK. Assume it's OK if either
// members has an [Exposed] extended attribute. TODO: do better.
if (getExtAttr(firstMember, 'Exposed') || getExtAttr(member, 'Exposed')) {
continue;
}
assert.fail(`duplicate definition of ${dfn.name} member ${member.name}`);
} else {
namedMembers.set(member.name, member);
}
}
}
// finally return a sorted list of merged definitions
return Array.from(dfns.values());
}
describe('Web IDL consistency', () => {
const dfns = [];
const includes = [];
const partials = [];
before(async () => {
const all = await idl.parseAll();
for (const [spec, ast] of Object.entries(all)) {
for (const dfn of ast) {
if (dfn.partial) {
partials.push(dfn);
} else if (dfn.type === 'includes') {
includes.push(dfn);
} else if (dfn.name) {
dfns.push(dfn);
} else {
assert.fail(`unknown definition in ${spec}: ${JSON.stringify(dfn)}`);
}
}
}
});
it('unique definitions', () => {
assert.equal(nameMap(dfns).size, dfns.length);
});
it('inheritance', () => {
const map = nameMap(dfns);
for (const dfn of map.values()) {
if (dfn.inheritance) {
const parent = map.get(dfn.inheritance);
assert(parent, `${dfn.name} inherits from missing type: ${dfn.inheritance}`);
assert.equal(dfn.type, parent.type, `${dfn.name} inherits from wrong type: ${parent.type}`);
}
}
});
// Validate that there are no unknown types or extended attributes.
it('all used types and extended attributes are defined', () => {
// There are types in lots of places in the AST (interface members,
// arguments, return types) and rather than trying to cover them all, walk
// the whole AST looking for "idlType".
const usedTypes = new Set();
const usedExtAttrs = new Set();
// Serialize and reparse the ast to not have to worry about own properties
// vs enumerable properties on the prototypes, etc.
const pending = [JSON.parse(JSON.stringify([...dfns, ...partials]))];
while (pending.length) {
const node = pending.pop();
for (const [key, value] of Object.entries(node)) {
if (key === 'idlType' && typeof value === 'string') {
usedTypes.add(value);
} else if (key === 'extAttrs' && Array.isArray(value)) {
for (const extAttr of value) {
usedExtAttrs.add(extAttr.name);
}
} else if (typeof value === 'object' && value !== null) {
pending.push(value);
}
}
}
const knownTypes = new Set([
// Types defined by Web IDL itself:
'any', // https://heycam.github.io/webidl/#idl-any
'ArrayBuffer', // https://heycam.github.io/webidl/#idl-ArrayBuffer
'bigint', // https://heycam.github.io/webidl/#idl-bigint
'boolean', // https://heycam.github.io/webidl/#idl-boolean
'byte', // https://heycam.github.io/webidl/#idl-byte
'ByteString', // https://heycam.github.io/webidl/#idl-ByteString
'DataView', // https://heycam.github.io/webidl/#idl-DataView
'DOMString', // https://heycam.github.io/webidl/#idl-DOMString
'double', // https://heycam.github.io/webidl/#idl-double
'float', // https://heycam.github.io/webidl/#idl-float
'Float32Array', // https://heycam.github.io/webidl/#idl-Float32Array
'Float64Array', // https://heycam.github.io/webidl/#idl-Float64Array
'Int16Array', // https://heycam.github.io/webidl/#idl-Int16Array
'Int32Array', // https://heycam.github.io/webidl/#idl-Int32Array
'Int8Array', // https://heycam.github.io/webidl/#idl-Int8Array
'long long', // https://heycam.github.io/webidl/#idl-long-long
'long', // https://heycam.github.io/webidl/#idl-long
'object', // https://heycam.github.io/webidl/#idl-object
'octet', // https://heycam.github.io/webidl/#idl-octet
'short', // https://heycam.github.io/webidl/#idl-short
'symbol', // https://heycam.github.io/webidl/#idl-symbol
'Uint16Array', // https://heycam.github.io/webidl/#idl-Uint16Array
'Uint32Array', // https://heycam.github.io/webidl/#idl-Uint32Array
'Uint8Array', // https://heycam.github.io/webidl/#idl-Uint8Array
'Uint8ClampedArray', // https://heycam.github.io/webidl/#idl-Uint8ClampedArray
'unrestricted double', // https://heycam.github.io/webidl/#idl-unrestricted-double
'unrestricted float', // https://heycam.github.io/webidl/#idl-unrestricted-float
'unsigned long long', // https://heycam.github.io/webidl/#idl-unsigned-long-long
'unsigned long', // https://heycam.github.io/webidl/#idl-unsigned-long
'unsigned short', // https://heycam.github.io/webidl/#idl-unsigned-short
'USVString', // https://heycam.github.io/webidl/#idl-USVString
'undefined', // https://heycam.github.io/webidl/#idl-undefined
// Types defined by other specs:
'CSSOMString', // https://drafts.csswg.org/cssom/#cssomstring-type
'WindowProxy' // https://html.spec.whatwg.org/multipage/window-object.html#windowproxy
]);
const knownExtAttrs = new Set([
// Extended attributes defined by Web IDL itself:
'AllowShared', // https://heycam.github.io/webidl/#AllowShared
'Clamp', // https://heycam.github.io/webidl/#Clamp
'CrossOriginIsolated', // https://heycam.github.io/webidl/#CrossOriginIsolated
'Default', // https://heycam.github.io/webidl/#Default
'EnforceRange', // https://heycam.github.io/webidl/#EnforceRange
'Exposed', // https://heycam.github.io/webidl/#Exposed
'Global', // https://heycam.github.io/webidl/#Global
'LegacyFactoryFunction', // https://heycam.github.io/webidl/#LegacyFactoryFunction
'LegacyLenientSetter', // https://heycam.github.io/webidl/#LegacyLenientSetter
'LegacyLenientThis', // https://heycam.github.io/webidl/#LegacyLenientThis
'LegacyNamespace', // https://heycam.github.io/webidl/#LegacyNamespace
'LegacyNoInterfaceObject', // https://heycam.github.io/webidl/#LegacyNoInterfaceObject
'LegacyNullToEmptyString', // https://heycam.github.io/webidl/#LegacyNullToEmptyString
'LegacyOverrideBuiltIns', // https://heycam.github.io/webidl/#LegacyOverrideBuiltIns
'LegacyTreatNonObjectAsNull', // https://heycam.github.io/webidl/#LegacyTreatNonObjectAsNull
'LegacyUnenumerableNamedProperties', // https://heycam.github.io/webidl/#LegacyUnenumerableNamedProperties
'LegacyUnforgeable', // https://heycam.github.io/webidl/#LegacyUnforgeable
'LegacyWindowAlias', // https://heycam.github.io/webidl/#LegacyWindowAlias
'NewObject', // https://heycam.github.io/webidl/#NewObject
'PutForwards', // https://heycam.github.io/webidl/#PutForwards
'Replaceable', // https://heycam.github.io/webidl/#Replaceable
'SameObject', // https://heycam.github.io/webidl/#SameObject
'SecureContext', // https://heycam.github.io/webidl/#SecureContext
'Unscopable', // https://heycam.github.io/webidl/#Unscopable
// Extended attributes defined by other specs:
'CEReactions', // https://html.spec.whatwg.org/multipage/custom-elements.html#cereactions
'HTMLConstructor', // https://html.spec.whatwg.org/multipage/dom.html#htmlconstructor
'Serializable', // https://html.spec.whatwg.org/multipage/structured-data.html#serializable
'StringContext', // https://w3c.github.io/webappsec-trusted-types/dist/spec/#webidl-string-context-xattr
'Transferable', // https://html.spec.whatwg.org/multipage/structured-data.html#transferable
'WebGLHandlesContextLoss' // https://www.khronos.org/registry/webgl/specs/latest/1.0/#5.14
]);
// Add any types defined by the parsed IDL, while also forbidding some that
// can be used mistakenly. Should match https://heycam.github.io/webidl/#idl-types.
const knownInvalidTypes = new Map();
for (const dfn of dfns) {
if (dfn.type === 'interface mixin' || dfn.type === 'namespace') {
knownInvalidTypes.set(dfn.name, dfn.type);
} else {
knownTypes.add(dfn.name);
}
}
for (const usedType of usedTypes) {
assert(!knownInvalidTypes.has(usedType),
`${knownInvalidTypes.get(usedType)} ${usedType} cannot be used as a type`);
assert(knownTypes.has(usedType), `type ${usedType} is used but never defined`);
}
for (const attr of usedExtAttrs) {
assert(knownExtAttrs.has(attr), `extended attribute ${attr} is used but never defined`);
}
});
// This test should remain the last one as it slightly modifies objects in dfns in place.
it('merging in partials/mixins', () => {
const merged = merge(dfns, partials, includes);
// To guard against new things being added to Web IDL which need special handling,
// such as dictionary mixins, check that the merged result has only known types.
// Also check that everything has a name and that no partials remain.
const knownTypes = new Set([
'callback interface',
'callback',
'dictionary',
'enum',
'interface',
'namespace',
'typedef'
]);
for (const dfn of merged) {
assert(dfn.name, 'definition has a name');
assert(!dfn.partial, 'definition is not partial');
assert(knownTypes.has(dfn.type), `unknown definition type: ${dfn.type}`)
}
});
});