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.
Files changed (43) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +158 -158
  3. package/index.js +11 -11
  4. package/package.json +53 -53
  5. package/reffy.js +248 -248
  6. package/src/browserlib/canonicalize-url.mjs +50 -50
  7. package/src/browserlib/create-outline.mjs +352 -352
  8. package/src/browserlib/extract-cssdfn.mjs +319 -319
  9. package/src/browserlib/extract-dfns.mjs +686 -686
  10. package/src/browserlib/extract-elements.mjs +205 -205
  11. package/src/browserlib/extract-headings.mjs +48 -48
  12. package/src/browserlib/extract-ids.mjs +28 -28
  13. package/src/browserlib/extract-links.mjs +28 -28
  14. package/src/browserlib/extract-references.mjs +203 -203
  15. package/src/browserlib/extract-webidl.mjs +134 -134
  16. package/src/browserlib/get-absolute-url.mjs +21 -21
  17. package/src/browserlib/get-generator.mjs +26 -26
  18. package/src/browserlib/get-lastmodified-date.mjs +13 -13
  19. package/src/browserlib/get-title.mjs +11 -11
  20. package/src/browserlib/informative-selector.mjs +16 -16
  21. package/src/browserlib/map-ids-to-headings.mjs +136 -136
  22. package/src/browserlib/reffy.json +53 -53
  23. package/src/cli/check-missing-dfns.js +609 -609
  24. package/src/cli/generate-idlnames.js +430 -430
  25. package/src/cli/generate-idlparsed.js +139 -139
  26. package/src/cli/merge-crawl-results.js +128 -128
  27. package/src/cli/parse-webidl.js +430 -430
  28. package/src/lib/css-grammar-parse-tree.schema.json +109 -109
  29. package/src/lib/css-grammar-parser.js +440 -440
  30. package/src/lib/fetch.js +55 -55
  31. package/src/lib/nock-server.js +119 -119
  32. package/src/lib/specs-crawler.js +605 -603
  33. package/src/lib/util.js +898 -898
  34. package/src/specs/missing-css-rules.json +197 -197
  35. package/src/specs/spec-equivalents.json +149 -149
  36. package/src/browserlib/extract-editors.mjs~ +0 -14
  37. package/src/browserlib/generate-es-dfn-report.sh~ +0 -4
  38. package/src/cli/csstree-grammar-check.js +0 -28
  39. package/src/cli/csstree-grammar-check.js~ +0 -10
  40. package/src/cli/csstree-grammar-parser.js +0 -11
  41. package/src/cli/csstree-grammar-parser.js~ +0 -1
  42. package/src/cli/extract-editors.js~ +0 -38
  43. 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
  }