reffy 4.0.5 → 5.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.
@@ -1,1055 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * The Markdown report generator takes an anomalies report as input and
4
- * generates a human-readable report in Markdown out of it. Depending on
5
- * parameters, the generated report may be a report per spec, a report per
6
- * issue, a dependencies report, or a diff report.
7
- *
8
- * The report generator can be called directly through:
9
- *
10
- * `node generate-report.js [anomalies report] [type]`
11
- *
12
- * where `anomalies report` is the name of a JSON file that contains the
13
- * anomalies report to parse and `type` is an optional parameter that specifies
14
- * the type of report to generate, one of `perspec` (default value) to produce
15
- * a report per spec, `perissue` to produce a report per issue, `dep` to
16
- * produce a dependencies report, or `diff` to produce a diff report. When
17
- * `diff` is used, an extra parameter must be given which must point to the
18
- * reference anomalies report the new report needs to be compared with.
19
- *
20
- * @module markdownGenerator
21
- */
22
-
23
- const requireFromWorkingDirectory = require('../lib/util').requireFromWorkingDirectory;
24
- const fetch = require('../lib/util').fetch;
25
-
26
-
27
- /**
28
- * Compares specs for ordering by title
29
- */
30
- const byTitle = (a, b) => a.title.toUpperCase().localeCompare(b.title.toUpperCase());
31
-
32
- /**
33
- * Returns true when two arrays are equal
34
- */
35
- const arrayEquals = (a, b, prop) =>
36
- (a.length === b.length) &&
37
- a.every(item => !!(prop ? b.find(i => i[prop] === item[prop]) : b.find(i => i === item)));
38
-
39
- /**
40
- * Options for date formatting
41
- */
42
- const dateOptions = {
43
- day: '2-digit',
44
- month: 'long',
45
- year: 'numeric'
46
- };
47
-
48
- const toSlug = name => name.replace(/([A-Z])/g, s => s.toLowerCase())
49
- .replace(/[^a-z0-9]/g, '_')
50
- .replace(/_+/g, '_');
51
-
52
- /**
53
- * Helper function that outputs main crawl info about a spec
54
- *
55
- * @function
56
- */
57
- function writeCrawlInfo(spec, withHeader, w) {
58
- let wres = '';
59
- w = w || (msg => wres += (msg || '') + '\n');
60
-
61
- if (withHeader) {
62
- w('#### Spec info {.info}');
63
- }
64
- else {
65
- w('Spec info:');
66
- }
67
- w();
68
-
69
- let crawledUrl = spec.crawled || spec.latest;
70
- w('- Initial URL: [' + spec.url + '](' + spec.url + ')');
71
- w('- Crawled URL: [' + crawledUrl + '](' + crawledUrl + ')');
72
- if (spec.date) {
73
- w('- Crawled version: ' + spec.date);
74
- }
75
- if (spec.nightly) {
76
- w('- Editor\'s Draft: [' + spec.nightly.url + '](' + spec.nightly.url + ')');
77
- }
78
- if (spec.release) {
79
- w('- Latest published version: [' + spec.release.url + '](' + spec.release.url + ')');
80
- }
81
- if (spec.repository) {
82
- let githubcom = spec.repository.match(/^https:\/\/github.com\/([^\/]*)\/([^\/]*)/);
83
- let repositoryName = spec.repository;
84
- if (githubcom) {
85
- repositoryName = 'GitHub ' + githubcom[1] + '/' + githubcom[2];
86
- }
87
- w('- Repository: [' + repositoryName + '](' + spec.repository + ')');
88
- }
89
- w('- Shortname: ' + (spec.shortname || 'no shortname'));
90
- return wres;
91
- }
92
-
93
-
94
- function writeDependenciesInfo(spec, results, withHeader, w) {
95
- let wres = '';
96
- w = w || (msg => wres += (msg || '') + '\n');
97
-
98
- if (withHeader) {
99
- w('#### Known dependencies on this specification {.dependencies}');
100
- w();
101
- }
102
-
103
- if (spec.report.referencedBy.normative.length > 0) {
104
- w('Normative references to this spec from:');
105
- w();
106
- spec.report.referencedBy.normative.forEach(s => {
107
- w('- [' + s.title + '](' + s.crawled + ')');
108
- });
109
- }
110
- else {
111
- w('No normative reference to this spec from other specs.');
112
- }
113
- w();
114
-
115
- // Check the list of specifications that should normatively reference
116
- // this specification because they use IDL content it defines.
117
- let shouldBeReferencedBy = results.filter(s =>
118
- s.report.missingWebIdlRef &&
119
- s.report.missingWebIdlRef.find(i =>
120
- i.refs.find(ref => (ref.url === spec.url))));
121
- if (shouldBeReferencedBy.length > 0) {
122
- w('Although they do not, the following specs should also normatively' +
123
- ' reference this spec because they use IDL terms it defines:');
124
- w();
125
- shouldBeReferencedBy.forEach(s => {
126
- w('- [' + s.title + '](' + s.crawled + ')');
127
- });
128
- w();
129
- }
130
-
131
- if (spec.report.referencedBy.informative.length > 0) {
132
- w('Informative references to this spec from:');
133
- w();
134
- spec.report.referencedBy.informative.forEach(s => {
135
- w('- [' + s.title + '](' + s.crawled + ')');
136
- });
137
- }
138
- else {
139
- w('No informative reference to this spec from other specs.');
140
- }
141
- return wres;
142
- }
143
-
144
- /**
145
- * Outputs a human-readable Markdown anomaly report from a crawl report,
146
- * with one entry per spec.
147
- *
148
- * The function spits the report to the console.
149
- *
150
- * @function
151
- */
152
- function generateReportPerSpec(study) {
153
- var count = 0;
154
- let wres = '';
155
- const w = msg => wres += (msg || '') + '\n';
156
- const results = study.results;
157
-
158
- w('% ' + (study.title || 'Reffy crawl results'));
159
- w('% Reffy');
160
- w('% ' + (new Date(study.date)).toLocaleDateString('en-US', dateOptions));
161
- w();
162
-
163
- const specReport = spec => {
164
- // Prepare anomaly flags
165
- let flags = ['spec'];
166
- if (spec.report.error) {
167
- flags.push('error');
168
- }
169
- else {
170
- if (!spec.report.ok) {
171
- flags.push('anomaly');
172
- }
173
- flags = flags.concat(Object.keys(spec.report)
174
- .filter(anomaly => (anomaly !== 'referencedBy'))
175
- .filter(anomaly => (Array.isArray(spec.report[anomaly]) ?
176
- (spec.report[anomaly].length > 0) :
177
- !!spec.report[anomaly])));
178
- }
179
- let attr = flags.reduce((res, anomaly) =>
180
- res + (res ? ' ' : '') + 'data-' + anomaly + '=true', '');
181
-
182
- w('### ' + spec.title + ' {' + attr + '}');
183
- w();
184
- writeCrawlInfo(spec, true, w);
185
- w();
186
-
187
- const report = spec.report;
188
- w('#### Potential issue(s) {.anomalies}');
189
- w();
190
- if (report.ok) {
191
- w('This specification looks good!');
192
- }
193
- else if (report.error) {
194
- w('The following network or parsing error occurred:');
195
- w('`' + report.error + '`');
196
- w();
197
- w('Reffy could not render this specification as a DOM tree and' +
198
- ' cannot say anything about it as a result. In particular,' +
199
- ' it cannot include content defined in this specification' +
200
- ' in the analysis of other specifications crawled in this' +
201
- ' report.');
202
- }
203
- else {
204
- if (report.noNormativeRefs) {
205
- w('- No normative references found');
206
- }
207
- if (report.hasInvalidIdl) {
208
- w('- Invalid WebIDL content found');
209
- }
210
- if (report.hasObsoleteIdl) {
211
- w('- Obsolete WebIDL constructs found');
212
- }
213
- if (report.noRefToWebIDL) {
214
- w('- Spec uses WebIDL but does not reference it normatively');
215
- }
216
- if (report.unknownExposedNames &&
217
- (report.unknownExposedNames.length > 0)) {
218
- w('- Unknown [Exposed] names used: ' +
219
- report.unknownExposedNames.map(name => '`' + name + '`').join(', '));
220
- }
221
- if (report.unknownIdlNames &&
222
- (report.unknownIdlNames.length > 0)) {
223
- w('- Unknown WebIDL names used: ' +
224
- report.unknownIdlNames.map(name => '`' + name + '`').join(', '));
225
- }
226
- if (report.redefinedIdlNames &&
227
- (report.redefinedIdlNames.length > 0)) {
228
- w('- WebIDL names also defined elsewhere: ');
229
- report.redefinedIdlNames.map(i => {
230
- w(' * `' + i.name + '` also defined in ' +
231
- i.refs.map(ref => ('[' + ref.title + '](' + ref.crawled + ')')).join(' and '));
232
- });
233
- }
234
- if (report.missingWebIdlRef &&
235
- (report.missingWebIdlRef.length > 0)) {
236
- w('- Missing references for WebIDL names: ');
237
- report.missingWebIdlRef.map(i => {
238
- w(' * `' + i.name + '` defined in ' +
239
- i.refs.map(ref => ('[' + ref.title + '](' + ref.crawled + ')')).join(' or '));
240
- });
241
- }
242
- [
243
- {prop: 'css', warning: false, title: 'No definition for CSS properties'},
244
- {prop: 'idl', warning: false, title: 'No definition for IDL properties'},
245
- {prop: 'css', warning: true, title: 'Possibly no definition for CSS properties'},
246
- {prop: 'idl', warning: true, title: 'Possibly no definition for IDL properties'}
247
- ].forEach(type => {
248
- if (report.missingDfns && report.missingDfns[type.prop] &&
249
- (report.missingDfns[type.prop].filter(r => !!r.warning === type.warning).length > 0)) {
250
- w('- ' + type.title + ': ');
251
- report.missingDfns[type.prop].filter(r => !!r.warning === type.warning).map(missing => {
252
- const exp = missing.expected;
253
- const found = missing.found;
254
- const foundFor = (found && found.for && found.for.length > 0) ?
255
- ' for ' + found.for.map(f => '`' + f + '`').join(',') :
256
- '';
257
- w(' * `' + exp.linkingText[0] + '`' +
258
- (exp.type ? ' with type `' + exp.type + '`' : '') +
259
- (missing.for ? ' for [`' + missing.for.linkingText[0] + '`](' + missing.for.href + ')' : '') +
260
- (found ? ', but found [`' + found.linkingText[0] + '`](' + found.href + ') with type `' + found.type + '`' + foundFor : ''));
261
- });
262
- }
263
- });
264
- if (report.missingLinkRef &&
265
- (report.missingLinkRef.length > 0)) {
266
- w('- Missing references for links: ');
267
- report.missingLinkRef.map(l => {
268
- w(' * [`' + l + '`](' + l + ')');
269
- });
270
- }
271
- if (report.inconsistentRef &&
272
- (report.inconsistentRef.length > 0)) {
273
- w('- Inconsistent references for links: ');
274
- report.inconsistentRef.map(l => {
275
- w(' * [`' + l.link + '`](' + l.link + '), related reference "' + l.ref.name + '" uses URL [`' + l.ref.url + '`](' + l.ref.url + ')');
276
- });
277
- }
278
- if (report.xrefs) {
279
- [
280
- { prop: 'notExported', title: 'External links to private terms' },
281
- { prop: 'notDfn', title: 'External links that neither target definitions nor headings' },
282
- { prop: 'brokenLinks', title: 'Broken external links' },
283
- { prop: 'evolvingLinks', title: 'External links to terms that no longer exist in the latest version of the targeted specification' },
284
- { prop: 'outdatedSpecs', title: 'External links to outdated specs' },
285
- { prop: 'datedUrls', title: 'External links that use a dated URL' }
286
- ].forEach(type => {
287
- if (report.xrefs[type.prop] && (report.xrefs[type.prop].length > 0)) {
288
- w('- ' + type.title + ':');
289
- report.xrefs[type.prop].map(l => {
290
- w(' * [`' + l + '`](' + l + ')');
291
- })
292
- }
293
- });
294
- }
295
- }
296
- w();
297
- writeDependenciesInfo(spec, results, true, w);
298
- w();
299
- w();
300
- };
301
-
302
-
303
- const orgs = [...new Set(study.results.map(r => r.organization))].sort();
304
- for (let org of orgs) {
305
- w(`# ${org} {#${toSlug(org)}}`);
306
- w();
307
- const groups = [...new Set(study.results.filter(r => r.organization === org).map(r => r.groups.map(g => g.name)).flat())].sort()
308
- for (let group of groups) {
309
- w(`## ${group} {#${toSlug(group)}}`);
310
- w();
311
- study.results.filter(r => r.organization === org && r.groups.find(g => g.name === group)).forEach(specReport);
312
- }
313
- }
314
-
315
- w();
316
- w();
317
-
318
- return wres;
319
- }
320
-
321
-
322
- /**
323
- * Outputs a human-readable Markdown anomaly report from a crawl report,
324
- * sorted by type of anomaly.
325
- *
326
- * The function spits the report to the console.
327
- *
328
- * @function
329
- */
330
- function generateReportPerIssue(study) {
331
- let wres = '';
332
- const w = msg => wres += (msg || '') + '\n';
333
-
334
- let count = 0;
335
- let results = study.results;
336
-
337
- w('% ' + (study.title || 'Reffy crawl results'));
338
- w('% Reffy');
339
- w('% ' + (new Date(study.date)).toLocaleDateString('en-US', dateOptions));
340
- w();
341
-
342
- count = results.length;
343
- w('' + count + ' specification' + ((count > 1) ? 's' : '') + ' were crawled in this report.');
344
- w();
345
- w();
346
-
347
- let parsingErrors = results.filter(spec => spec.report.error);
348
- if (parsingErrors.length > 0) {
349
- w('## Specifications that could not be rendered');
350
- w();
351
- w('Reffy could not fetch or render these specifications for some reason.' +
352
- ' This may happen when a network error occurred or when a specification' +
353
- ' uses an old version of ReSpec.');
354
- w();
355
- count = 0;
356
- parsingErrors.forEach(spec => {
357
- count += 1;
358
- w('- [' + spec.title + '](' + spec.crawled + '): `' + spec.report.error + '`');
359
- });
360
- w();
361
- w('=> ' + count + ' specification' + ((count > 1) ? 's' : '') + ' found');
362
- w();
363
- w();
364
-
365
- // Remove specs that could not be parsed from the rest of the report
366
- results = results.filter(spec => !spec.report.error);
367
- }
368
-
369
-
370
- count = 0;
371
- w('## Specifications without normative dependencies');
372
- w();
373
- results
374
- .filter(spec => spec.report.noNormativeRefs)
375
- .forEach(spec => {
376
- count += 1;
377
- w('- [' + spec.title + '](' + spec.crawled + ')');
378
- });
379
- w();
380
- w('=> ' + count + ' specification' + ((count > 1) ? 's' : '') + ' found');
381
- if (count > 0) {
382
- w();
383
- w('Basically all specifications have normative dependencies on some other' +
384
- ' specification. Reffy could not find any normative dependencies for the' +
385
- ' specifications mentioned above, which seems strange.');
386
- }
387
- w();
388
- w();
389
-
390
- count = 0;
391
- w('## List of specifications with invalid WebIDL content');
392
- w();
393
- results
394
- .filter(spec => spec.report.hasInvalidIdl)
395
- .forEach(spec => {
396
- count += 1;
397
- w('- [' + spec.title + '](' + spec.crawled + ')');
398
- });
399
- w();
400
- w('=> ' + count + ' specification' + ((count > 1) ? 's' : '') + ' found');
401
- if (count > 0) {
402
- w();
403
- w('WebIDL continues to evolve. Reffy may incorrectly report as invalid' +
404
- ' perfectly valid WebIDL content if the specification uses bleeding-edge' +
405
- ' WebIDL features');
406
- }
407
- w();
408
- w();
409
-
410
- count = 0;
411
- w('## List of specifications with obsolete WebIDL constructs');
412
- w();
413
- results
414
- .filter(spec => spec.report.hasObsoleteIdl)
415
- .forEach(spec => {
416
- count += 1;
417
- w('- [' + spec.title + '](' + spec.crawled + ')');
418
- });
419
- w();
420
- w('=> ' + count + ' specification' + ((count > 1) ? 's' : '') + ' found');
421
- if (count > 0) {
422
- w();
423
- w('A typical example is the use of `[]` instead of `FrozenArray`.');
424
- }
425
- w();
426
- w();
427
-
428
- count = 0;
429
- w('## Specifications that use WebIDL but do not reference the WebIDL spec');
430
- w();
431
- results.forEach(spec => {
432
- if (spec.report.noRefToWebIDL) {
433
- count += 1;
434
- w('- [' + spec.title + '](' + spec.crawled + ')');
435
- }
436
- });
437
- w();
438
- w('=> ' + count + ' specification' + ((count > 1) ? 's' : '') + ' found');
439
- if (count > 0) {
440
- w();
441
- ('All specifications that define WebIDL content should have a ' +
442
- ' **normative** reference to the WebIDL specification. ' +
443
- ' Some specifications listed here may reference the WebIDL' +
444
- ' specification informatively, but that is not enough!');
445
- }
446
- w();
447
- w();
448
-
449
-
450
- count = 0;
451
- w('## List of [Exposed] names not defined in the specifications crawled');
452
- w();
453
- var idlNames = {};
454
- results.forEach(spec => {
455
- if (!spec.report.unknownExposedNames ||
456
- (spec.report.unknownExposedNames.length === 0)) {
457
- return;
458
- }
459
- spec.report.unknownExposedNames.forEach(name => {
460
- if (!idlNames[name]) {
461
- idlNames[name] = [];
462
- }
463
- idlNames[name].push(spec);
464
- });
465
- });
466
- Object.keys(idlNames).sort().forEach(name => {
467
- count += 1;
468
- w('- `' + name + '` used in ' +
469
- idlNames[name].map(ref => ('[' + ref.title + '](' + ref.crawled + ')')).join(', '));
470
- });
471
- w();
472
- w('=> ' + count + ' [Exposed] name' + ((count > 1) ? 's' : '') + ' found');
473
- if (count > 0) {
474
- w();
475
- w('Please keep in mind that Reffy only knows about IDL terms defined in the' +
476
- ' specifications that were crawled **and** that do not have invalid IDL content.');
477
- }
478
- w();
479
- w();
480
-
481
-
482
- count = 0;
483
- w('## List of WebIDL names not defined in the specifications crawled');
484
- w();
485
- idlNames = {};
486
- results.forEach(spec => {
487
- if (!spec.report.unknownIdlNames ||
488
- (spec.report.unknownIdlNames.length === 0)) {
489
- return;
490
- }
491
- spec.report.unknownIdlNames.forEach(name => {
492
- if (!idlNames[name]) {
493
- idlNames[name] = [];
494
- }
495
- idlNames[name].push(spec);
496
- });
497
- });
498
- Object.keys(idlNames).sort().forEach(name => {
499
- count += 1;
500
- w('- `' + name + '` used in ' +
501
- idlNames[name].map(ref => ('[' + ref.title + '](' + ref.crawled + ')')).join(', '));
502
- });
503
- w();
504
- w('=> ' + count + ' WebIDL name' + ((count > 1) ? 's' : '') + ' found');
505
- if (count > 0) {
506
- w();
507
- w('Some of them may be type errors in specs (e.g. "int" does not exist, "Array" cannot be used on its own, etc.)');
508
- w('Also, please keep in mind that Reffy only knows about IDL terms defined in the' +
509
- ' specifications that were crawled **and** that do not have invalid IDL content.');
510
- }
511
- w();
512
- w();
513
-
514
- count = 0;
515
- w('## List of WebIDL names defined in more than one spec');
516
- w();
517
- idlNames = {};
518
- results.forEach(spec => {
519
- if (!spec.report.redefinedIdlNames ||
520
- (spec.report.redefinedIdlNames.length === 0)) {
521
- return;
522
- }
523
- spec.report.redefinedIdlNames.forEach(i => {
524
- if (!idlNames[i.name]) {
525
- idlNames[i.name] = [];
526
- }
527
- idlNames[i.name].push(spec);
528
- });
529
- });
530
- Object.keys(idlNames).sort().forEach(name => {
531
- count += 1;
532
- w('- `' + name + '` defined in ' +
533
- idlNames[name].map(ref => ('[' + ref.title + '](' + ref.crawled + ')')).join(' and '));
534
- });
535
- w();
536
- w('=> ' + count + ' WebIDL name' + ((count > 1) ? 's' : '') + ' found');
537
- if (count > 0) {
538
- w();
539
- w('"There can be only one"...');
540
- }
541
- w();
542
- w();
543
-
544
- count = 0;
545
- var countrefs = 0;
546
- w('## Missing references for WebIDL names');
547
- w();
548
- results.forEach(spec => {
549
- if (spec.report.missingWebIdlRef &&
550
- (spec.report.missingWebIdlRef.length > 0)) {
551
- count += 1;
552
- if (spec.report.missingWebIdlRef.length === 1) {
553
- countrefs += 1;
554
- let i = spec.report.missingWebIdlRef[0];
555
- w('- [' + spec.title + '](' + spec.crawled + ')' +
556
- ' uses `' + i.name + '` but does not reference ' +
557
- i.refs.map(ref => ('[' + ref.title + '](' + ref.crawled + ')')).join(' or '));
558
- }
559
- else {
560
- w('- [' + spec.title + '](' + spec.crawled + ') uses:');
561
- spec.report.missingWebIdlRef.map(i => {
562
- countrefs += 1;
563
- w(' * `' + i.name + '` but does not reference ' +
564
- i.refs.map(ref => ('[' + ref.title + '](' + ref.crawled + ')')).join(' or '));
565
- });
566
- }
567
- }
568
- });
569
- w();
570
- w('=> ' + countrefs + ' missing reference' + ((countrefs > 1) ? 's' : '') +
571
- ' for IDL definitions found in ' + count + ' specification' +
572
- ((count > 1) ? 's' : ''));
573
- w();
574
- w();
575
-
576
- [
577
- {prop: 'css', warning: false, title: 'No definition for CSS properties'},
578
- {prop: 'idl', warning: false, title: 'No definition for IDL properties'},
579
- {prop: 'css', warning: true, title: 'Possibly no definition for CSS properties'},
580
- {prop: 'idl', warning: true, title: 'Possibly no definition for IDL properties'}
581
- ].forEach(type => {
582
- count = 0;
583
- countrefs = 0;
584
- w('## ' + type.title);
585
- w();
586
-
587
- results.forEach(spec => {
588
- if (spec.report.missingDfns &&
589
- spec.report.missingDfns[type.prop] &&
590
- (spec.report.missingDfns[type.prop].filter(r => !!r.warning === type.warning).length > 0)) {
591
- count += 1;
592
-
593
- w('- [' + spec.title + '](' + spec.crawled + '):');
594
- spec.report.missingDfns[type.prop].filter(r => !!r.warning === type.warning).map(missing => {
595
- countrefs += 1;
596
- const exp = missing.expected;
597
- const found = missing.found;
598
- const foundFor = (found && found.for && found.for.length > 0) ?
599
- ' for ' + found.for.map(f => '`' + f + '`').join(',') :
600
- '';
601
- w(' * `' + exp.linkingText[0] + '`' +
602
- (exp.type ? ' with type `' + exp.type + '`' : '') +
603
- (missing.for ? ' for [`' + missing.for.linkingText[0] + '`](' + missing.for.href + ')' : '') +
604
- (found ? ', but found [`' + found.linkingText[0] + '`](' + found.href + ') with type `' + found.type + '`' + foundFor : ''));
605
- });
606
- }
607
- });
608
-
609
- w();
610
- w('=> ' + countrefs + ' propert' + ((countrefs > 1) ? 'ies' : 'y') +
611
- ' without definition found in ' + count + ' specification' +
612
- ((count > 1) ? 's' : ''));
613
- w();
614
- w();
615
- });
616
-
617
-
618
- count = 0;
619
- countrefs = 0;
620
- w('## Missing references based on document links');
621
- w();
622
- results.forEach(spec => {
623
- if (spec.report.missingLinkRef &&
624
- (spec.report.missingLinkRef.length > 0)) {
625
- count += 1;
626
- if (spec.report.missingLinkRef.length === 1) {
627
- countrefs += 1;
628
- let l = spec.report.missingLinkRef[0];
629
- w('- [' + spec.title + '](' + spec.crawled + ')' +
630
- ' links to [`' + l + '`](' + l + ') but does not list it' +
631
- ' in its references');
632
- }
633
- else {
634
- w('- [' + spec.title + '](' + spec.crawled + ') links to:');
635
- spec.report.missingLinkRef.forEach(l => {
636
- countrefs++;
637
- w(' * [`' + l + '`](' + l + ') but does not list it ' +
638
- 'in its references');
639
- });
640
- }
641
- }
642
- });
643
- w();
644
- w('=> ' + countrefs + ' missing reference' + ((countrefs > 1) ? 's' : '') +
645
- ' for links found in ' + count + ' specification' +
646
- ((count > 1) ? 's' : ''));
647
- if (count > 0) {
648
- w();
649
- w('Any link to an external document from within a specification should' +
650
- ' trigger the creation of a corresponding entry in the references' +
651
- ' section.');
652
- w();
653
- w('Note Reffy only reports on links to "well-known" specs and ignores' +
654
- ' links to non-usual specs (e.g. PDF documents, etc.) for now.');
655
- }
656
- w();
657
- w();
658
-
659
- count = 0;
660
- countrefs = 0;
661
- w('## Reference URL is inconsistent with URL used in document links');
662
- w();
663
- results.forEach(spec => {
664
- if (spec.report.inconsistentRef &&
665
- (spec.report.inconsistentRef.length > 0)) {
666
- count += 1;
667
- if (spec.report.inconsistentRef.length === 1) {
668
- countrefs += 1;
669
- let l = spec.report.inconsistentRef[0];
670
- w('- [' + spec.title + '](' + spec.crawled + ')' +
671
- ' links to [`' + l.link + '`](' + l.link + ') but related reference "' + l.ref.name + '" uses URL [`' + l.ref.url + '`](' + l.ref.url + ')');
672
- }
673
- else {
674
- w('- [' + spec.title + '](' + spec.crawled + ') links to:');
675
- spec.report.inconsistentRef.forEach(l => {
676
- countrefs++;
677
- w(' * [`' + l.link + '`](' + l.link + ') but related reference "' + l.ref.name + '" uses URL [`' + l.ref.url + '`](' + l.ref.url + ')');
678
- });
679
- }
680
- }
681
- });
682
- w();
683
- w('=> ' + countrefs + ' inconsistent reference' + ((countrefs > 1) ? 's' : '') +
684
- ' for links found in ' + count + ' specification' +
685
- ((count > 1) ? 's' : ''));
686
- if (count > 0) {
687
- w();
688
- w('Links in the body of a specification should be to the same document' +
689
- ' as that pointed to by the related reference in the References section.' +
690
- ' The specifications reported here use a different URL. For instance,' +
691
- ' they may use a link to the Editor\'s Draft but target the latest' +
692
- ' published version in the References section.' +
693
- ' There should be some consistency across the specification.');
694
- }
695
- w();
696
- w();
697
-
698
- [
699
- { prop: 'notExported', title: 'External links to private terms' },
700
- { prop: 'notDfn', title: 'External links that neither target definitions nor headings' },
701
- { prop: 'brokenLinks', title: 'Broken external links' },
702
- { prop: 'evolvingLinks', title: 'External links to terms that no longer exist in the latest version of the targeted specification' },
703
- { prop: 'outdatedSpecs', title: 'External links to outdated specs' },
704
- { prop: 'datedUrls', title: 'External links that use a dated URL' }
705
- ].forEach(type => {
706
- count = 0;
707
- countrefs = 0;
708
- w('## ' + type.title);
709
- w();
710
-
711
- results.forEach(spec => {
712
- if (spec.report.xrefs &&
713
- spec.report.xrefs[type.prop] &&
714
- (spec.report.xrefs[type.prop].length > 0)) {
715
- count += 1;
716
-
717
- w('- [' + spec.title + '](' + spec.crawled + '):');
718
- spec.report.xrefs[type.prop].map(l => {
719
- countrefs += 1;
720
- w(' * [`' + l + '`](' + l + ')');
721
- });
722
- }
723
- });
724
-
725
- w();
726
- w('=> ' + countrefs + ' problematic external link' + ((countrefs > 1) ? 's' : '') +
727
- ' found in ' + count + ' specification' +
728
- ((count > 1) ? 's' : ''));
729
- w();
730
- w();
731
- });
732
-
733
-
734
- return wres;
735
- }
736
-
737
-
738
- /**
739
- * Outputs a human-readable Markdown dependencies report from a crawl report,
740
- * one entry per spec.
741
- *
742
- * The function spits the report to the console.
743
- *
744
- * @function
745
- */
746
- function generateDependenciesReport(study) {
747
- let wres = '';
748
- const w = msg => wres += (msg || '') + '\n';
749
-
750
- let count = 0;
751
- const results = study.results;
752
-
753
- w('# Reffy dependencies report');
754
- w();
755
- w('Reffy is a spec exploration tool.' +
756
- ' It takes a list of specifications as input, fetches and parses the latest Editor\'s Draft' +
757
- ' of each of these specifications to study the IDL content that it defines, the links that it' +
758
- ' contains, and the normative and informative references that it lists.');
759
- w();
760
- w('The report below lists incoming links for each specification, in other words the list' +
761
- ' of specifications that normatively or informatively reference a given specification.');
762
- w();
763
- w('By definition, Reffy only knows about incoming links from specifications that have been' +
764
- ' crawled and that could successfully be parsed. Other specifications that Reffy does' +
765
- ' not know anything about may reference specifications listed here.');
766
- w();
767
- results.forEach(spec => {
768
- w('## ' + spec.title);
769
- w();
770
- writeCrawlInfo(spec, false, w);
771
- w();
772
- writeDependenciesInfo(spec, results, false, w);
773
- w();
774
- w();
775
- });
776
-
777
- return wres;
778
- }
779
-
780
-
781
- /**
782
- * Outputs a human-readable diff between two crawl reports, one entry per spec.
783
- *
784
- * The function spits the report to the console.
785
- *
786
- * @function
787
- */
788
- function generateDiffReport(study, refStudy, options) {
789
- options = options || {};
790
- let wres = '';
791
- const w = msg => wres += (msg || '') + '\n';
792
-
793
- const results = study.results;
794
- const resultsRef = refStudy.results;
795
-
796
- // Compute diff for all specs
797
- // (note we're only interested in specs that are part in the new crawl,
798
- // and won't report on specs that were there before and got dropped)
799
- let resultsDiff = results.map(spec => {
800
- let ref = resultsRef.find(s => s.url === spec.url) || {
801
- missing: true,
802
- report: {
803
- unknownExposedNames: [],
804
- unknownIdlNames: [],
805
- redefinedIdlNames: [],
806
- missingWebIdlRef: [],
807
- missingLinkRef: [],
808
- inconsistentRef: []
809
- }
810
- };
811
-
812
- const report = spec.report;
813
- const reportRef = ref.report;
814
-
815
- const getSimpleDiff = prop =>
816
- (report[prop] !== reportRef[prop]) ?
817
- {
818
- ins: (typeof report[prop] !== 'undefined') ? report[prop] : null,
819
- del: (typeof reportRef[prop] !== 'undefined') ? reportRef[prop] : null
820
- } :
821
- null;
822
- const getArrayDiff = (prop, key) =>
823
- (!arrayEquals(report[prop], reportRef[prop], key) &&
824
- (!options.onlyNew || report[prop].find(item => !reportRef[prop].find(i => (key ? i[key] === item[key] : i === item))))) ?
825
- {
826
- ins: report[prop].filter(item => !reportRef[prop].find(i => (key ? i[key] === item[key] : i === item))),
827
- del: reportRef[prop].filter(item => !report[prop].find(i => (key ? i[key] === item[key] : i === item)))
828
- } :
829
- null;
830
-
831
- // Compute diff between new and ref report for that spec
832
- const diff = {
833
- title: (spec.title !== ref.title) ? {
834
- ins: (typeof spec.title !== 'undefined') ? spec.title : null,
835
- del: (typeof ref.title !== 'undefined') ? ref.title : null
836
- } : null,
837
- ok: getSimpleDiff('ok'),
838
- error: getSimpleDiff('error'),
839
- noNormativeRefs: getSimpleDiff('noNormativeRefs'),
840
- noRefToWebIDL: getSimpleDiff('noRefToWebIDL'),
841
- hasInvalidIdl: getSimpleDiff('hasInvalidIdl'),
842
- hasObsoleteIdl: getSimpleDiff('hasObsoleteIdl'),
843
- unknownExposedNames: getArrayDiff('unknownExposedNames'),
844
- unknownIdlNames: getArrayDiff('unknownIdlNames'),
845
- redefinedIdlNames: getArrayDiff('redefinedIdlNames', 'name'),
846
- missingWebIdlRef: getArrayDiff('missingWebIdlRef', 'name'),
847
- missingLinkRef: getArrayDiff('missingLinkRef'),
848
- inconsistentRef: getArrayDiff('inconsistentRef', 'link')
849
- };
850
-
851
- return {
852
- title: spec.title,
853
- shortname: spec.shortname,
854
- date: spec.date,
855
- url: spec.url,
856
- release: spec.release,
857
- nightly: spec.nightly,
858
- repository: spec.repository,
859
- isNewSpec: ref.missing,
860
- hasDiff: Object.keys(diff).some(key => diff[key] !== null),
861
- diff
862
- };
863
- });
864
-
865
- if (!options.onlyNew) {
866
- resultsDiff = resultsDiff.concat(resultsRef
867
- .map(spec => {
868
- let ref = results.find(s => s.url === spec.url);
869
- if (ref) return null;
870
- return {
871
- title: spec.title,
872
- shortname: spec.shortname,
873
- date: spec.date,
874
- url: spec.url,
875
- release: spec.release,
876
- nightly: spec.nightly,
877
- crawled: spec.crawled,
878
- repository: spec.repository,
879
- isUnknownSpec: true,
880
- hasDiff: true
881
- };
882
- })
883
- .filter(spec => !!spec));
884
- resultsDiff.sort(byTitle);
885
- }
886
-
887
- w('% Diff between report from "' +
888
- (new Date(study.date)).toLocaleDateString('en-US', dateOptions) +
889
- '" and reference report from "' +
890
- (new Date(refStudy.date)).toLocaleDateString('en-US', dateOptions) +
891
- '"');
892
- w('% Reffy');
893
- w('% ' + (new Date(study.date)).toLocaleDateString('en-US', dateOptions));
894
- w();
895
-
896
- resultsDiff.forEach(spec => {
897
- // Nothing to report if crawl result is the same
898
- if (!spec.hasDiff) {
899
- return;
900
- }
901
-
902
- w('## ' + spec.title);
903
- w();
904
-
905
- let crawledUrl = spec.crawled || spec.latest;
906
- w('- Initial URL: [' + spec.url + '](' + spec.url + ')');
907
- w('- Crawled URL: [' + crawledUrl + '](' + crawledUrl + ')');
908
- if (spec.nightly && (spec.nightly.url !== crawledUrl)) {
909
- w('- Editor\'s Draft: [' + spec.nightly.url + '](' + spec.nightly.url + ')');
910
- }
911
- if (spec.repository) {
912
- let githubcom = spec.repository.match(/^https:\/\/github.com\/([^\/]*)\/([^\/]*)/);
913
- let repositoryName = spec.repository;
914
- if (githubcom) {
915
- repositoryName = 'GitHub ' + githubcom[1] + '/' + githubcom[2];
916
- }
917
- w('- Repository: [' + repositoryName + '](' + spec.repository + ')');
918
- }
919
-
920
- if (spec.isNewSpec) {
921
- w('- This specification was not in the reference crawl report.');
922
- w();
923
- w();
924
- return;
925
- }
926
-
927
- if (spec.isUnknownSpec) {
928
- w('- This specification is not in the new crawl report.');
929
- w();
930
- w();
931
- return;
932
- }
933
-
934
- const diff = spec.diff;
935
- const simpleDiff = prop =>
936
- ((diff[prop].ins !== null) ? '*INS* ' + diff[prop].ins : '') +
937
- (((diff[prop].ins !== null) && (diff[prop].del !== null)) ? ' / ' : '') +
938
- ((diff[prop].del !== null) ? '*DEL* ' + diff[prop].del : '');
939
- const arrayDiff = (prop, key) =>
940
- ((diff[prop].ins.length > 0) ? '*INS* ' + diff[prop].ins.map(i => (key ? i[key] : i)).join(', ') : '') +
941
- (((diff[prop].ins.length > 0) && (diff[prop].del.length > 0)) ? ' / ' : '') +
942
- ((diff[prop].del.length > 0) ? '*DEL* ' + diff[prop].del.map(i => (key ? i[key] : i)).join(', ') : '');
943
-
944
- [
945
- { title: 'Spec title', prop: 'title', diff: 'simple' },
946
- { title: 'Spec is OK', prop: 'ok', diff: 'simple' },
947
- { title: 'Spec could not be rendered', prop: 'error', diff: 'simple' },
948
- { title: 'No normative references found', prop: 'noNormativeRefs', diff: 'simple' },
949
- { title: 'Invalid WebIDL content found', prop: 'hasInvalidIdl', diff: 'simple' },
950
- { title: 'Obsolete WebIDL constructs found', prop: 'hasObsoleteIdl', diff: 'simple' },
951
- { title: 'Spec does not reference WebIDL normatively', prop: 'noRefToWebIDL', diff: 'simple' },
952
- { title: 'Unknown [Exposed] names used', prop: 'unknownExposedNames', diff: 'array' },
953
- { title: 'Unknown WebIDL names used', prop: 'unknownIdlNames', diff: 'array' },
954
- { title: 'WebIDL names also defined elsewhere', prop: 'redefinedIdlNames', diff: 'array', key: 'name' },
955
- { title: 'Missing references for WebIDL names', prop: 'missingWebIdlRef', diff: 'array', key: 'name' },
956
- { title: 'Missing references for links', prop: 'missingLinkRef', diff: 'array' },
957
- { title: 'Inconsistent references for links', prop: 'inconsistentRef', diff: 'array', key: 'link' }
958
- ].forEach(item => {
959
- // Only report actual changes, and don't report other changes when
960
- // the spec could not be rendered in one of the crawl reports
961
- if (diff[item.prop] && ((item.prop === 'error') || (item.prop === 'title') || (item.prop === 'latest') || !diff.error)) {
962
- w('- ' + item.title + ': ' + ((item.diff === 'simple') ?
963
- simpleDiff(item.prop) :
964
- arrayDiff(item.prop, item.key)));
965
- }
966
- });
967
- w();
968
- w();
969
- });
970
-
971
- return wres;
972
- }
973
-
974
-
975
- /**
976
- * Main function that generates a Markdown report from a study file.
977
- *
978
- * @function
979
- * @param {String} studyFile Path to the study file to parse
980
- * @param {Object} options Type of report to generate and other options
981
- * @return {String} The generated report
982
- */
983
- async function generateReport(studyFile, options) {
984
- options = options || {};
985
- if (!studyFile) {
986
- throw new Error('Required filename parameter missing');
987
- }
988
- if (options.diffReport && !options.refStudyFile) {
989
- throw new Error('Required filename to reference crawl for diff missing');
990
- }
991
-
992
- let study;
993
- try {
994
- study = requireFromWorkingDirectory(studyFile);
995
- } catch (e) {
996
- throw new Error('Impossible to read ' + studyFile + ': ' + e);
997
- }
998
-
999
- let refStudy = {};
1000
- if (options.diffReport) {
1001
- if (options.refStudyFile.startsWith('http')) {
1002
- try {
1003
- let response = await fetch(options.refStudyFile, { nolog: true });
1004
- refStudy = await response.json();
1005
- }
1006
- catch (e) {
1007
- throw new Error('Impossible to fetch ' + options.refStudyFile + ': ' + e);
1008
- }
1009
- return generateDiffReport(study, refStudy, { onlyNew: options.onlyNew });
1010
- }
1011
- else {
1012
- try {
1013
- refStudy = requireFromWorkingDirectory(options.refStudyFile);
1014
- } catch (e) {
1015
- throw new Error('Impossible to read ' + options.refStudyFile + ': ' + e);
1016
- }
1017
- return generateDiffReport(study, refStudy, { onlyNew: options.onlyNew });
1018
- }
1019
- }
1020
- else if (options.depReport) {
1021
- return generateDependenciesReport(study);
1022
- }
1023
- else if (options.perSpec) {
1024
- return generateReportPerSpec(study);
1025
- }
1026
- else {
1027
- return generateReportPerIssue(study);
1028
- }
1029
- return report;
1030
- }
1031
-
1032
-
1033
- /**************************************************
1034
- Export methods for use as module
1035
- **************************************************/
1036
- module.exports.generateReport = generateReport;
1037
-
1038
-
1039
- /**************************************************
1040
- Code run if the code is run as a stand-alone module
1041
- **************************************************/
1042
- if (require.main === module) {
1043
- const studyFile = process.argv[2];
1044
- const options = {
1045
- perSpec: !!process.argv[3] || (process.argv[3] === 'perspec'),
1046
- depReport: (process.argv[3] === 'dep'),
1047
- diffReport: (process.argv[3] === 'diff'),
1048
- refStudyFile: (process.argv[3] === 'diff') ? process.argv[4] : null,
1049
- onlyNew: (process.argv[5] === 'onlynew')
1050
- };
1051
-
1052
- generateReport(studyFile, options)
1053
- .then(report => console.log(report))
1054
- .catch(err => console.error(err.toString()));
1055
- }