runtime-type-inspector 1.0.0 → 2.0.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 (54) hide show
  1. package/.eslintrc.js +480 -0
  2. package/README.md +12 -4
  3. package/jsconfig.json +10 -0
  4. package/package.json +32 -4
  5. package/plugin-parcel/index.mjs +4 -0
  6. package/plugin-rollup/index.mjs +78 -0
  7. package/plugin-webpack/index.mjs +4 -0
  8. package/rollup.config.mjs +183 -0
  9. package/src-runtime/assertType.mjs +54 -0
  10. package/src-runtime/createDiv.mjs +69 -0
  11. package/src-runtime/index.mjs +21 -0
  12. package/src-runtime/makeJSDoc.mjs +40 -0
  13. package/src-runtime/registerClass.mjs +11 -0
  14. package/src-runtime/registerTypedef.mjs +12 -0
  15. package/src-runtime/typecheckDiv.mjs +3 -0
  16. package/src-runtime/typecheckEvery.mjs +21 -0
  17. package/src-runtime/typecheckOptions.mjs +60 -0
  18. package/src-runtime/typecheckTable.mjs +14 -0
  19. package/src-runtime/typecheckWarn.mjs +23 -0
  20. package/src-runtime/validateArray.mjs +26 -0
  21. package/src-runtime/validateDivision.mjs +31 -0
  22. package/src-runtime/validateMap.mjs +36 -0
  23. package/src-runtime/validateNumber.mjs +29 -0
  24. package/src-runtime/validateObject.mjs +43 -0
  25. package/src-runtime/validateRecord.mjs +31 -0
  26. package/src-runtime/validateSet.mjs +26 -0
  27. package/src-runtime/validateTuple.mjs +28 -0
  28. package/src-runtime/validateType.mjs +214 -0
  29. package/src-runtime/validateTypedef.mjs +18 -0
  30. package/src-runtime/validateUnion.mjs +19 -0
  31. package/src-runtime/youCanAddABreakpointHere.mjs +11 -0
  32. package/src-transpiler/Stringifier.mjs +1268 -0
  33. package/src-transpiler/StringifierWithTypeAssertions.mjs +478 -0
  34. package/src-transpiler/addTypeChecks.mjs +20 -0
  35. package/src-transpiler/ast2json.mjs +13 -0
  36. package/src-transpiler/ast2jsonForComparison.mjs +28 -0
  37. package/src-transpiler/code2ast2code.mjs +16 -0
  38. package/src-transpiler/expandType.mjs +187 -0
  39. package/src-transpiler/expandTypeDepFree.mjs +122 -0
  40. package/src-transpiler/extractNameAndOptionality.mjs +23 -0
  41. package/src-transpiler/index.mjs +13 -0
  42. package/src-transpiler/parseJSDoc.mjs +69 -0
  43. package/src-transpiler/parseJSDocSetter.mjs +16 -0
  44. package/src-transpiler/parseJSDocTypedef.mjs +69 -0
  45. package/src-transpiler/simplifyType.mjs +31 -0
  46. package/src-transpiler/stat.mjs +13 -0
  47. package/ui/babel-core.mjs +5 -0
  48. package/ui/babel-helper-plugin-utils.mjs +7 -0
  49. package/ui/babel-parser.mjs +8 -0
  50. package/ui/babel-plugin-syntax-typescript.mjs +58 -0
  51. package/ui/importmap.js +38 -0
  52. package/ui/index.html +30 -0
  53. package/ui/index.mjs +379 -0
  54. package/src/index.mjs +0 -1
