reffy 6.2.0 → 6.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +158 -158
- package/index.js +11 -11
- package/package.json +53 -53
- package/reffy.js +248 -248
- package/src/browserlib/canonicalize-url.mjs +50 -50
- package/src/browserlib/create-outline.mjs +352 -352
- package/src/browserlib/extract-cssdfn.mjs +319 -319
- package/src/browserlib/extract-dfns.mjs +686 -686
- package/src/browserlib/extract-elements.mjs +205 -205
- package/src/browserlib/extract-headings.mjs +48 -48
- package/src/browserlib/extract-ids.mjs +28 -28
- package/src/browserlib/extract-links.mjs +28 -28
- package/src/browserlib/extract-references.mjs +203 -203
- package/src/browserlib/extract-webidl.mjs +134 -134
- package/src/browserlib/get-absolute-url.mjs +21 -21
- package/src/browserlib/get-generator.mjs +26 -26
- package/src/browserlib/get-lastmodified-date.mjs +13 -13
- package/src/browserlib/get-title.mjs +11 -11
- package/src/browserlib/informative-selector.mjs +16 -16
- package/src/browserlib/map-ids-to-headings.mjs +136 -136
- package/src/browserlib/reffy.json +53 -53
- package/src/cli/check-missing-dfns.js +609 -609
- package/src/cli/generate-idlnames.js +430 -430
- package/src/cli/generate-idlparsed.js +139 -139
- package/src/cli/merge-crawl-results.js +128 -128
- package/src/cli/parse-webidl.js +430 -430
- package/src/lib/css-grammar-parse-tree.schema.json +109 -109
- package/src/lib/css-grammar-parser.js +440 -440
- package/src/lib/fetch.js +55 -55
- package/src/lib/nock-server.js +119 -119
- package/src/lib/specs-crawler.js +605 -603
- package/src/lib/util.js +898 -898
- package/src/specs/missing-css-rules.json +197 -197
- package/src/specs/spec-equivalents.json +149 -149
- package/src/browserlib/extract-editors.mjs~ +0 -14
- package/src/browserlib/generate-es-dfn-report.sh~ +0 -4
- package/src/cli/csstree-grammar-check.js +0 -28
- package/src/cli/csstree-grammar-check.js~ +0 -10
- package/src/cli/csstree-grammar-parser.js +0 -11
- package/src/cli/csstree-grammar-parser.js~ +0 -1
- package/src/cli/extract-editors.js~ +0 -38
- package/src/cli/process-specs.js~ +0 -28
|
@@ -1,610 +1,610 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* The definitions checker compares CSS, dfns, and IDL extracts created by Reffy
|
|
4
|
-
* to detect CSS/IDL terms that do not have a corresponding dfn in the
|
|
5
|
-
* specification.
|
|
6
|
-
*
|
|
7
|
-
* The definitions checker can be called directly through:
|
|
8
|
-
*
|
|
9
|
-
* `node check-missing-dfns.js [crawl report] [spec] [format]`
|
|
10
|
-
*
|
|
11
|
-
* where:
|
|
12
|
-
* - `crawl report` is the local path to the root folder that contains the
|
|
13
|
-
* `index.json` and the extracts (e.g. `reports/ed`)
|
|
14
|
-
* - `spec` is the optional shortname of the specification on which to focus or
|
|
15
|
-
* `all` (default) to check all specs
|
|
16
|
-
* - `format` is the optional output format. Either `json` or `markdown` with
|
|
17
|
-
* `markdown` being the default.
|
|
18
|
-
*
|
|
19
|
-
* @module checker
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
const path = require('path');
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* List of spec shortnames that, so far, don't follow the dfns data model
|
|
26
|
-
*/
|
|
27
|
-
const specsWithObsoleteDfnsModel = [
|
|
28
|
-
'svg-animations', 'svg-markers', 'svg-strokes', 'SVG2',
|
|
29
|
-
'webgl1', 'webgl2',
|
|
30
|
-
'webrtc-identity'
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Return true when provided arrays are "equal", meaning that they contain the
|
|
36
|
-
* same items
|
|
37
|
-
*
|
|
38
|
-
* @function
|
|
39
|
-
* @private
|
|
40
|
-
* @param {Array} a First array to compare
|
|
41
|
-
* @param {Array} b Second array to compare
|
|
42
|
-
* @return {boolean} True when arrays are equal
|
|
43
|
-
*/
|
|
44
|
-
function arraysEqual(a, b) {
|
|
45
|
-
return Array.isArray(a) &&
|
|
46
|
-
Array.isArray(b) &&
|
|
47
|
-
a.length === b.length &&
|
|
48
|
-
a.every((val, index) => val === b[index]);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Return the list of expected definitions from the CSS extract
|
|
54
|
-
*
|
|
55
|
-
* @function
|
|
56
|
-
* @private
|
|
57
|
-
* @param {Object} css The root of the object that describes CSS terms in the
|
|
58
|
-
* CSS extract
|
|
59
|
-
* @return {Array} An array of expected definitions
|
|
60
|
-
*/
|
|
61
|
-
function getExpectedDfnsFromCSS(css) {
|
|
62
|
-
let expected = [];
|
|
63
|
-
|
|
64
|
-
// Add the list of expected properties, filtering out properties that define
|
|
65
|
-
// new values to an existing property (defined elsewhere)
|
|
66
|
-
expected = expected.concat(
|
|
67
|
-
Object.values(css.properties || {})
|
|
68
|
-
.filter(desc => !desc.newValues)
|
|
69
|
-
.map(desc => {
|
|
70
|
-
return {
|
|
71
|
-
linkingText: [desc.name],
|
|
72
|
-
type: 'property',
|
|
73
|
-
'for': []
|
|
74
|
-
};
|
|
75
|
-
})
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
// Add the list of expected descriptors
|
|
79
|
-
expected = expected.concat(
|
|
80
|
-
Object.values(css.descriptors || {}).flat().map(desc => {
|
|
81
|
-
return {
|
|
82
|
-
linkingText: [desc.name],
|
|
83
|
-
type: 'descriptor',
|
|
84
|
-
'for': [desc.for]
|
|
85
|
-
};
|
|
86
|
-
})
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
// Add the list of expected "values".
|
|
90
|
-
// Note: we don't qualify the "type" of values in valuespaces and don't store
|
|
91
|
-
// the scope of values either (the "for" property). Definition types can be
|
|
92
|
-
// "type", "function", "value", etc. in practice. The comparison cannot be
|
|
93
|
-
// perfect as a result.
|
|
94
|
-
expected = expected.concat(
|
|
95
|
-
Object.entries(css.valuespaces || {}).map(([name, desc]) => {
|
|
96
|
-
return {
|
|
97
|
-
linkingText: [name],
|
|
98
|
-
value: desc.value
|
|
99
|
-
};
|
|
100
|
-
})
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
return expected;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Return true when the given CSS definition matches the expected definition
|
|
109
|
-
*
|
|
110
|
-
* @function
|
|
111
|
-
* @private
|
|
112
|
-
* @param {Object} expected Expected definition
|
|
113
|
-
* @param {Object} actual Actual definition to check
|
|
114
|
-
* @return {Boolean} true when actual definition matches the expected one
|
|
115
|
-
*/
|
|
116
|
-
function matchCSSDfn(expected, actual) {
|
|
117
|
-
return arraysEqual(expected.linkingText, actual.linkingText) &&
|
|
118
|
-
(!expected.for || arraysEqual(expected.for, actual.for)) &&
|
|
119
|
-
(!expected.type || (expected.type === actual.type));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Return the list of expected definitions from the IDL extract
|
|
125
|
-
*
|
|
126
|
-
* @function
|
|
127
|
-
* @private
|
|
128
|
-
* @param {Object} css The root of the object that describes IDL terms in the
|
|
129
|
-
* `idlparsed` extract.
|
|
130
|
-
* @return {Array} An array of expected definitions
|
|
131
|
-
*/
|
|
132
|
-
function getExpectedDfnsFromIdl(idl = {}) {
|
|
133
|
-
// Parse IDL names that the spec defines
|
|
134
|
-
const idlNames = Object.values(idl.idlNames || {});
|
|
135
|
-
let expected = idlNames.map(name => getExpectedDfnsFromIdlDesc(name)).flat();
|
|
136
|
-
|
|
137
|
-
// Parse members of IDL names that the spec extends
|
|
138
|
-
const idlExtendedNames = Object.values(idl.idlExtendedNames || {});
|
|
139
|
-
expected = expected.concat(idlExtendedNames.map(extended =>
|
|
140
|
-
extended.map(name => getExpectedDfnsFromIdlDesc(name, { excludeRoot: true })))
|
|
141
|
-
.flat(2));
|
|
142
|
-
return expected;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Return true if the given parsed IDL object describes a default toJSON
|
|
148
|
-
* operation that references:
|
|
149
|
-
* https://heycam.github.io/webidl/#default-tojson-steps
|
|
150
|
-
*
|
|
151
|
-
* @function
|
|
152
|
-
* @private
|
|
153
|
-
* @param {Object} desc Parsed IDL object to check
|
|
154
|
-
* @return {Boolean} true when object describes a default toJSON operation.
|
|
155
|
-
*/
|
|
156
|
-
function isDefaultToJSONOperation(desc) {
|
|
157
|
-
return (desc.type === 'operation') &&
|
|
158
|
-
(desc.name === 'toJSON') &&
|
|
159
|
-
(desc.extAttrs && desc.extAttrs.find(attr => attr.name === "Default"));
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Return the expected definition for the given parsed IDL structure
|
|
165
|
-
*
|
|
166
|
-
* @function
|
|
167
|
-
* @public
|
|
168
|
-
* @param {Object} desc The object that describes the IDL term in the
|
|
169
|
-
* `idlparsed` extract.
|
|
170
|
-
* @param {Object} parentDesc (optional) The object that describes the parent
|
|
171
|
-
* IDL term of the term to parse (used to compute the `for` property).
|
|
172
|
-
* @return {Object} The expected definition, or null if no expected definition
|
|
173
|
-
* is defined.
|
|
174
|
-
*/
|
|
175
|
-
function getExpectedDfnFromIdlDesc(idl, parentIdl) {
|
|
176
|
-
function serializeArgs(args = []) {
|
|
177
|
-
return args
|
|
178
|
-
.map(arg => arg.variadic ? `...${arg.name}` : arg.name)
|
|
179
|
-
.join(', ');
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
let expected = {
|
|
183
|
-
linkingText: [idl.name],
|
|
184
|
-
type: idl.type,
|
|
185
|
-
'for': parentIdl && (parentIdl !== idl) ? [parentIdl.name] : []
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
switch (idl.type) {
|
|
189
|
-
case 'attribute':
|
|
190
|
-
case 'const':
|
|
191
|
-
break;
|
|
192
|
-
|
|
193
|
-
case 'constructor':
|
|
194
|
-
// Ignore constructors for HTML elements, the spec has a dedicated
|
|
195
|
-
// section for them:
|
|
196
|
-
// https://html.spec.whatwg.org/multipage/dom.html#html-element-constructors
|
|
197
|
-
if (!parentIdl.name.startsWith('HTML')) {
|
|
198
|
-
expected.linkingText = [`constructor(${serializeArgs(idl.arguments)})`];
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
expected = null;
|
|
202
|
-
}
|
|
203
|
-
break;
|
|
204
|
-
|
|
205
|
-
case 'enum':
|
|
206
|
-
break;
|
|
207
|
-
|
|
208
|
-
case 'enum-value':
|
|
209
|
-
// The enumeration could include the empty string as a value. There
|
|
210
|
-
// cannot be a matching definition in that case.
|
|
211
|
-
// Note: look for the quoted value and the unquoted value
|
|
212
|
-
const value = idl.value.replace(/^"(.*)"$/, '$1');
|
|
213
|
-
expected.linkingText = (value !== '') ? [`"${value}"`, value] : [`"${value}"`];
|
|
214
|
-
break;
|
|
215
|
-
|
|
216
|
-
case 'field':
|
|
217
|
-
expected.type = 'dict-member';
|
|
218
|
-
break;
|
|
219
|
-
|
|
220
|
-
case 'callback':
|
|
221
|
-
case 'callback interface':
|
|
222
|
-
case 'dictionary':
|
|
223
|
-
case 'interface':
|
|
224
|
-
case 'interface mixin':
|
|
225
|
-
case 'namespace':
|
|
226
|
-
expected.type =
|
|
227
|
-
(idl.type === 'callback interface') ? 'callback' :
|
|
228
|
-
(idl.type === 'interface mixin') ? 'interface' :
|
|
229
|
-
idl.type;
|
|
230
|
-
// Ignore partial definition
|
|
231
|
-
if (idl.partial) {
|
|
232
|
-
expected = null;
|
|
233
|
-
}
|
|
234
|
-
break;
|
|
235
|
-
|
|
236
|
-
case 'includes':
|
|
237
|
-
expected = null;
|
|
238
|
-
break;
|
|
239
|
-
|
|
240
|
-
case 'iterable':
|
|
241
|
-
case 'maplike':
|
|
242
|
-
case 'setlike':
|
|
243
|
-
// No definition expected for iterable, maplike and setlike members
|
|
244
|
-
expected = null;
|
|
245
|
-
break;
|
|
246
|
-
|
|
247
|
-
case 'operation':
|
|
248
|
-
// Stringification behavior is typically defined with a
|
|
249
|
-
// "stringification behavior" definition scoped to the interface
|
|
250
|
-
if (idl.special === 'stringifier') {
|
|
251
|
-
expected.linkingText = ['stringification behavior', 'stringificationbehavior'];
|
|
252
|
-
expected.type = 'dfn';
|
|
253
|
-
}
|
|
254
|
-
// Ignore special "getter", "setter", "deleter" operations when they don't
|
|
255
|
-
// have an identifier. They should link to a definition in the prose, but
|
|
256
|
-
// the labels seem arbitrary for now.
|
|
257
|
-
// Also ignore default toJSON operations. Steps are defined in WebIDL.
|
|
258
|
-
else if ((idl.name ||
|
|
259
|
-
((idl.special !== 'getter') &&
|
|
260
|
-
(idl.special !== 'setter') &&
|
|
261
|
-
(idl.special !== 'deleter'))) &&
|
|
262
|
-
!isDefaultToJSONOperation(idl)) {
|
|
263
|
-
expected.linkingText = [`${idl.name}(${serializeArgs(idl.arguments)})`];
|
|
264
|
-
expected.type = 'method';
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
expected = null;
|
|
268
|
-
}
|
|
269
|
-
break;
|
|
270
|
-
|
|
271
|
-
case 'typedef':
|
|
272
|
-
break;
|
|
273
|
-
|
|
274
|
-
case 'argument':
|
|
275
|
-
expected = null;
|
|
276
|
-
break;
|
|
277
|
-
|
|
278
|
-
default:
|
|
279
|
-
console.warn('Unsupported IDL type', idl.type, idl);
|
|
280
|
-
expected = null;
|
|
281
|
-
break;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return expected;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Return the list of expected definitions from a parsed IDL extract entry.
|
|
290
|
-
*
|
|
291
|
-
* The function is recursive.
|
|
292
|
-
*
|
|
293
|
-
* @function
|
|
294
|
-
* @private
|
|
295
|
-
* @param {Object} idl The object that describes the IDL term in the
|
|
296
|
-
* `idlparsed` extract.
|
|
297
|
-
* @return {Array} An array of expected definitions
|
|
298
|
-
*/
|
|
299
|
-
function getExpectedDfnsFromIdlDesc(idl, {excludeRoot} = {excludeRoot: false}) {
|
|
300
|
-
const res = [];
|
|
301
|
-
const parentIdl = idl;
|
|
302
|
-
const idlToProcess = excludeRoot ? [] : [idl];
|
|
303
|
-
|
|
304
|
-
switch (idl.type) {
|
|
305
|
-
case 'enum':
|
|
306
|
-
if (idl.values) {
|
|
307
|
-
idlToProcess.push(...idl.values);
|
|
308
|
-
}
|
|
309
|
-
break;
|
|
310
|
-
|
|
311
|
-
case 'callback':
|
|
312
|
-
case 'callback interface':
|
|
313
|
-
case 'dictionary':
|
|
314
|
-
case 'interface':
|
|
315
|
-
case 'interface mixin':
|
|
316
|
-
case 'namespace':
|
|
317
|
-
if (idl.members) {
|
|
318
|
-
idlToProcess.push(...idl.members);
|
|
319
|
-
}
|
|
320
|
-
break;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
idlToProcess.forEach(idl => {
|
|
324
|
-
const expected = getExpectedDfnFromIdlDesc(idl, parentIdl);
|
|
325
|
-
if (expected) {
|
|
326
|
-
expected.access = 'public';
|
|
327
|
-
expected.informative = false;
|
|
328
|
-
res.push(expected);
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
return res;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Return true when the given IDL definition matches the expected definition.
|
|
338
|
-
*
|
|
339
|
-
* The function handles overloaded methods, though not properly. That is, it
|
|
340
|
-
* will only find the "right" definition for an overloaded method if the number
|
|
341
|
-
* and/or the name of the arguments differ between the overloaded definitions.
|
|
342
|
-
* Otherwise it will just match the first definition that looks good.
|
|
343
|
-
*
|
|
344
|
-
* The function works around Respec's issue #3200 for methods and constructors
|
|
345
|
-
* that take only optional parameters:
|
|
346
|
-
* https://github.com/w3c/respec/issues/3200
|
|
347
|
-
*
|
|
348
|
-
* @function
|
|
349
|
-
* @private
|
|
350
|
-
* @param {Object} expected Expected definition
|
|
351
|
-
* @param {Object} actual Actual definition to check
|
|
352
|
-
* @param {Object} options Comparison options
|
|
353
|
-
* @return {Boolean} true when actual definition matches the expected one
|
|
354
|
-
*/
|
|
355
|
-
function matchIdlDfn(expected, actual,
|
|
356
|
-
{skipArgs, skipFor, skipType} = {skipArgs: false, skipFor: false, skipType: false}) {
|
|
357
|
-
const fixedLt = actual.linkingText
|
|
358
|
-
.map(lt => lt.replace(/!overload-\d/, ''))
|
|
359
|
-
.map(lt => lt.replace(/\(, /, '('));
|
|
360
|
-
let found = expected.linkingText.some(val => fixedLt.includes(val));
|
|
361
|
-
if (!found && skipArgs) {
|
|
362
|
-
const names = fixedLt.map(lt => lt.replace(/\(.*\)/, ''));
|
|
363
|
-
found = expected.linkingText.some(val => {
|
|
364
|
-
const valname = val.replace(/\(.*\)/, '');
|
|
365
|
-
return names.find(name => name === valname);
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
return found &&
|
|
369
|
-
(expected.for.every(val => actual.for.includes(val)) || skipFor) &&
|
|
370
|
-
(expected.type === actual.type || skipType);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Checks the CSS and IDL extracts against the dfns extract for the given spec
|
|
376
|
-
*
|
|
377
|
-
* @function
|
|
378
|
-
* @public
|
|
379
|
-
* @param {Object} spec Crawl result for the spec to parse
|
|
380
|
-
* @param {String} options Check options. Set the rootFolder property to the
|
|
381
|
-
* root folder against which to resolve relative paths to load CSS/IDL
|
|
382
|
-
* extracts (only needed if the extracts have not yet been loaded and attached
|
|
383
|
-
* to the spec object). Set the includeObsolete property to true to include
|
|
384
|
-
* detailed results about specs that use an obsolete dfns data model.
|
|
385
|
-
* @return {Object} An object with a css and idl property, each of them holding
|
|
386
|
-
* an array of missing CSS or IDL definitions. The function returns null when
|
|
387
|
-
* there are no missing definitions.
|
|
388
|
-
*/
|
|
389
|
-
function checkSpecDefinitions(spec, options = {}) {
|
|
390
|
-
if (!options.includeObsolete && specsWithObsoleteDfnsModel.includes(spec.shortname)) {
|
|
391
|
-
return { obsoleteDfnsModel: true };
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const dfns = (typeof spec.dfns === "string") ?
|
|
395
|
-
require(path.resolve(options.rootFolder, spec.dfns)).dfns :
|
|
396
|
-
(spec.dfns || []);
|
|
397
|
-
const css = (typeof spec.css === "string") ?
|
|
398
|
-
require(path.resolve(options.rootFolder, spec.css)) :
|
|
399
|
-
(spec.css || {});
|
|
400
|
-
const idl = (typeof spec.idlparsed === "string") ?
|
|
401
|
-
require(path.resolve(options.rootFolder, spec.idlparsed)).idlparsed :
|
|
402
|
-
spec.idlparsed;
|
|
403
|
-
|
|
404
|
-
// Make sure that all expected CSS definitions exist in the dfns extract
|
|
405
|
-
const expectedCSSDfns = getExpectedDfnsFromCSS(css);
|
|
406
|
-
const missingCSSDfns = expectedCSSDfns.map(expected => {
|
|
407
|
-
let actual = dfns.find(dfn => matchCSSDfn(expected, dfn));
|
|
408
|
-
if (!actual && !expected.type) {
|
|
409
|
-
// Right definition is missing. For valuespaces that define functions,
|
|
410
|
-
// look for a function definition without the enclosing "<>" instead
|
|
411
|
-
const altText = [expected.linkingText[0].replace(/^<(.*)\(\)>$/, '$1()')];
|
|
412
|
-
actual = dfns.find(dfn => arraysEqual(altText, dfn.linkingText));
|
|
413
|
-
}
|
|
414
|
-
if (!actual && expected.value) {
|
|
415
|
-
// Still missing? For valuespaces that define functions, this may be
|
|
416
|
-
// because there is no definition without parameters, try to find the
|
|
417
|
-
// actual value instead
|
|
418
|
-
actual = dfns.find(dfn => arraysEqual([expected.value], dfn.linkingText));
|
|
419
|
-
}
|
|
420
|
-
if (actual) {
|
|
421
|
-
// Right definition found
|
|
422
|
-
return null;
|
|
423
|
-
}
|
|
424
|
-
else {
|
|
425
|
-
// Right definition is missing, there may be a definition that looks
|
|
426
|
-
// like the one we're looking for
|
|
427
|
-
const found = dfns.find(dfn =>
|
|
428
|
-
arraysEqual(dfn.linkingText, expected.linkingText));
|
|
429
|
-
return { expected, found };
|
|
430
|
-
}
|
|
431
|
-
}).filter(missing => !!missing);
|
|
432
|
-
|
|
433
|
-
// Make sure that all expected IDL definitions exist in the dfns extract
|
|
434
|
-
const expectedIdlDfns = getExpectedDfnsFromIdl(idl);
|
|
435
|
-
const missingIdlDfns = expectedIdlDfns.map(expected => {
|
|
436
|
-
let actual = dfns.find(dfn => matchIdlDfn(expected, dfn));
|
|
437
|
-
if (actual) {
|
|
438
|
-
// Right definition found
|
|
439
|
-
return null;
|
|
440
|
-
}
|
|
441
|
-
else {
|
|
442
|
-
// Right definition is missing, include the interface's definitions to
|
|
443
|
-
// be able to link to it in the report
|
|
444
|
-
let parent = null;
|
|
445
|
-
if (expected.for && expected.for[0]) {
|
|
446
|
-
parent = dfns.find(dfn =>
|
|
447
|
-
(dfn.linkingText[0] === expected.for[0]) &&
|
|
448
|
-
['callback', 'dictionary', 'enum', 'interface', 'namespace'].includes(dfn.type));
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Look for a definition that seems as close as possible to the one
|
|
452
|
-
// we're looking for, in the following order:
|
|
453
|
-
// 1. For operations, find a definition without taking arguments into
|
|
454
|
-
// account and report possible match with a "warning" flag.
|
|
455
|
-
// 2. For terms linked to a parent interface-like object, find a match
|
|
456
|
-
// scoped to the same parent without taking the type into account.
|
|
457
|
-
// 3. Look for a definition with the same name, neither taking the type
|
|
458
|
-
// nor the parent into account.
|
|
459
|
-
let found = dfns.find(dfn => matchIdlDfn(expected, dfn, { skipArgs: true }));
|
|
460
|
-
if (found) {
|
|
461
|
-
return { expected, found, for: parent, warning: true };
|
|
462
|
-
}
|
|
463
|
-
found = dfns.find(dfn => matchIdlDfn(expected, dfn,
|
|
464
|
-
{ skipArgs: true, skipType: true }));
|
|
465
|
-
if (found) {
|
|
466
|
-
return { expected, found, for: parent };
|
|
467
|
-
}
|
|
468
|
-
found = dfns.find(dfn => matchIdlDfn(expected, dfn,
|
|
469
|
-
{ skipArgs: true, skipType: true, skipFor: true }));
|
|
470
|
-
return { expected, found, for: parent };
|
|
471
|
-
}
|
|
472
|
-
}).filter(missing => !!missing);
|
|
473
|
-
|
|
474
|
-
// Report results
|
|
475
|
-
return {
|
|
476
|
-
css: missingCSSDfns,
|
|
477
|
-
idl: missingIdlDfns
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
/**
|
|
483
|
-
* Checks the CSS and IDL extracts against the dfns extract for all specs in
|
|
484
|
-
* the report.
|
|
485
|
-
*
|
|
486
|
-
* @function
|
|
487
|
-
* @public
|
|
488
|
-
* @param {String} pathToReport Path to the root folder that contains the
|
|
489
|
-
* `index.json` report file and the extracts subfolders.
|
|
490
|
-
* @param {Object} options Check options. Set the "shortname" property to a
|
|
491
|
-
* spec's shortname to only check that spec.
|
|
492
|
-
* @return {Array} The list of specifications along with dfn problems that have
|
|
493
|
-
* been identified. Each entry has `url`, 'crawled`, `shortname` properties to
|
|
494
|
-
* identify the specification, and a `missing` property that is an object that
|
|
495
|
-
* may have `css` and `idl` properties which list missing CSS/IDL definitions.
|
|
496
|
-
*/
|
|
497
|
-
function checkDefinitions(pathToReport, options = {}) {
|
|
498
|
-
const rootFolder = path.resolve(process.cwd(), pathToReport);
|
|
499
|
-
const index = require(path.resolve(rootFolder, 'index.json')).results;
|
|
500
|
-
|
|
501
|
-
// Check all dfns against CSS and IDL extracts
|
|
502
|
-
const checkOptions = {
|
|
503
|
-
rootFolder,
|
|
504
|
-
includeObsolete: !!options.shortname
|
|
505
|
-
};
|
|
506
|
-
const missing = index
|
|
507
|
-
.filter(spec => !options.shortname || spec.shortname === options.shortname)
|
|
508
|
-
.map(spec => {
|
|
509
|
-
const res = {
|
|
510
|
-
url: spec.url,
|
|
511
|
-
crawled: spec.crawled,
|
|
512
|
-
shortname: spec.shortname,
|
|
513
|
-
};
|
|
514
|
-
if (!spec.dfns) {
|
|
515
|
-
return res;
|
|
516
|
-
}
|
|
517
|
-
res.missing = checkSpecDefinitions(spec, checkOptions);
|
|
518
|
-
return res;
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
return missing;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* Report missing dfn to the console as Markdown
|
|
527
|
-
*
|
|
528
|
-
* @function
|
|
529
|
-
* @private
|
|
530
|
-
* @param {Object} missing Object that describes missing dfn
|
|
531
|
-
*/
|
|
532
|
-
function reportMissing(missing) {
|
|
533
|
-
const exp = missing.expected;
|
|
534
|
-
const found = missing.found;
|
|
535
|
-
const foundFor = (found && found.for && found.for.length > 0) ?
|
|
536
|
-
' for ' + found.for.map(f => `\`${f}\``).join(',') :
|
|
537
|
-
'';
|
|
538
|
-
console.log(`- \`${exp.linkingText[0]}\` ${exp.type ? `with type \`${exp.type}\`` : ''}` +
|
|
539
|
-
(missing.for ? ` for [\`${missing.for.linkingText[0]}\`](${missing.for.href})` : '') +
|
|
540
|
-
(found ? `, but found [\`${found.linkingText[0]}\`](${found.href}) with type \`${found.type}\`${foundFor}` : ''));
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
/**************************************************
|
|
545
|
-
Export methods for use as module
|
|
546
|
-
**************************************************/
|
|
547
|
-
module.exports.checkSpecDefinitions = checkSpecDefinitions;
|
|
548
|
-
module.exports.checkDefinitions = checkDefinitions;
|
|
549
|
-
|
|
550
|
-
// "Inner" functions that the IDL names generator uses to link IDL terms with
|
|
551
|
-
// their definition (see generate-idlnames.js)
|
|
552
|
-
module.exports.getExpectedDfnFromIdlDesc = getExpectedDfnFromIdlDesc;
|
|
553
|
-
module.exports.matchIdlDfn = matchIdlDfn;
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
/**************************************************
|
|
558
|
-
Code run if the code is run as a stand-alone module
|
|
559
|
-
**************************************************/
|
|
560
|
-
if (require.main === module) {
|
|
561
|
-
const pathToReport = process.argv[2];
|
|
562
|
-
const shortname = process.argv[3] || 'all';
|
|
563
|
-
const format = process.argv[4] || 'markdown';
|
|
564
|
-
|
|
565
|
-
const options = (shortname === 'all') ? undefined : { shortname };
|
|
566
|
-
let res = checkDefinitions(pathToReport, options);
|
|
567
|
-
if (shortname === 'all') {
|
|
568
|
-
res = res
|
|
569
|
-
.filter(result => result.missing &&
|
|
570
|
-
!result.missing.obsoleteDfnsModel &&
|
|
571
|
-
((result.missing.css.length > 0) || (result.missing.idl.length > 0)));
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
if (format === 'json') {
|
|
575
|
-
console.log(JSON.stringify(res, null, 2));
|
|
576
|
-
}
|
|
577
|
-
else {
|
|
578
|
-
res.forEach(result => {
|
|
579
|
-
const missing = result.missing || {css: [], idl: []};
|
|
580
|
-
const errors = ['css', 'idl']
|
|
581
|
-
.map(type => result.missing[type].filter(missing => !missing.warning))
|
|
582
|
-
.flat();
|
|
583
|
-
const warnings = ['css', 'idl']
|
|
584
|
-
.map(type => result.missing[type].filter(missing => missing.warning))
|
|
585
|
-
.flat();
|
|
586
|
-
console.log('<details>');
|
|
587
|
-
console.log(`<summary><b><a href="${result.crawled}">${result.shortname}</a></b> (${errors.length} errors, ${warnings.length} warnings)</summary>`);
|
|
588
|
-
console.log();
|
|
589
|
-
if (errors.length === 0 && warnings.length === 0) {
|
|
590
|
-
console.log('All good!');
|
|
591
|
-
}
|
|
592
|
-
if (errors.length > 0) {
|
|
593
|
-
console.log('<details open>');
|
|
594
|
-
console.log(`<summary><i>Errors</i> (${errors.length})</summary>`);
|
|
595
|
-
console.log();
|
|
596
|
-
errors.forEach(reportMissing);
|
|
597
|
-
console.log('</details>');
|
|
598
|
-
}
|
|
599
|
-
if (warnings.length > 0) {
|
|
600
|
-
console.log('<details open>');
|
|
601
|
-
console.log(`<summary><i>Warnings</i> (${warnings.length})</summary>`);
|
|
602
|
-
console.log();
|
|
603
|
-
warnings.forEach(reportMissing);
|
|
604
|
-
console.log('</details>');
|
|
605
|
-
}
|
|
606
|
-
console.log('</details>');
|
|
607
|
-
console.log();
|
|
608
|
-
})
|
|
609
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The definitions checker compares CSS, dfns, and IDL extracts created by Reffy
|
|
4
|
+
* to detect CSS/IDL terms that do not have a corresponding dfn in the
|
|
5
|
+
* specification.
|
|
6
|
+
*
|
|
7
|
+
* The definitions checker can be called directly through:
|
|
8
|
+
*
|
|
9
|
+
* `node check-missing-dfns.js [crawl report] [spec] [format]`
|
|
10
|
+
*
|
|
11
|
+
* where:
|
|
12
|
+
* - `crawl report` is the local path to the root folder that contains the
|
|
13
|
+
* `index.json` and the extracts (e.g. `reports/ed`)
|
|
14
|
+
* - `spec` is the optional shortname of the specification on which to focus or
|
|
15
|
+
* `all` (default) to check all specs
|
|
16
|
+
* - `format` is the optional output format. Either `json` or `markdown` with
|
|
17
|
+
* `markdown` being the default.
|
|
18
|
+
*
|
|
19
|
+
* @module checker
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* List of spec shortnames that, so far, don't follow the dfns data model
|
|
26
|
+
*/
|
|
27
|
+
const specsWithObsoleteDfnsModel = [
|
|
28
|
+
'svg-animations', 'svg-markers', 'svg-strokes', 'SVG2',
|
|
29
|
+
'webgl1', 'webgl2',
|
|
30
|
+
'webrtc-identity'
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Return true when provided arrays are "equal", meaning that they contain the
|
|
36
|
+
* same items
|
|
37
|
+
*
|
|
38
|
+
* @function
|
|
39
|
+
* @private
|
|
40
|
+
* @param {Array} a First array to compare
|
|
41
|
+
* @param {Array} b Second array to compare
|
|
42
|
+
* @return {boolean} True when arrays are equal
|
|
43
|
+
*/
|
|
44
|
+
function arraysEqual(a, b) {
|
|
45
|
+
return Array.isArray(a) &&
|
|
46
|
+
Array.isArray(b) &&
|
|
47
|
+
a.length === b.length &&
|
|
48
|
+
a.every((val, index) => val === b[index]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Return the list of expected definitions from the CSS extract
|
|
54
|
+
*
|
|
55
|
+
* @function
|
|
56
|
+
* @private
|
|
57
|
+
* @param {Object} css The root of the object that describes CSS terms in the
|
|
58
|
+
* CSS extract
|
|
59
|
+
* @return {Array} An array of expected definitions
|
|
60
|
+
*/
|
|
61
|
+
function getExpectedDfnsFromCSS(css) {
|
|
62
|
+
let expected = [];
|
|
63
|
+
|
|
64
|
+
// Add the list of expected properties, filtering out properties that define
|
|
65
|
+
// new values to an existing property (defined elsewhere)
|
|
66
|
+
expected = expected.concat(
|
|
67
|
+
Object.values(css.properties || {})
|
|
68
|
+
.filter(desc => !desc.newValues)
|
|
69
|
+
.map(desc => {
|
|
70
|
+
return {
|
|
71
|
+
linkingText: [desc.name],
|
|
72
|
+
type: 'property',
|
|
73
|
+
'for': []
|
|
74
|
+
};
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Add the list of expected descriptors
|
|
79
|
+
expected = expected.concat(
|
|
80
|
+
Object.values(css.descriptors || {}).flat().map(desc => {
|
|
81
|
+
return {
|
|
82
|
+
linkingText: [desc.name],
|
|
83
|
+
type: 'descriptor',
|
|
84
|
+
'for': [desc.for]
|
|
85
|
+
};
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Add the list of expected "values".
|
|
90
|
+
// Note: we don't qualify the "type" of values in valuespaces and don't store
|
|
91
|
+
// the scope of values either (the "for" property). Definition types can be
|
|
92
|
+
// "type", "function", "value", etc. in practice. The comparison cannot be
|
|
93
|
+
// perfect as a result.
|
|
94
|
+
expected = expected.concat(
|
|
95
|
+
Object.entries(css.valuespaces || {}).map(([name, desc]) => {
|
|
96
|
+
return {
|
|
97
|
+
linkingText: [name],
|
|
98
|
+
value: desc.value
|
|
99
|
+
};
|
|
100
|
+
})
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return expected;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Return true when the given CSS definition matches the expected definition
|
|
109
|
+
*
|
|
110
|
+
* @function
|
|
111
|
+
* @private
|
|
112
|
+
* @param {Object} expected Expected definition
|
|
113
|
+
* @param {Object} actual Actual definition to check
|
|
114
|
+
* @return {Boolean} true when actual definition matches the expected one
|
|
115
|
+
*/
|
|
116
|
+
function matchCSSDfn(expected, actual) {
|
|
117
|
+
return arraysEqual(expected.linkingText, actual.linkingText) &&
|
|
118
|
+
(!expected.for || arraysEqual(expected.for, actual.for)) &&
|
|
119
|
+
(!expected.type || (expected.type === actual.type));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Return the list of expected definitions from the IDL extract
|
|
125
|
+
*
|
|
126
|
+
* @function
|
|
127
|
+
* @private
|
|
128
|
+
* @param {Object} css The root of the object that describes IDL terms in the
|
|
129
|
+
* `idlparsed` extract.
|
|
130
|
+
* @return {Array} An array of expected definitions
|
|
131
|
+
*/
|
|
132
|
+
function getExpectedDfnsFromIdl(idl = {}) {
|
|
133
|
+
// Parse IDL names that the spec defines
|
|
134
|
+
const idlNames = Object.values(idl.idlNames || {});
|
|
135
|
+
let expected = idlNames.map(name => getExpectedDfnsFromIdlDesc(name)).flat();
|
|
136
|
+
|
|
137
|
+
// Parse members of IDL names that the spec extends
|
|
138
|
+
const idlExtendedNames = Object.values(idl.idlExtendedNames || {});
|
|
139
|
+
expected = expected.concat(idlExtendedNames.map(extended =>
|
|
140
|
+
extended.map(name => getExpectedDfnsFromIdlDesc(name, { excludeRoot: true })))
|
|
141
|
+
.flat(2));
|
|
142
|
+
return expected;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Return true if the given parsed IDL object describes a default toJSON
|
|
148
|
+
* operation that references:
|
|
149
|
+
* https://heycam.github.io/webidl/#default-tojson-steps
|
|
150
|
+
*
|
|
151
|
+
* @function
|
|
152
|
+
* @private
|
|
153
|
+
* @param {Object} desc Parsed IDL object to check
|
|
154
|
+
* @return {Boolean} true when object describes a default toJSON operation.
|
|
155
|
+
*/
|
|
156
|
+
function isDefaultToJSONOperation(desc) {
|
|
157
|
+
return (desc.type === 'operation') &&
|
|
158
|
+
(desc.name === 'toJSON') &&
|
|
159
|
+
(desc.extAttrs && desc.extAttrs.find(attr => attr.name === "Default"));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Return the expected definition for the given parsed IDL structure
|
|
165
|
+
*
|
|
166
|
+
* @function
|
|
167
|
+
* @public
|
|
168
|
+
* @param {Object} desc The object that describes the IDL term in the
|
|
169
|
+
* `idlparsed` extract.
|
|
170
|
+
* @param {Object} parentDesc (optional) The object that describes the parent
|
|
171
|
+
* IDL term of the term to parse (used to compute the `for` property).
|
|
172
|
+
* @return {Object} The expected definition, or null if no expected definition
|
|
173
|
+
* is defined.
|
|
174
|
+
*/
|
|
175
|
+
function getExpectedDfnFromIdlDesc(idl, parentIdl) {
|
|
176
|
+
function serializeArgs(args = []) {
|
|
177
|
+
return args
|
|
178
|
+
.map(arg => arg.variadic ? `...${arg.name}` : arg.name)
|
|
179
|
+
.join(', ');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let expected = {
|
|
183
|
+
linkingText: [idl.name],
|
|
184
|
+
type: idl.type,
|
|
185
|
+
'for': parentIdl && (parentIdl !== idl) ? [parentIdl.name] : []
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
switch (idl.type) {
|
|
189
|
+
case 'attribute':
|
|
190
|
+
case 'const':
|
|
191
|
+
break;
|
|
192
|
+
|
|
193
|
+
case 'constructor':
|
|
194
|
+
// Ignore constructors for HTML elements, the spec has a dedicated
|
|
195
|
+
// section for them:
|
|
196
|
+
// https://html.spec.whatwg.org/multipage/dom.html#html-element-constructors
|
|
197
|
+
if (!parentIdl.name.startsWith('HTML')) {
|
|
198
|
+
expected.linkingText = [`constructor(${serializeArgs(idl.arguments)})`];
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
expected = null;
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
|
|
205
|
+
case 'enum':
|
|
206
|
+
break;
|
|
207
|
+
|
|
208
|
+
case 'enum-value':
|
|
209
|
+
// The enumeration could include the empty string as a value. There
|
|
210
|
+
// cannot be a matching definition in that case.
|
|
211
|
+
// Note: look for the quoted value and the unquoted value
|
|
212
|
+
const value = idl.value.replace(/^"(.*)"$/, '$1');
|
|
213
|
+
expected.linkingText = (value !== '') ? [`"${value}"`, value] : [`"${value}"`];
|
|
214
|
+
break;
|
|
215
|
+
|
|
216
|
+
case 'field':
|
|
217
|
+
expected.type = 'dict-member';
|
|
218
|
+
break;
|
|
219
|
+
|
|
220
|
+
case 'callback':
|
|
221
|
+
case 'callback interface':
|
|
222
|
+
case 'dictionary':
|
|
223
|
+
case 'interface':
|
|
224
|
+
case 'interface mixin':
|
|
225
|
+
case 'namespace':
|
|
226
|
+
expected.type =
|
|
227
|
+
(idl.type === 'callback interface') ? 'callback' :
|
|
228
|
+
(idl.type === 'interface mixin') ? 'interface' :
|
|
229
|
+
idl.type;
|
|
230
|
+
// Ignore partial definition
|
|
231
|
+
if (idl.partial) {
|
|
232
|
+
expected = null;
|
|
233
|
+
}
|
|
234
|
+
break;
|
|
235
|
+
|
|
236
|
+
case 'includes':
|
|
237
|
+
expected = null;
|
|
238
|
+
break;
|
|
239
|
+
|
|
240
|
+
case 'iterable':
|
|
241
|
+
case 'maplike':
|
|
242
|
+
case 'setlike':
|
|
243
|
+
// No definition expected for iterable, maplike and setlike members
|
|
244
|
+
expected = null;
|
|
245
|
+
break;
|
|
246
|
+
|
|
247
|
+
case 'operation':
|
|
248
|
+
// Stringification behavior is typically defined with a
|
|
249
|
+
// "stringification behavior" definition scoped to the interface
|
|
250
|
+
if (idl.special === 'stringifier') {
|
|
251
|
+
expected.linkingText = ['stringification behavior', 'stringificationbehavior'];
|
|
252
|
+
expected.type = 'dfn';
|
|
253
|
+
}
|
|
254
|
+
// Ignore special "getter", "setter", "deleter" operations when they don't
|
|
255
|
+
// have an identifier. They should link to a definition in the prose, but
|
|
256
|
+
// the labels seem arbitrary for now.
|
|
257
|
+
// Also ignore default toJSON operations. Steps are defined in WebIDL.
|
|
258
|
+
else if ((idl.name ||
|
|
259
|
+
((idl.special !== 'getter') &&
|
|
260
|
+
(idl.special !== 'setter') &&
|
|
261
|
+
(idl.special !== 'deleter'))) &&
|
|
262
|
+
!isDefaultToJSONOperation(idl)) {
|
|
263
|
+
expected.linkingText = [`${idl.name}(${serializeArgs(idl.arguments)})`];
|
|
264
|
+
expected.type = 'method';
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
expected = null;
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
|
|
271
|
+
case 'typedef':
|
|
272
|
+
break;
|
|
273
|
+
|
|
274
|
+
case 'argument':
|
|
275
|
+
expected = null;
|
|
276
|
+
break;
|
|
277
|
+
|
|
278
|
+
default:
|
|
279
|
+
console.warn('Unsupported IDL type', idl.type, idl);
|
|
280
|
+
expected = null;
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return expected;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Return the list of expected definitions from a parsed IDL extract entry.
|
|
290
|
+
*
|
|
291
|
+
* The function is recursive.
|
|
292
|
+
*
|
|
293
|
+
* @function
|
|
294
|
+
* @private
|
|
295
|
+
* @param {Object} idl The object that describes the IDL term in the
|
|
296
|
+
* `idlparsed` extract.
|
|
297
|
+
* @return {Array} An array of expected definitions
|
|
298
|
+
*/
|
|
299
|
+
function getExpectedDfnsFromIdlDesc(idl, {excludeRoot} = {excludeRoot: false}) {
|
|
300
|
+
const res = [];
|
|
301
|
+
const parentIdl = idl;
|
|
302
|
+
const idlToProcess = excludeRoot ? [] : [idl];
|
|
303
|
+
|
|
304
|
+
switch (idl.type) {
|
|
305
|
+
case 'enum':
|
|
306
|
+
if (idl.values) {
|
|
307
|
+
idlToProcess.push(...idl.values);
|
|
308
|
+
}
|
|
309
|
+
break;
|
|
310
|
+
|
|
311
|
+
case 'callback':
|
|
312
|
+
case 'callback interface':
|
|
313
|
+
case 'dictionary':
|
|
314
|
+
case 'interface':
|
|
315
|
+
case 'interface mixin':
|
|
316
|
+
case 'namespace':
|
|
317
|
+
if (idl.members) {
|
|
318
|
+
idlToProcess.push(...idl.members);
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
idlToProcess.forEach(idl => {
|
|
324
|
+
const expected = getExpectedDfnFromIdlDesc(idl, parentIdl);
|
|
325
|
+
if (expected) {
|
|
326
|
+
expected.access = 'public';
|
|
327
|
+
expected.informative = false;
|
|
328
|
+
res.push(expected);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
return res;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Return true when the given IDL definition matches the expected definition.
|
|
338
|
+
*
|
|
339
|
+
* The function handles overloaded methods, though not properly. That is, it
|
|
340
|
+
* will only find the "right" definition for an overloaded method if the number
|
|
341
|
+
* and/or the name of the arguments differ between the overloaded definitions.
|
|
342
|
+
* Otherwise it will just match the first definition that looks good.
|
|
343
|
+
*
|
|
344
|
+
* The function works around Respec's issue #3200 for methods and constructors
|
|
345
|
+
* that take only optional parameters:
|
|
346
|
+
* https://github.com/w3c/respec/issues/3200
|
|
347
|
+
*
|
|
348
|
+
* @function
|
|
349
|
+
* @private
|
|
350
|
+
* @param {Object} expected Expected definition
|
|
351
|
+
* @param {Object} actual Actual definition to check
|
|
352
|
+
* @param {Object} options Comparison options
|
|
353
|
+
* @return {Boolean} true when actual definition matches the expected one
|
|
354
|
+
*/
|
|
355
|
+
function matchIdlDfn(expected, actual,
|
|
356
|
+
{skipArgs, skipFor, skipType} = {skipArgs: false, skipFor: false, skipType: false}) {
|
|
357
|
+
const fixedLt = actual.linkingText
|
|
358
|
+
.map(lt => lt.replace(/!overload-\d/, ''))
|
|
359
|
+
.map(lt => lt.replace(/\(, /, '('));
|
|
360
|
+
let found = expected.linkingText.some(val => fixedLt.includes(val));
|
|
361
|
+
if (!found && skipArgs) {
|
|
362
|
+
const names = fixedLt.map(lt => lt.replace(/\(.*\)/, ''));
|
|
363
|
+
found = expected.linkingText.some(val => {
|
|
364
|
+
const valname = val.replace(/\(.*\)/, '');
|
|
365
|
+
return names.find(name => name === valname);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return found &&
|
|
369
|
+
(expected.for.every(val => actual.for.includes(val)) || skipFor) &&
|
|
370
|
+
(expected.type === actual.type || skipType);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Checks the CSS and IDL extracts against the dfns extract for the given spec
|
|
376
|
+
*
|
|
377
|
+
* @function
|
|
378
|
+
* @public
|
|
379
|
+
* @param {Object} spec Crawl result for the spec to parse
|
|
380
|
+
* @param {String} options Check options. Set the rootFolder property to the
|
|
381
|
+
* root folder against which to resolve relative paths to load CSS/IDL
|
|
382
|
+
* extracts (only needed if the extracts have not yet been loaded and attached
|
|
383
|
+
* to the spec object). Set the includeObsolete property to true to include
|
|
384
|
+
* detailed results about specs that use an obsolete dfns data model.
|
|
385
|
+
* @return {Object} An object with a css and idl property, each of them holding
|
|
386
|
+
* an array of missing CSS or IDL definitions. The function returns null when
|
|
387
|
+
* there are no missing definitions.
|
|
388
|
+
*/
|
|
389
|
+
function checkSpecDefinitions(spec, options = {}) {
|
|
390
|
+
if (!options.includeObsolete && specsWithObsoleteDfnsModel.includes(spec.shortname)) {
|
|
391
|
+
return { obsoleteDfnsModel: true };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const dfns = (typeof spec.dfns === "string") ?
|
|
395
|
+
require(path.resolve(options.rootFolder, spec.dfns)).dfns :
|
|
396
|
+
(spec.dfns || []);
|
|
397
|
+
const css = (typeof spec.css === "string") ?
|
|
398
|
+
require(path.resolve(options.rootFolder, spec.css)) :
|
|
399
|
+
(spec.css || {});
|
|
400
|
+
const idl = (typeof spec.idlparsed === "string") ?
|
|
401
|
+
require(path.resolve(options.rootFolder, spec.idlparsed)).idlparsed :
|
|
402
|
+
spec.idlparsed;
|
|
403
|
+
|
|
404
|
+
// Make sure that all expected CSS definitions exist in the dfns extract
|
|
405
|
+
const expectedCSSDfns = getExpectedDfnsFromCSS(css);
|
|
406
|
+
const missingCSSDfns = expectedCSSDfns.map(expected => {
|
|
407
|
+
let actual = dfns.find(dfn => matchCSSDfn(expected, dfn));
|
|
408
|
+
if (!actual && !expected.type) {
|
|
409
|
+
// Right definition is missing. For valuespaces that define functions,
|
|
410
|
+
// look for a function definition without the enclosing "<>" instead
|
|
411
|
+
const altText = [expected.linkingText[0].replace(/^<(.*)\(\)>$/, '$1()')];
|
|
412
|
+
actual = dfns.find(dfn => arraysEqual(altText, dfn.linkingText));
|
|
413
|
+
}
|
|
414
|
+
if (!actual && expected.value) {
|
|
415
|
+
// Still missing? For valuespaces that define functions, this may be
|
|
416
|
+
// because there is no definition without parameters, try to find the
|
|
417
|
+
// actual value instead
|
|
418
|
+
actual = dfns.find(dfn => arraysEqual([expected.value], dfn.linkingText));
|
|
419
|
+
}
|
|
420
|
+
if (actual) {
|
|
421
|
+
// Right definition found
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
// Right definition is missing, there may be a definition that looks
|
|
426
|
+
// like the one we're looking for
|
|
427
|
+
const found = dfns.find(dfn =>
|
|
428
|
+
arraysEqual(dfn.linkingText, expected.linkingText));
|
|
429
|
+
return { expected, found };
|
|
430
|
+
}
|
|
431
|
+
}).filter(missing => !!missing);
|
|
432
|
+
|
|
433
|
+
// Make sure that all expected IDL definitions exist in the dfns extract
|
|
434
|
+
const expectedIdlDfns = getExpectedDfnsFromIdl(idl);
|
|
435
|
+
const missingIdlDfns = expectedIdlDfns.map(expected => {
|
|
436
|
+
let actual = dfns.find(dfn => matchIdlDfn(expected, dfn));
|
|
437
|
+
if (actual) {
|
|
438
|
+
// Right definition found
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
// Right definition is missing, include the interface's definitions to
|
|
443
|
+
// be able to link to it in the report
|
|
444
|
+
let parent = null;
|
|
445
|
+
if (expected.for && expected.for[0]) {
|
|
446
|
+
parent = dfns.find(dfn =>
|
|
447
|
+
(dfn.linkingText[0] === expected.for[0]) &&
|
|
448
|
+
['callback', 'dictionary', 'enum', 'interface', 'namespace'].includes(dfn.type));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Look for a definition that seems as close as possible to the one
|
|
452
|
+
// we're looking for, in the following order:
|
|
453
|
+
// 1. For operations, find a definition without taking arguments into
|
|
454
|
+
// account and report possible match with a "warning" flag.
|
|
455
|
+
// 2. For terms linked to a parent interface-like object, find a match
|
|
456
|
+
// scoped to the same parent without taking the type into account.
|
|
457
|
+
// 3. Look for a definition with the same name, neither taking the type
|
|
458
|
+
// nor the parent into account.
|
|
459
|
+
let found = dfns.find(dfn => matchIdlDfn(expected, dfn, { skipArgs: true }));
|
|
460
|
+
if (found) {
|
|
461
|
+
return { expected, found, for: parent, warning: true };
|
|
462
|
+
}
|
|
463
|
+
found = dfns.find(dfn => matchIdlDfn(expected, dfn,
|
|
464
|
+
{ skipArgs: true, skipType: true }));
|
|
465
|
+
if (found) {
|
|
466
|
+
return { expected, found, for: parent };
|
|
467
|
+
}
|
|
468
|
+
found = dfns.find(dfn => matchIdlDfn(expected, dfn,
|
|
469
|
+
{ skipArgs: true, skipType: true, skipFor: true }));
|
|
470
|
+
return { expected, found, for: parent };
|
|
471
|
+
}
|
|
472
|
+
}).filter(missing => !!missing);
|
|
473
|
+
|
|
474
|
+
// Report results
|
|
475
|
+
return {
|
|
476
|
+
css: missingCSSDfns,
|
|
477
|
+
idl: missingIdlDfns
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Checks the CSS and IDL extracts against the dfns extract for all specs in
|
|
484
|
+
* the report.
|
|
485
|
+
*
|
|
486
|
+
* @function
|
|
487
|
+
* @public
|
|
488
|
+
* @param {String} pathToReport Path to the root folder that contains the
|
|
489
|
+
* `index.json` report file and the extracts subfolders.
|
|
490
|
+
* @param {Object} options Check options. Set the "shortname" property to a
|
|
491
|
+
* spec's shortname to only check that spec.
|
|
492
|
+
* @return {Array} The list of specifications along with dfn problems that have
|
|
493
|
+
* been identified. Each entry has `url`, 'crawled`, `shortname` properties to
|
|
494
|
+
* identify the specification, and a `missing` property that is an object that
|
|
495
|
+
* may have `css` and `idl` properties which list missing CSS/IDL definitions.
|
|
496
|
+
*/
|
|
497
|
+
function checkDefinitions(pathToReport, options = {}) {
|
|
498
|
+
const rootFolder = path.resolve(process.cwd(), pathToReport);
|
|
499
|
+
const index = require(path.resolve(rootFolder, 'index.json')).results;
|
|
500
|
+
|
|
501
|
+
// Check all dfns against CSS and IDL extracts
|
|
502
|
+
const checkOptions = {
|
|
503
|
+
rootFolder,
|
|
504
|
+
includeObsolete: !!options.shortname
|
|
505
|
+
};
|
|
506
|
+
const missing = index
|
|
507
|
+
.filter(spec => !options.shortname || spec.shortname === options.shortname)
|
|
508
|
+
.map(spec => {
|
|
509
|
+
const res = {
|
|
510
|
+
url: spec.url,
|
|
511
|
+
crawled: spec.crawled,
|
|
512
|
+
shortname: spec.shortname,
|
|
513
|
+
};
|
|
514
|
+
if (!spec.dfns) {
|
|
515
|
+
return res;
|
|
516
|
+
}
|
|
517
|
+
res.missing = checkSpecDefinitions(spec, checkOptions);
|
|
518
|
+
return res;
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
return missing;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Report missing dfn to the console as Markdown
|
|
527
|
+
*
|
|
528
|
+
* @function
|
|
529
|
+
* @private
|
|
530
|
+
* @param {Object} missing Object that describes missing dfn
|
|
531
|
+
*/
|
|
532
|
+
function reportMissing(missing) {
|
|
533
|
+
const exp = missing.expected;
|
|
534
|
+
const found = missing.found;
|
|
535
|
+
const foundFor = (found && found.for && found.for.length > 0) ?
|
|
536
|
+
' for ' + found.for.map(f => `\`${f}\``).join(',') :
|
|
537
|
+
'';
|
|
538
|
+
console.log(`- \`${exp.linkingText[0]}\` ${exp.type ? `with type \`${exp.type}\`` : ''}` +
|
|
539
|
+
(missing.for ? ` for [\`${missing.for.linkingText[0]}\`](${missing.for.href})` : '') +
|
|
540
|
+
(found ? `, but found [\`${found.linkingText[0]}\`](${found.href}) with type \`${found.type}\`${foundFor}` : ''));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
/**************************************************
|
|
545
|
+
Export methods for use as module
|
|
546
|
+
**************************************************/
|
|
547
|
+
module.exports.checkSpecDefinitions = checkSpecDefinitions;
|
|
548
|
+
module.exports.checkDefinitions = checkDefinitions;
|
|
549
|
+
|
|
550
|
+
// "Inner" functions that the IDL names generator uses to link IDL terms with
|
|
551
|
+
// their definition (see generate-idlnames.js)
|
|
552
|
+
module.exports.getExpectedDfnFromIdlDesc = getExpectedDfnFromIdlDesc;
|
|
553
|
+
module.exports.matchIdlDfn = matchIdlDfn;
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
/**************************************************
|
|
558
|
+
Code run if the code is run as a stand-alone module
|
|
559
|
+
**************************************************/
|
|
560
|
+
if (require.main === module) {
|
|
561
|
+
const pathToReport = process.argv[2];
|
|
562
|
+
const shortname = process.argv[3] || 'all';
|
|
563
|
+
const format = process.argv[4] || 'markdown';
|
|
564
|
+
|
|
565
|
+
const options = (shortname === 'all') ? undefined : { shortname };
|
|
566
|
+
let res = checkDefinitions(pathToReport, options);
|
|
567
|
+
if (shortname === 'all') {
|
|
568
|
+
res = res
|
|
569
|
+
.filter(result => result.missing &&
|
|
570
|
+
!result.missing.obsoleteDfnsModel &&
|
|
571
|
+
((result.missing.css.length > 0) || (result.missing.idl.length > 0)));
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (format === 'json') {
|
|
575
|
+
console.log(JSON.stringify(res, null, 2));
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
res.forEach(result => {
|
|
579
|
+
const missing = result.missing || {css: [], idl: []};
|
|
580
|
+
const errors = ['css', 'idl']
|
|
581
|
+
.map(type => result.missing[type].filter(missing => !missing.warning))
|
|
582
|
+
.flat();
|
|
583
|
+
const warnings = ['css', 'idl']
|
|
584
|
+
.map(type => result.missing[type].filter(missing => missing.warning))
|
|
585
|
+
.flat();
|
|
586
|
+
console.log('<details>');
|
|
587
|
+
console.log(`<summary><b><a href="${result.crawled}">${result.shortname}</a></b> (${errors.length} errors, ${warnings.length} warnings)</summary>`);
|
|
588
|
+
console.log();
|
|
589
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
590
|
+
console.log('All good!');
|
|
591
|
+
}
|
|
592
|
+
if (errors.length > 0) {
|
|
593
|
+
console.log('<details open>');
|
|
594
|
+
console.log(`<summary><i>Errors</i> (${errors.length})</summary>`);
|
|
595
|
+
console.log();
|
|
596
|
+
errors.forEach(reportMissing);
|
|
597
|
+
console.log('</details>');
|
|
598
|
+
}
|
|
599
|
+
if (warnings.length > 0) {
|
|
600
|
+
console.log('<details open>');
|
|
601
|
+
console.log(`<summary><i>Warnings</i> (${warnings.length})</summary>`);
|
|
602
|
+
console.log();
|
|
603
|
+
warnings.forEach(reportMissing);
|
|
604
|
+
console.log('</details>');
|
|
605
|
+
}
|
|
606
|
+
console.log('</details>');
|
|
607
|
+
console.log();
|
|
608
|
+
})
|
|
609
|
+
}
|
|
610
610
|
}
|