-
-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathvalidate.js
More file actions
1219 lines (1128 loc) · 40.2 KB
/
validate.js
File metadata and controls
1219 lines (1128 loc) · 40.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* Script to validate the syntax of the YAML files against the TDoc spec
* https://wiki.appcelerator.org/display/guides2/TDoc+Specification
*
* Execute `node validate.js --help` for usage
*
* Dependencies: colors ~0.6.2 and node-appc ~0.2.14
*/
'use strict';
const fs = require('fs');
const nodeappc = require('node-appc');
const colors = require('colors'); // eslint-disable-line no-unused-vars
const common = require('./lib/common.js');
let doc = {};
let standaloneFlag = false;
// List of "whitelisted" types provided via cli flag
// if we are unable to find these types we do not error
// This gives more control versus the standalone Flag which just ignores any type errors
const whitelistedTypes = [];
// Constants that are valid, but are windows specific, so would fail validation
const whitelistedConstants = [
'Titanium.UI.Windows.ListViewScrollPosition.*'
];
const AVAILABILITY = [ 'always', 'creation', 'not-creation' ];
const PERMISSIONS = [ 'read-only', 'write-only', 'read-write' ];
const Examples = [ {
required: {
title: 'String',
example: 'Markdown'
}
} ];
const Deprecated = {
required: {
since: 'Since'
},
optional: {
removed: 'Removed',
notes: 'String'
}
};
// TODO: Replace validateReturns with this once we fix docs to not have array of returns (when they should instead have an array of type under returns)
// const Returns = {
// required: {
// type: 'DataType' // FIXME: also needs to handle void
// },
// optional: {
// summary: 'String',
// constants: 'Constants'
// }
// };
const validSyntax = {
required: {
name: 'ASCIIString',
summary: 'String',
},
optional: {
description: 'Markdown',
createable: 'Boolean',
platforms: 'Platforms',
'exclude-platforms': 'Platforms',
excludes: {
optional: {
events: 'Array<events.name>',
methods: 'Array<methods.name>',
properties: 'Array<properties.name>'
}
},
examples: Examples,
osver: 'OSVersions',
extends: 'Class',
deprecated: Deprecated,
since: 'Since',
events: [ {
required: {
name: 'LowercaseASCIIString',
summary: 'String',
},
optional: {
description: 'String',
platforms: 'Platforms',
since: 'Since',
deprecated: Deprecated,
osver: 'OSVersions',
properties: [ {
required: {
name: 'ASCIIString',
summary: 'String',
type: 'DataType',
},
optional: {
optional: 'Boolean',
platforms: 'Platforms',
deprecated: Deprecated,
since: 'Since',
'exclude-platforms': 'Platforms',
constants: 'Constants'
}
} ],
'exclude-platforms': 'Platforms',
notes: 'Invalid'
}
} ],
methods: [ {
required: {
name: 'ASCIIString',
summary: 'String'
},
optional: {
description: 'String',
returns: 'Returns',
platforms: 'Platforms',
since: 'Since',
deprecated: Deprecated,
examples: Examples,
osver: 'OSVersions',
parameters: [ {
required: {
name: 'ASCIIString',
summary: 'String',
type: 'DataType'
},
optional: {
optional: 'Boolean',
default: 'Default',
repeatable: 'Boolean',
constants: 'Constants',
notes: 'Invalid'
}
} ],
'exclude-platforms': 'Platforms',
notes: 'Invalid'
}
} ],
properties: [ {
required: {
name: 'ASCIIString',
summary: 'String',
type: 'DataType'
},
optional: {
description: 'String',
platforms: 'Platforms',
since: 'Since',
deprecated: Deprecated,
osver: 'OSVersions',
examples: Examples,
permission: 'Permission',
availability: 'Availability',
accessors: 'Boolean',
optional: 'Boolean',
value: 'Primitive',
default: 'Default',
'exclude-platforms': 'Platforms',
constants: 'Constants',
notes: 'Invalid'
}
} ]
}
};
// We define issues in the apidocs as Problem instances with severities
// this way we can warn about issues but not have them fail validation
const ERROR = 1;
const WARNING = 2;
const INFO = 3;
class Problem {
/**
* @param {string} message the message giving details of the problem
* @param {1|2|3} [severity=1] severity level of this problem
*/
constructor(message, severity = ERROR) {
this.message = message;
this.severity = severity;
}
isWarning() {
return this.severity === WARNING;
}
isError() {
return this.severity === ERROR;
}
isInfo() {
return this.severity === INFO;
}
toString() {
return this.message;
}
}
/**
* Validate if an API exists in a class and its ancestors
* We use this to validate "excludes" references refer to methods/properties/events that actually exist on parents
* @param {string[]} names Array of API names to verify
* @param {'events'|'methods'|'properties'} type API type
* @param {string} className Name of class to check
* @returns {null|Problem} possible Problem
*/
function validateAPINames(names, type, className) {
const apis = doc[className][type]; // grab the type's events/properties/methods
// This modifies the 'names' arrays as we go, removing entries where we've found a match
if (apis) {
apis.forEach(function (api) {
const index = names.indexOf(api.name);
if (~index) {
names.splice(index);
}
});
}
if (type === 'methods' && 'properties' in doc[className]) {
// Evaluate setters and getters
// if we're looking for getProp/setProp and the type has a declared property for the same name, match it up
doc[className].properties.forEach(function (property) {
const Prop = property.name.charAt(0).toUpperCase() + property.name.slice(1);
const setterIndex = names.indexOf(`set${Prop}`);
if (~setterIndex) {
names.splice(setterIndex);
}
const getterIndex = names.indexOf(`get${Prop}`);
if (~getterIndex) {
names.splice(getterIndex);
}
});
}
if ('extends' in doc[className]) {
// Evaluate parent class
const parent = doc[className]['extends'];
if (parent in doc) {
// the parent type exists, so recurse with remaining api names against the parent
return validateAPINames(names, type, parent);
}
// This is a whitelisted type, so ignore it
if (whitelistedTypes.includes(parent)) {
return;
}
if (standaloneFlag) {
console.warn('WARNING! Cannot validate parent class: %s'.yellow, parent);
return;
}
// TODO: Also make note of remaining apis we haven't matched?
return new Problem(`Invalid parent class: ${parent}`);
}
// We still have unmatched (not found) apis
if (names.length > 0) {
return new Problem(`Could not find the following ${type}: ${names}`);
}
// All good
return null;
}
/**
* Validate boolean type
* @param {*} bool possible boolean value
* @return {null|Problem} possible Problem if not a boolean
*/
function validateBoolean(bool) {
if (typeof bool !== 'boolean') {
return new Problem(`Not a boolean value: ${bool}`);
}
return null;
}
/**
* Validate class is in docs
* @param {string} className class name
* @returns {null|Problem} possible Problem if not found in docs
*/
function validateClass(className) {
if (!(className in doc)) {
// is it a builtin?
if (common.DATA_TYPES.includes(className)) {
return null;
}
if (standaloneFlag) {
return new Problem(`Cannot validate class: ${className} (standalone flag is set)`, WARNING);
}
if (whitelistedTypes.includes(className)) {
return null;
}
return new Problem(`Not a valid or known class/type: ${className}`);
}
return null;
}
/**
* Validate constant is in docs
* @param {string|string[]} constants arry or string of constant names
* @returns {Problem[]} array of Problems if any given constants weren't found in the docs
*/
function validateConstants(constants) {
const errors = [];
// "coerce" to Array
constants = Array.isArray(constants) ? constants : [ constants ];
// validate each one
constants.forEach(c => {
const possibleProblem = validateConstant(c);
if (possibleProblem) {
errors.push(possibleProblem);
}
});
return errors;
}
/**
* @param {string} constant name of the referenced constant
* @returns {null|Problem} possible Problem
*/
function validateConstant(constant) {
// skip windows constants that are OK, but would be marked invalid
if (whitelistedConstants.includes(constant)) {
return null;
}
// is it hanging on a real type (that has properties, so therefore would have constants)
const typeName = constant.substring(0, constant.lastIndexOf('.'));
if (!(typeName in doc) || !('properties' in doc[typeName]) || doc[typeName] === null) {
return new Problem(`Invalid constant: ${constant}, type ${typeName} does not exist`);
}
const propertyNames = doc[typeName].properties.map(p => p.name);
// Grab the last segment of the namespace (the "base name")
const propertyBaseName = constant.split('.').pop();
// check for wildcard references!
if (propertyBaseName.charAt(propertyBaseName.length - 1) === '*') {
// TODO: Report a problem for wildcards? Maybe warning for now?
const wildcardPrefix = propertyBaseName.substring(0, propertyBaseName.length - 1);
// check that we have at least one property matching this prefix?
for (let i = 0; i < propertyNames.length; i++) {
if (propertyNames[i].indexOf(wildcardPrefix) === 0) {
// found it!
return null;
}
}
// didn't find it!
return new Problem(`Invalid constant: ${constant}`);
}
// Can we find this constant listed as a property on the referenced type/class?
if (!propertyNames.includes(propertyBaseName)) {
return new Problem(`Invalid constant: ${constant}`);
}
return null;
}
/**
* Parses the generic portion of a complex type string (the stuff inside the brackets)
* so for "Map<Number, String>", this would get "<Number, String>". Has to handle recursive complex types proeprly.
* @param {string} rawSubTypeString i.e. "Object", "void", "Map<Number, String>, Set<String>"
* @returns {string[]}
*/
function parseSubTypes(rawSubTypeString) {
const regex = /([^,<\s]+(<.+?>)?)/gm;
const types = [];
let m;
while ((m = regex.exec(rawSubTypeString)) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (m.index === regex.lastIndex) {
regex.lastIndex++;
}
types.push(m[0]);
}
return types;
}
/**
* Validate type
* @param {string|string[]} type array of strings, or single string with a type name
* @param {string|null} fullTypeContext full context of the type (for recursion), i.e. 'Callback<Object>' or 'Array<Object>'
* @returns {Problem[]} problems (may be empty)
*/
// FIXME: Some types may only be valid in some scenarios, i.e. 'void' for return type/callback arg (which is really 'undefined')
function validateDataType(type, fullTypeContext) {
if (Array.isArray(type)) {
const errors = [];
type.forEach(elem => {
errors.push(...validateDataType(elem));
});
return errors;
}
// Check for compound types: Array<>, Callback<>, Function<>, Dictionary<>, Set<>, Promise<>, Map<>
const lessThanIndex = type.indexOf('<');
const greaterThanIndex = type.lastIndexOf('>');
if (lessThanIndex !== -1 && greaterThanIndex !== -1) {
// Compound data type
const baseType = type.slice(0, lessThanIndex);
if (!common.COMPLEX_TYPES.has(baseType)) {
return [ new Problem(`${baseType} is not a valid complex type, must be one of ${Array.from(common.COMPLEX_TYPES.keys())}: ${fullTypeContext || type}`) ];
}
const subTypes = parseSubTypes(type.slice(lessThanIndex + 1, greaterThanIndex));
const argCount = common.COMPLEX_TYPES.get(baseType);
// Enforce complex types have correct number of generics specified
if (argCount !== 0 && subTypes.length !== argCount) {
return [ new Problem(`${type} must have ${argCount} generic type(s) specified, but had ${subTypes.length}: ${fullTypeContext || type}`) ];
}
// Special case for Callback<void> or Function<void>
if (argCount === 0 && subTypes.length === 1 && subTypes[0] === 'void') {
return [];
}
// check the generic types
const errors = [];
subTypes.forEach(sub => {
errors.push(...validateDataType(sub.trim(), type));
});
return errors;
}
// not written as a compound type...
const errors = [];
// Is this a built in Javascript type?
if (common.DATA_TYPES.includes(type)) {
// Should it have been written as a complex type?
if (common.COMPLEX_TYPES.has(type)) {
const argCount = common.COMPLEX_TYPES.get(type); // may be 0 if Function/Callback
// Enforce as ERROR if Promise/Set/Map doesn't have exact generic type count
const severity = [ 'Map', 'Set', 'Promise' ].includes(type) ? ERROR : WARNING;
errors.push(new Problem(`${type} ${severity === ERROR ? 'must' : 'should'} have ${argCount || 'any number of'} generic type(s) specified, but had 0: ${fullTypeContext || type}`, severity));
} else if (type === 'Object') {
// Warn about generic Object types (Dictionary is handled above as a complex type)
// TODO: How can we mark/skip the valid cases here? Some APIs really do need to say "Object" as the arg/return value
errors.push(new Problem(`Please define a new type rather than using the generic Object type reference: ${fullTypeContext || type}`, WARNING));
}
return errors;
}
// Is this a type in our APIDocs, or on our whitelist? (or are we on standalone mode?)
const possibleProblem = validateClass(type);
if (possibleProblem) {
errors.push(possibleProblem);
}
// class/type is fine or whitelisted
return errors;
}
/**
* Validate default value
* @param {*} val possible primitive or object
* @returns {null|Problem} possible Problem if not a primitive or object
*/
function validateDefault(val) {
if (validatePrimitive(val) && (typeof val !== 'object')) {
return new Problem(`Not a valid data type or string: ${val}`);
}
return null;
}
/**
* Validate platform listing
* @param {*} platforms possible number
* @returns {null|Problem} possible Problem if not valid
*/
function validatePlatforms(platforms) {
// FIXME: If something explicitly lists a platform excluded at some higher level, we should error/warn!
if (!Array.isArray(platforms)) {
return new Problem('must be an array of valid platform names');
}
if (platforms.length === 0) {
return new Problem('array must not be empty. Remove to fall back to "default" platforms based on "since" value; or remove doc entry if this applies to no platforms.');
}
// Filter the platforms against common.VALID_PLATFORMS - any remaining are invalid
const remaining = platforms.filter(p => !common.VALID_PLATFORMS.includes(p));
if (remaining && remaining.length !== 0) {
return new Problem(`Invalid platform name(s): ${remaining}`);
}
return null;
}
/**
* Validate availability
* @param {*} availability possible availability
* @returns {null|Problem} possible Problem if not valid
*/
function validateAvailability(availability) {
return validateOneOf(AVAILABILITY, availability);
}
function validateOneOf(possibilities, value) {
if (!possibilities.includes(value)) {
return new Problem(`must be one of: ${possibilities}. was: ${value}`);
}
return null;
}
/**
* Validate permission value
* @param {string} permission possible permission
* @param {string} propertyName name of the property whose permissions is being set
* @returns {null|Problem} possible Problem if not valid
*/
function validatePermission(permission, propertyName) {
const possibleProblem = validateOneOf(PERMISSIONS, permission);
if (possibleProblem) {
return possibleProblem;
}
// It's one of our enumerated values, but if it looks like a constant, should probably be 'read-only'
// consider UPPER(_MORE)* style constants (i.e. can't start/end with underscores)
if (/^[A-Z]+(_[A-Z]+)*$/.test(propertyName) && permission !== 'read-only') {
return new Problem(`property name is all caps so permissions should likely be read-only (was ${permission})`, WARNING);
}
return null;
}
/**
* Validate number
* @param {*} number possible number
* @returns {null|Problem} possible Problem if not a number
*/
function validateNumber(number) {
if (typeof number !== 'number') {
return new Problem(`Not a number value: ${number}`);
}
return null;
}
/**
* Validate OS version
* @param {object} oses map of os names to versions
* @returns {Problem[]} possible problems (may be empty)
*/
function validateOSVersions(oses) {
const problems = [];
for (const key in oses) {
if (~common.VALID_OSES.indexOf(key)) {
for (const x in oses[key]) {
switch (x) {
case 'max':
case 'min':
const possibleVersionProblem = validateVersion(oses[key][x]);
if (possibleVersionProblem) {
problems.push(possibleVersionProblem);
}
break;
case 'versions':
// eslint-disable-next-line no-loop-func
oses[key][x].forEach(elem => {
const possibleVersionProblem = validateVersion(elem);
if (possibleVersionProblem) {
problems.push(possibleVersionProblem);
}
});
break;
default:
problems.push(new Problem(`Unknown key: ${x}`));
}
}
} else {
problems.push(new Problem(`Invalid OS: ${key}; valid OSes are: ${common.VALID_OSES}`));
}
}
return problems;
}
/**
* Validate primitive
* @param {object|number|boolean|string} x possible primitive value
* @return {null|Problem} possible Problem if not a primitive
*/
function validatePrimitive(x) {
if (validateBoolean(x) && validateNumber(x) && validateString(x)) {
return new Problem(`Not a primitive value (Boolean, Number, String): ${x}`);
}
return null;
}
/**
* Validate return value
* @param {object|object[]} ret An array of objects, or object
* @param {object} [ret.type] return type
* @param {object} [ret.summary] summary of value
* @param {object} [ret.constants] possible constant values
* @returns {Problem[]} problems (may be empty)
*/
function validateReturns(ret) {
const errors = [];
if (Array.isArray(ret)) {
errors.push(new Problem('Replace array of returns with single returns value with type having array of type names', WARNING));
ret.forEach(elem => errors.push(...validateReturns(elem)));
} else {
let sawType = false;
for (const key in ret) {
switch (key) {
case 'type':
if (ret[key] !== 'void') {
errors.push(...validateDataType(ret['type']));
}
sawType = true;
break;
case 'summary':
const possibleProblem = validateString(ret['summary']);
if (possibleProblem) {
errors.push(possibleProblem);
}
break;
case 'constants':
errors.push(...validateConstants(ret['constants']));
break;
default:
errors.push(new Problem(`Invalid key: ${key}`));
}
}
if (!sawType) {
errors.push(new Problem('Missing "type" for returns'));
}
}
return errors;
}
/**
* Validate since version
* @param {object|string} version object holding platform/os to version string; or a normal version string
* @param {Set<string>} platformsInContext the set of platforms this api member is listed as available on
* @returns {Problem[]} array of Problems (may be empty)
*/
function validateSince(version, platformsInContext) {
const errors = [];
// since values may be listed per-platform
if (typeof version === 'object') {
for (const platform in version) {
// Validate the platform specified is valid in this context (i.e. this api member is actually available on this platform!)
if (!platformsInContext.has(platform)) {
errors.push(new Problem(`Platform specified in 'since' ('${platform}') isn't one of the platforms this API is marked as available upon: ${Array.from(platformsInContext)}`));
}
// TODO: Check if the since value is extraneous (duplicates the inherited value)
// This can be true if they explitly state the same value *or* if the implict "default" value for a platform would have been used
// i.e. macos default is 9.2.0, so even if the property/method states a blanket "3.3.0", 9.2.0 would be used for macos
// so we do not need to break it out by platform!
// (Or do we want to encourage explicit values if they align with default?)
if (platform in common.DEFAULT_VERSIONS) {
try {
// is it reporting a version before our initial version of the platform?
if (nodeappc.version.lt(version[platform], common.DEFAULT_VERSIONS[platform])) {
errors.push(new Problem(`Minimum version for ${platform} is ${common.DEFAULT_VERSIONS[platform]}`));
}
} catch (e) {
// it reported an invalid version (unparseable as a version)
errors.push(new Problem(`Invalid version string: ${version[platform]}`));
}
} else {
// platform doesn't exist(!) - maybe a typo?
errors.push(new Problem(`Invalid platform: ${platform}`));
}
}
return errors;
}
const possibleProblem = validateVersion(version);
if (possibleProblem) {
errors.push(possibleProblem);
}
return errors;
}
/**
* Validate deprecated.removed version
* @param {string} version raw version number string
* @param {Set<string>} platformsInContext the set of platforms this api member is listed as available on
* @returns {Problem[]} array of Problems (may be empty)
*/
function validateRemoved(version, platformsInContext) {
const errors = [];
const possibleProblem = validateVersion(version);
// If we can't parse it as a version, we can't compare, so just return the initial error
if (possibleProblem) {
errors.push(possibleProblem);
return errors;
}
// FIXME: if the platform isn't listed explicitly at this level, we may just ignore it
// How can we get access to the list of explicit platforms?! It'd be up the hierarchy from current object
platformsInContext.forEach(p => {
const earliestVersion = common.DEFAULT_VERSIONS[p];
if (nodeappc.version.lt(version, earliestVersion)) {
errors.push(new Problem(`API was removed in version: ${version}, but lists a platform introduced in ${earliestVersion}: ${p}`));
}
});
return errors;
}
/**
* Validates string type
* @param {*} str possible string value
* @returns {null|Problem} possible Problem if value isn't a string
*/
function validateString(str) {
if (typeof str !== 'string') {
return new Problem(`Not a string value: ${str}`);
}
return null;
}
/**
* Validates string type and only ASCII characetrs (typically used to make method/property/event/parameter names ASCII only)
* @param {*} str possible string value
* @returns {null|Problem} possible Problem if value isn't a string or contains non-ASCII characters
*/
function validateASCIIString(str) {
const problem = validateString(str);
if (problem) {
return problem;
}
if (!/^[\x00-\x7F]*$/.test(str)) { // eslint-disable-line no-control-regex
return new Problem('String contains non-ASCII characters.');
}
return null;
}
/**
* Validate string
* @param {*} str possible string value
* @returns {null|Problem} possible Problem if value isn't a string or contains non-ASCII characters
*/
function validateLowercaseString(str) {
const problem = validateASCIIString(str);
if (problem) {
return problem;
}
if (str.toLowerCase() !== str) {
return new Problem('Name should be all lowercase.', WARNING);
}
return null;
}
/**
* Validatea markdown content (string type, can be converted from markdwn to html without erroring)
* @param {string} str possible markdown string
* @returns {null|Problem} possible Problem if not valid markdown (or string!)
*/
function validateMarkdown(str) {
const stringProblem = validateString(str);
if (stringProblem) {
return stringProblem;
}
try {
common.markdownToHTML(str);
} catch (e) {
return new Problem(`Error parsing markdown block "${str}": ${e}`);
}
return null;
}
/**
* Validate version
* @param {string} version possible version string
* @return {null|Problem} possible Problem if not a value version string
*/
function validateVersion(version) {
try {
nodeappc.version.lt('0.0.1', version);
} catch (e) {
return new Problem(`Invalid version: ${version}`);
}
return null;
}
/**
* adds the contents of the second map to the first (merging values with shared keys)
* @param {Map<String, Problem[]>} map1 map getting modified by having another map merged into it
* @param {Map<String, Problem[]>} map2 map to combine with the first
* @returns {Map<String, Problem[]>}
*/
function mergeMaps(map1, map2) {
if (map2 === null || map2 === undefined || map2.size < 1) {
return map1;
}
map2.forEach((val, key) => {
if (map1.has(key)) {
// need to merge the results
const combined = map1.get(key).concat(val);
map1.set(key, combined);
} else {
map1.set(key, val);
}
});
return map1;
}
/**
* @param {string|null} baseNamespace may be null, may be a dotted namespace
* @param {string} keyName base name of the key we're working on
* @returns {string}
*/
function generateFullPath(baseNamespace, keyName) {
if (baseNamespace) {
return `${baseNamespace}.${keyName}`;
}
return keyName;
}
/**
* Validates an object against a syntax dictionary
* @param {Object} obj Object (from the parsed apidocs) to validate
* @param {Object} syntax Dictionary defining the syntax
* @param {string} type type name? the current key from apidocs?
* @param {string} currentKey current key
* @param {string} className Name of class being validated
* @param {string} fullKeyPath full namespace of the key
* @param {Set<string>} platformsInContext set of platforms this API is declared to be filtered to
* @returns {Map<String, Problem[]>} mapping from keys to array of problems found for that key
*/
function validateObjectAgainstSyntax(obj, syntax, type, currentKey, className, fullKeyPath, platformsInContext) {
// If syntax is a dictionary, validate object against syntax dictionary
let result = new Map();
// Narrow the set of platforms
// TODO: allow method parameters to narrow too?
if (type === 'properties' || type === 'methods' || type === 'events') {
// If the set is the exact same **and** they specified a platforms explicitly, warn that it was unnecessary?
const before = platformsInContext.size;
platformsInContext = filterPlatforms(obj, platformsInContext);
const after = platformsInContext.size;
if (before === after && obj.platforms) {
result.set(generateFullPath(fullKeyPath, 'platforms'), [ new Problem(`Unnecessary platforms listing which is the same as the inherited set: ${obj.platforms}`, WARNING) ]);
}
}
// Ensure required keys exist and then validate them
const requiredKeys = syntax.required;
for (const requiredKey in requiredKeys) {
const fullRequiredKeyPath = generateFullPath(fullKeyPath, requiredKey);
if (requiredKey in obj) {
result = mergeMaps(result, validateKey(obj[requiredKey], requiredKeys[requiredKey], requiredKey, className, fullRequiredKeyPath, platformsInContext));
} else {
// We're missing a required field. Check the parent to see if it's filled in there.
// Only do this check when we're overriding an event, property or method, not top-level fields like 'summary'
const parentClassName = doc[className]['extends'];
let parent = doc[parentClassName];
let parentValue = null;
if (type && parent) {
const array = parent[type];
if (array) {
// find matching name in array
for (let i = 0; i < array.length; i++) {
if (array[i] && array[i].name === currentKey) { // eslint-disable-line max-depth
parent = array[i];
break;
}
}
if (parent) {
parentValue = parent[requiredKey];
}
}
}
if (!parentValue) {
result.set(fullRequiredKeyPath, [ new Problem(`Required property "${requiredKey}" not found`) ]);
}
}
}
// Validate optional keys if they're on the object
const optionalKeys = syntax.optional;
for (const optionalKey in optionalKeys) {
if (optionalKey in obj) {
result = mergeMaps(result, validateKey(obj[optionalKey], optionalKeys[optionalKey], optionalKey, className, generateFullPath(fullKeyPath, optionalKey), platformsInContext));
}
}
// Find keys on obj that aren't required or optional!
for (const possiblyInvalidKey in obj) {
// If doesn't start with underscores, and isn't required or optional...
const isRequired = requiredKeys ? (possiblyInvalidKey in requiredKeys) : false;
const isOptional = optionalKeys ? (possiblyInvalidKey in optionalKeys) : false;
if (possiblyInvalidKey.indexOf('__') !== 0 && !isRequired && !isOptional) {
// We found some entry in our docs that isn't required or optional in our syntax definition
// so it's probably extraneous (or a typo)
result.set(generateFullPath(fullKeyPath, possiblyInvalidKey), [ new Problem(`Invalid key(s) in ${className}: ${possiblyInvalidKey}`) ]);
}
}
return result;
}
/**
* @param {string} key map key
* @param {null|Problem} possibleProblem possible problem
* @returns {Map<string, Problem[]>}
*/
function possibleProblemAsMap(key, possibleProblem) {
const map = new Map();
if (possibleProblem) {
map.set(key, [ possibleProblem ]);
}
return map;
}
/**
* @param {string} key map key
* @param {Problem[]} possibleProblems possible problems
* @returns {Map<string, Problem[]>}
*/
function possibleProblemArrayAsMap(key, possibleProblems) {
const map = new Map();
if (possibleProblems && possibleProblems.length > 0) {
map.set(key, possibleProblems);
}
return map;
}
/**
* @param {string} fullKeyPath something like 'properties[propName].permission' is expected
* @returns {string} returns 'propName' in the example
*/
function getPropertyName(fullKeyPath) {
const lastOpenBracketIndex = fullKeyPath.lastIndexOf('[');
const lastCloseBracketIndex = fullKeyPath.lastIndexOf(']');
return fullKeyPath.slice(lastOpenBracketIndex + 1, lastCloseBracketIndex);
}
/**
* Validates an object against a syntax dictionary
* @param {Object} obj Object to validate
* @param {Object} syntax Dictionary defining the syntax
* @param {String} currentKey Current key being validated
* @param {String} className Name of class being validated
* @param {string} fullKeyPath full namespace of the current key
* @param {Set<string>} platformsInContext set of platforms this API member is said to be filtered to
* @returns {Map<String, Problem[]>} keys are the key paths, values are an array of Problems found for that specific apidoc tree member
*/
function validateKey(obj, syntax, currentKey, className, fullKeyPath, platformsInContext) {
// if a value is an Array in the syntax definition, it basically means that the given element can have 0+ instances of the wrapped object/syntax
if (Array.isArray(syntax)) {
if (syntax.length !== 1) {
// if the array has more than one entry, the syntax definition is busted
return possibleProblemAsMap(fullKeyPath, new Problem(`Syntax tree definition has more than one Array element at ${syntax}. Please fix it.`));
}
// Should only contain one entry, an object holding the definition of elements allowed in the array for this key
const firstSyntaxElement = syntax[0];
if (typeof firstSyntaxElement !== 'object') {
return possibleProblemAsMap(fullKeyPath, new Problem(`Syntax tree definition has a non-Object Array element at ${syntax}. Please fix it.`));
}
// Ok so this element defines how the entries for the key's array should look (i.e. how a given single property, method, event or example are defined)
// obj here should be an array!
if (!Array.isArray(obj)) {
return possibleProblemAsMap(fullKeyPath, new Problem(`We expect an Array of values for ${currentKey}, but received non-Array: ${obj}`));
}
// Validate each object against the syntax
let problemMap = new Map();
obj.forEach((elem, index) => {
const name = elem.name || '__noname';
const nameOrIndex = elem.name || index;
problemMap = mergeMaps(problemMap, validateObjectAgainstSyntax(elem, firstSyntaxElement, currentKey, name, className, `${fullKeyPath}[${nameOrIndex}]`, platformsInContext));
});
return problemMap;
}
// we're matching a given parsed apidoc tree item/node against the defined syntax tree/object (defined at the top of this file)
if (typeof syntax === 'object') {
return validateObjectAgainstSyntax(obj, syntax, null, currentKey, className, fullKeyPath, platformsInContext);
}
// We have a specific syntax element to validate against
switch (syntax) {
case 'Boolean':
return possibleProblemAsMap(fullKeyPath, validateBoolean(obj));
case 'Class':
return possibleProblemAsMap(fullKeyPath, validateClass(obj));
case 'Constants':
return possibleProblemArrayAsMap(fullKeyPath, validateConstants(obj));
case 'DataType':
return possibleProblemArrayAsMap(fullKeyPath, validateDataType(obj));
case 'Default':
return possibleProblemAsMap(fullKeyPath, validateDefault(obj));
case 'Number':
return possibleProblemAsMap(fullKeyPath, validateNumber(obj));
case 'OSVersions':
return possibleProblemArrayAsMap(fullKeyPath, validateOSVersions(obj));
case 'Removed':
return possibleProblemArrayAsMap(fullKeyPath, validateRemoved(obj, platformsInContext));
case 'Primitive':
return possibleProblemAsMap(fullKeyPath, validatePrimitive(obj));
case 'Returns':
return possibleProblemArrayAsMap(fullKeyPath, validateReturns(obj));