@@ -0,0 +1,478 @@
1
+ import {expandTypeDepFree} from './expandTypeDepFree.mjs';
2
+ import {parseJSDoc } from './parseJSDoc.mjs';
3
+ import {parseJSDocSetter } from './parseJSDocSetter.mjs';
4
+ import {parseJSDocTypedef} from './parseJSDocTypedef.mjs';
5
+ import {statReset } from './stat.mjs';
6
+ import {Stringifier } from './Stringifier.mjs';
7
+ /** @typedef {import('@babel/types').Node} Node */
8
+ /** @typedef {import('./stat.mjs').Stat} Stat */
9
+ /**
10
+ * @typedef {object} Options
11
+ * @property {boolean} [forceCurly]
12
+ * @property {boolean} [validateDivision]
13
+ * @property {Function} [expandType]
14
+ * @property {string} [filename]
15
+ */
16
+ class StringifierWithTypeAssertions extends Stringifier {
17
+ /**
18
+ * @param {Options} [options]
19
+ */
20
+ constructor({
21
+ forceCurly = true,
22
+ validateDivision = true,
23
+ expandType = expandTypeDepFree,
24
+ filename
25
+ } = {}) {
26
+ super();
27
+ this.forceCurly = forceCurly;
28
+ this.validateDivision = validateDivision;
29
+ // @todo collect every type + manually validate as test set
30
+ // + implement expandType using Babel Flow type parser aswell
31
+ this.expandType = expandType;
32
+ this.filename = filename;
33
+ }
34
+ /** @type {Record<string, Stat>} */
35
+ stats = {
36
+ 'FunctionDeclaration' : {checked: 0, unchecked: 0},
37
+ 'FunctionExpression' : {checked: 0, unchecked: 0},
38
+ 'ArrowFunctionExpression': {checked: 0, unchecked: 0},
39
+ 'ClassMethod#constructor': {checked: 0, unchecked: 0},
40
+ 'ClassMethod#method' : {checked: 0, unchecked: 0},
41
+ 'ClassMethod#set' : {checked: 0, unchecked: 0},
42
+ 'ClassMethod#get' : {checked: 0, unchecked: 0},
43
+ };
44
+ /**
45
+ * We expand type-asserted ArrowFunctionExpressions in order to add type assertions.
46
+ * @override
47
+ * @param {import("@babel/types").ArrowFunctionExpression} node - The Babel AST node.
48
+ * @returns {string} Stringification of the node.
49
+ */
50
+ ArrowFunctionExpression(node) {
51
+ const {async, body, generator, params/*, extra*/} = node;
52
+ let out = '';
53
+ if (async) {
54
+ out += 'async ';
55
+ }
56
+ if (generator) {
57
+ out += ' * ';
58
+ }
59
+ out += this.FunctionDeclarationParams(params);
60
+ out += ' =>';
61
+ if (body.type === 'BlockStatement') {
62
+ out += this.toSource(body);
63
+ } else {
64
+ out += ' {\n';
65
+ out += this.generateTypeChecks(node);
66
+ out += this.spaces + 'return ' + this.toSource(body) + ';\n';
67
+ out += '}';
68
+ }
69
+ return out;
70
+ }
71
+ /**
72
+ * @override
73
+ * @param {import("@babel/types").ClassDeclaration} node - The Babel AST node.
74
+ * @returns {string} Stringification of the node.
75
+ */
76
+ ClassDeclaration(node) {
77
+ const {id} = node;
78
+ const id_ = this.toSource(id);
79
+ let out = super.ClassDeclaration(node);
80
+ out += `${this.spaces}registerClass(${id_});`;
81
+ return out;
82
+ }
83
+ /**
84
+ * @param {Node} node
85
+ * @param {string} type
86
+ * @returns {Node|undefined}
87
+ */
88
+ findParentOfType(node, type) {
89
+ const currentIndex = this.parents.findLastIndex(_ => _ == node);
90
+ return this.parents.findLast((_, i) => {
91
+ if (i > currentIndex) {
92
+ return false;
93
+ }
94
+ return _.type === type;
95
+ });
96
+ }
97
+ /**
98
+ * @param {Node} node - Start searching above this node.
99
+ * @param {string} type - Type we are searching for.
100
+ * @param {Function} predicate - Stop searching when this returns true.
101
+ * @returns {Node|undefined}
102
+ */
103
+ findParentOfTypeWithPredicate(node, type, predicate) {
104
+ const {parents} = this;
105
+ let i = parents.findLastIndex(_ => _ == node);
106
+ i--; // not interested in our start node
107
+ while (i >= 0) {
108
+ const parent = parents[i];
109
+ if (predicate(parent)) {
110
+ return;
111
+ }
112
+ if (parent.type === type) {
113
+ return parent;
114
+ }
115
+ i--;
116
+ }
117
+ }
118
+ /**
119
+ *
120
+ * @param {...any} args
121
+ */
122
+ warn(...args) {
123
+ if (this.filename) {
124
+ console.warn("[WARN]", this.filename);
125
+ }
126
+ console.warn(...args);
127
+ }
128
+ /**
129
+ * @param {Node} node - The Babel AST node.
130
+ * @returns {undefined | {}}
131
+ */
132
+ getJSDoc(node) {
133
+ if (node.type === 'BlockStatement') {
134
+ node = this.parent;
135
+ }
136
+ // Receive the leadingComments from the ExpressionStatement, not the FunctionExpression itself.
137
+ if (node.type === 'FunctionExpression') {
138
+ if (node.leadingComments) {
139
+ this.warn("Case of FunctionExpression containing its own leadingComments is not handled");
140
+ return;
141
+ }
142
+ node = this.findParentOfType(node, 'ExpressionStatement');
143
+ if (!node) {
144
+ return;
145
+ }
146
+ }
147
+ let {leadingComments} = node;
148
+ // Receive the leadingComments from ExportNamedDeclaration, if FunctionDeclaration has none
149
+ if (!leadingComments) {
150
+ if (node.type === 'FunctionDeclaration') {
151
+ const exportNamedDeclaration = this.findParentOfType(node, 'ExportNamedDeclaration');
152
+ leadingComments = exportNamedDeclaration?.leadingComments;
153
+ }
154
+ if (node.type === 'ArrowFunctionExpression') {
155
+ let tmp = this.findParentOfType(node, 'VariableDeclaration');
156
+ if (!tmp?.leadingComments) {
157
+ function isFunc(node) {
158
+ // console.log("isFunc", node.type);
159
+ return (
160
+ node.type === 'ArrowFunctionExpression' ||
161
+ node.type === 'FunctionDeclaration'
162
+ );
163
+ }
164
+ tmp = this.findParentOfTypeWithPredicate(node, 'ExportNamedDeclaration', isFunc);
165
+ }
166
+ leadingComments = tmp?.leadingComments;
167
+ }
168
+ }
169
+ if (leadingComments && leadingComments.length) {
170
+ const lastComment = leadingComments[leadingComments.length - 1];
171
+ if (lastComment.type == "CommentBlock") {
172
+ if (lastComment.value.includes('@event')) {
173
+ return;
174
+ }
175
+ if (node.type === 'ClassMethod' && node.kind === 'set') {
176
+ const paramName = this.getNameOfParam(node.params[0]);
177
+ if (node.params.length !== 1) {
178
+ this.warn("getJSDoc> setters require exactly one argument");
179
+ }
180
+ return {
181
+ [paramName]: parseJSDocSetter(lastComment.value, this.expandType)
182
+ }
183
+ }
184
+ return parseJSDoc(lastComment.value, this.expandType);
185
+ }
186
+ }
187
+ }
188
+ /**
189
+ * @param {Node} param - The Babel AST node.
190
+ * @returns {string}
191
+ */
192
+ getNameOfParam(param) {
193
+ if (param.type === 'Identifier') {
194
+ return param.name;
195
+ } else if (param.type === 'AssignmentPattern') {
196
+ if (param.left.type === 'Identifier') {
197
+ return param.left.name;
198
+ }
199
+ }
200
+ debugger;
201
+ this.warn("unable to extra name from param in specified way - may contain too much information");
202
+ return this.toSource(param);
203
+ }
204
+ statsReset() {
205
+ Object.values(this.stats).forEach(statReset);
206
+ }
207
+ statsPrint() {
208
+ console.table(this.stats);
209
+ }
210
+ /**
211
+ * @param {Node} node - The Babel AST node.
212
+ * @returns {Stat}
213
+ */
214
+ getStatsForNode(node) {
215
+ const {parentType, stats} = this;
216
+ if (parentType == 'ClassMethod') {
217
+ const kind = this.parent.kind;
218
+ return stats[`ClassMethod#${kind}`];
219
+ } else if (node.type === 'ArrowFunctionExpression') {
220
+ return stats.ArrowFunctionExpression;
221
+ }
222
+ const stat = stats[parentType];
223
+ if (!stat) {
224
+ this.warn("getStatsForNode> dummy, but unhandled... fix for node type", node);
225
+ return {checked: 0, unchecked: 0};
226
+ }
227
+ return stat;
228
+ }
229
+ /**
230
+ * @param {Node} node - The Babel AST node.
231
+ * @param {string} name
232
+ * @returns {boolean}
233
+ */
234
+ nodeHasParamName(node, name) {
235
+ if (node.type === 'BlockStatement') {
236
+ node = this.parent;
237
+ }
238
+ const {params} = node;
239
+ if (!params) {
240
+ this.warn("nodeHasParamName> Expected params for", {node, name});
241
+ return false;
242
+ }
243
+ return params.some(node => {
244
+ const {type} = node;
245
+ if (type === "AssignmentPattern") {
246
+ const {left} = node;
247
+ console.assert(
248
+ left.type === 'Identifier' ||
249
+ left.type === 'ObjectPattern' ||
250
+ left.type === 'ArrayPattern' ||
251
+ left.type === 'AssignmentPattern',
252
+ 'Expected Identifier or ObjectPattern'
253
+ );
254
+ return left.name === name;
255
+ } else if (type === 'Identifier') {
256
+ return node.name === name;
257
+ } else if (type === 'ArrayPattern' || type === 'ObjectPattern' || type === 'RestElement') {
258
+ return false;
259
+ } else {
260
+ const _ = new Stringifier();
261
+ const code = _.toSource(node);
262
+ console.log("Unknown type to test params for", type, code);
263
+ }
264
+ return false;
265
+ })
266
+ }
267
+ /**
268
+ * @override
269
+ * @param {Node} node - The Babel AST node.
270
+ * @returns {string}
271
+ */
272
+ generateTypeChecks(node) {
273
+ const {parentType} = this;
274
+ if (
275
+ node.type === 'BlockStatement' &&
276
+ parentType != 'FunctionDeclaration' &&
277
+ parentType != 'ClassMethod' &&
278
+ parentType != 'FunctionExpression'
279
+ ) {
280
+ return '';
281
+ }
282
+ const jsdoc = this.getJSDoc(node);
283
+ // return '// ' + JSON.stringify(jsdoc) + '\n';
284
+ const stat = this.getStatsForNode(node);
285
+ if (!jsdoc) {
286
+ stat.unchecked++;
287
+ return '';
288
+ }
289
+ stat.checked++;
290
+ let spaces = this.spaces;
291
+ if (node.type !== 'ArrowFunctionExpression') {
292
+ spaces += ' ';
293
+ }
294
+ let out = '';
295
+ //out += `${spaces}/*${spaces} node.type=${node.type}\n${spaces}
296
+ // ${JSON.stringify(jsdoc)}\n${parent}\n${spaces}*/\n`;
297
+ for (let name in jsdoc) {
298
+ const type = jsdoc[name];
299
+ const hasParam = this.nodeHasParamName(node, name);
300
+ if (!hasParam) {
301
+ let testNode = node;
302
+ if (node.type === 'BlockStatement') {
303
+ testNode = this.parent;
304
+ }
305
+ const paramIndex = Object.keys(jsdoc).findIndex(_ => _ === name);
306
+ const param = testNode.params[paramIndex];
307
+ if (param) {
308
+ const isObjectPattern = param.type === 'ObjectPattern';
309
+ const isArrayPattern = param.type === 'ArrayPattern';
310
+ const isSupportedPattern = isObjectPattern || isArrayPattern;
311
+ // There are four kinds of patterns:
312
+ // ObjectPattern:
313
+ // function test({x = 123}) {return x;} test({x: 456});
314
+ // ArrayPattern:
315
+ // function test([x = 123]) {return x;}; test([456]);
316
+ // AssignmentPattern made up of ObjectPattern:
317
+ // function test({x = 123} = {}) {return x;} test();
318
+ // AssignmentPattern made up of ArrayPattern:
319
+ // function test([x = 123] = []) {return x;} test();
320
+ if (isSupportedPattern) {
321
+ // The name doesn't matter any longer, because any pattern inherently
322
+ // drops the identifier from the AST. But we can access it
323
+ // via arguments[paramIndex] anyway.
324
+ name = `arguments[${paramIndex}]`;
325
+ } else if (param.type === 'AssignmentPattern') {
326
+ const loc = this.getName(node);
327
+ if (param.left.type === 'ArrayPattern' && type.type === 'array') {
328
+ // Add a type assertion for each element of the ArrayPattern
329
+ for (const element of param.left.elements) {
330
+ if (element.type !== 'Identifier') {
331
+ this.warn('Only Identifier case handled right now');
332
+ continue;
333
+ }
334
+ const t = JSON.stringify(type.elementType, null, 2).replaceAll('\n', '\n' + spaces);
335
+ out += `${spaces}if (!assertType(${element.name}, ${t}, '${loc}', '${name}')) {\n`;
336
+ out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
337
+ }
338
+ continue;
339
+ } else if (param.left.type === 'ObjectPattern' && type.type === 'object') {
340
+ // Add a type assertion for each property of the ObjectPattern
341
+ for (const property of param.left.properties) {
342
+ if (property.key.type !== 'Identifier') {
343
+ this.warn('ObjectPattern> Only Identifier case handled right now');
344
+ continue;
345
+ }
346
+ const keyName = property.key.name;
347
+ const subType = type.properties[keyName];
348
+ if (!subType) {
349
+ this.warn("missing subtype information in JSDoc");
350
+ continue;
351
+ }
352
+ const t = JSON.stringify(subType, null, 2).replaceAll('\n', '\n' + spaces);
353
+ out += `${spaces}if (!assertType(${keyName}, ${t}, '${loc}', '${name}')) {\n`;
354
+ out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
355
+ }
356
+ continue;
357
+ }
358
+ this.warn(`generateTypeChecks> ${loc}> todo implement`,
359
+ `AssignmentPattern for parameter ${name}`);
360
+ continue;
361
+ }
362
+ } else {
363
+ const loc = this.getName(node);
364
+ this.warn(`generateTypeChecks> ${loc}> Missing param: ${name}`);
365
+ continue;
366
+ }
367
+ }
368
+ let t = JSON.stringify(type, null, 2).replaceAll('\n', '\n' + spaces);
369
+ if (type === 'this') {
370
+ const classDecl = this.findParentOfType(node, 'ClassDeclaration');
371
+ if (!classDecl?.id) {
372
+ this.warn('generateTypeChecks> !classDecl?.id');
373
+ }
374
+ t = '"' + this.toSource(classDecl.id) + '"';
375
+ }
376
+ const loc = this.getName(node);
377
+ let prevCheck = '';
378
+ // JSDoc doesn't support multiple function signatures yet, but this is
379
+ // exactly what we would need to deal with ObjectPool'ing
380
+ if (
381
+ loc === 'ContactPoint#constructor' ||
382
+ loc === 'ContactResult#constructor' ||
383
+ loc === 'SingleContactResult#constructor'
384
+ ) {
385
+ prevCheck = 'arguments.length !== 0 && ';
386
+ }
387
+ out += `${spaces}if (${prevCheck}!assertType(${name}, ${t}, '${loc}', '${name}')) {\n`;
388
+ out += `${spaces} youCanAddABreakpointHere();\n${spaces}}\n`;
389
+ }
390
+ return out;
391
+ }
392
+ /**
393
+ * @param {Node} node - The Babel AST node.
394
+ * @returns {string} Stringification of the node.
395
+ */
396
+ getName(node) {
397
+ const toSource = this.toSource.bind(this);
398
+ if (node.type === 'BlockStatement') {
399
+ node = this.parent;
400
+ }
401
+ const {type, key, id} = node;
402
+ switch (type) {
403
+ case 'FunctionDeclaration':
404
+ return toSource(id);
405
+ case "ClassMethod":
406
+ const classDecl = this.parents.findLast(_ => _.type == 'ClassDeclaration');
407
+ let out = ''
408
+ if (classDecl) {
409
+ out += toSource(classDecl.id) + '#';
410
+ }
411
+ out += toSource(key);
412
+ return out;
413
+ case 'FunctionExpression':
414
+ if (id) {
415
+ return toSource(id);
416
+ }
417
+ const expressionStatement = this.findParentOfType(node, 'ExpressionStatement');
418
+ if (!expressionStatement) {
419
+ return "unnamed function expression";
420
+ }
421
+ return toSource(expressionStatement.expression.left);
422
+ case 'ArrowFunctionExpression':
423
+ const parent = this.findParentOfType(node, 'VariableDeclarator');
424
+ if (parent) {
425
+ return toSource(parent.id);
426
+ } else {
427
+ return "getName> missing parent for " + node.type;
428
+ }
429
+ default:
430
+ this.warn("getName> unhandled type", type, "for", node);
431
+ return '/*MISSING*/';
432
+ }
433
+ }
434
+ /**
435
+ * @override
436
+ * @param {import("@babel/types").BinaryExpression} node - The Babel AST node.
437
+ * @returns {string} Stringification of the node.
438
+ */
439
+ BinaryExpression(node) {
440
+ if (!this.validateDivision) {
441
+ return super.BinaryExpression(node);
442
+ }
443
+ const {left, operator, right} = node;
444
+ const left_ = this.toSource(left);
445
+ const right_ = this.toSource(right);
446
+ if (operator === '/') {
447
+ return `validateDivision(${left_}, ${right_})`;
448
+ }
449
+ return `${left_} ${operator} ${right_}`;
450
+ }
451
+ /** @type {Record<string, object>} */
452
+ typedefs = {};
453
+ /**
454
+ * @override
455
+ * @param {import("@babel/types").File} node - The Babel AST node.
456
+ * @returns {string} Stringification of the node.
457
+ */
458
+ File(node) {
459
+ const {errors, program, comments} = node;
460
+ if (comments) {
461
+ for (const comment of comments) {
462
+ const warn = this.warn.bind(this);
463
+ parseJSDocTypedef(this.typedefs, warn, comment, this.expandType);
464
+ }
465
+ }
466
+ //console.log("this.typedefs", this.typedefs);
467
+ let out = '';
468
+ for (const name in this.typedefs) {
469
+ const typedef = this.typedefs[name];
470
+ const json = JSON.stringify(typedef, null, 2);
471
+ out += `registerTypedef('${name}', ${json});\n`;
472
+ }
473
+ const code = this.toSource(program) + '\n';
474
+ out += code;
475
+ return out;
476
+ }
477
+ }
478
+ export {StringifierWithTypeAssertions};
@@ -0,0 +1,20 @@
1
+ import {parse } from '@babel/parser';
2
+ import {StringifierWithTypeAssertions} from './StringifierWithTypeAssertions.mjs';
3
+ /**
4
+ * Simple facade which does all the processing.
5
+ * @param {string} src - The input source with JSDoc comments.
6
+ * @param {import('./StringifierWithTypeAssertions.mjs').Options} [options]
7
+ * @returns {string} The output source with runtime type testing.
8
+ */
9
+ function addTypeChecks(src, options) {
10
+ try {
11
+ const stringifierWithTypeAssertions = new StringifierWithTypeAssertions(options);
12
+ const ast = parse(src, {sourceType: 'module'});
13
+ const out = stringifierWithTypeAssertions.toSource(ast);
14
+ return out;
15
+ } catch (e) {
16
+ console.error(e);
17
+ return `/*<addTypeChecks-error>*/${src}/*</addTypeChecks-error>*/`;
18
+ }
19
+ }
20
+ export {addTypeChecks};
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @param {object} ast - The Babel AST.
3
+ * @returns {string} String representation in JSON format for debugging/inspecting the AST.
4
+ */
5
+ function ast2json(ast) {
6
+ return JSON.stringify(ast, function(name, val) {
7
+ if (name == "loc" || name == "start" || name == "end") {
8
+ return undefined; // remove
9
+ }
10
+ return val; // keep
11
+ }, 2);
12
+ }
13
+ export {ast2json};
@@ -0,0 +1,28 @@
1
+ const drop = [
2
+ 'loc',
3
+ 'start',
4
+ 'end',
5
+ 'leadingComments',
6
+ 'trailingComments',
7
+ 'innerComments',
8
+ 'innerComments',
9
+ 'comments',
10
+ ];
11
+ /**
12
+ * @example
13
+ * setRight(ast2jsonForComparison(parseSync("/** *"))); // Close comment with / after last *
14
+ * @param {object} ast - The Babel AST.
15
+ * @returns {string} String representation in JSON format for debugging/inspecting the AST.
16
+ */
17
+ function ast2jsonForComparison(ast) {
18
+ return JSON.stringify(ast, function(name, val) {
19
+ if (name === 'trailingComma' || name === 'parenStart') {
20
+ return 'offset removed for better comparison';
21
+ }
22
+ if (drop.includes(name)) {
23
+ return undefined; // remove
24
+ }
25
+ return val; // keep
26
+ }, 2);
27
+ }
28
+ export {ast2jsonForComparison};
@@ -0,0 +1,16 @@
1
+ import {parse } from "@babel/parser";
2
+ import {Stringifier} from "./Stringifier.mjs";
3
+ /**
4
+ * A roundtrip between code -> AST -> code to validate Stringifier.
5
+ * @param {string} code - The code.
6
+ */
7
+ function code2ast2code(code) {
8
+ const stringifier = new Stringifier();
9
+ const ast = parse(code, {sourceType: 'module'});
10
+ if (!ast) {
11
+ return;
12
+ }
13
+ const out = stringifier.toSource(ast);
14
+ return out;
15
+ }
16
+ export {code2ast2code};