hermes-parser 0.7.0 → 0.8.0

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.
@@ -21,6 +21,7 @@ import type {HermesNode} from './HermesAST';
21
21
  import type {ParserOptions} from './ParserOptions';
22
22
 
23
23
  import HermesASTAdapter from './HermesASTAdapter';
24
+ import {getModuleDocblock} from './getModuleDocblock';
24
25
 
25
26
  declare var BigInt: ?(value: $FlowFixMe) => mixed;
26
27
 
@@ -45,6 +46,9 @@ export default class HermesToESTreeAdapter extends HermesASTAdapter {
45
46
  };
46
47
 
47
48
  node.range = [loc.rangeStart, loc.rangeEnd];
49
+
50
+ delete node.start;
51
+ delete node.end;
48
52
  }
49
53
 
50
54
  mapNode(node: HermesNode): HermesNode {
@@ -81,11 +85,22 @@ export default class HermesToESTreeAdapter extends HermesASTAdapter {
81
85
  return this.mapExportNamedDeclaration(node);
82
86
  case 'ExportAllDeclaration':
83
87
  return this.mapExportAllDeclaration(node);
84
- case 'PrivateName':
85
- case 'ClassPrivateProperty':
86
- return this.mapPrivateProperty(node);
87
88
  case 'Property':
88
89
  return this.mapProperty(node);
90
+ case 'FunctionDeclaration':
91
+ case 'FunctionExpression':
92
+ case 'ArrowFunctionExpression':
93
+ return this.mapFunction(node);
94
+ case 'PrivateName':
95
+ return this.mapPrivateName(node);
96
+ case 'ClassProperty':
97
+ case 'ClassPrivateProperty':
98
+ return this.mapClassProperty(node);
99
+ case 'MemberExpression':
100
+ case 'OptionalMemberExpression':
101
+ case 'CallExpression':
102
+ case 'OptionalCallExpression':
103
+ return this.mapChainExpression(node);
89
104
  default:
90
105
  return this.mapNodeDefault(node);
91
106
  }
@@ -95,6 +110,8 @@ export default class HermesToESTreeAdapter extends HermesASTAdapter {
95
110
  node = this.mapNodeDefault(node);
96
111
  node.sourceType = this.getSourceType();
97
112
 
113
+ node.docblock = getModuleDocblock(node);
114
+
98
115
  return node;
99
116
  }
100
117
 
@@ -230,4 +247,191 @@ export default class HermesToESTreeAdapter extends HermesASTAdapter {
230
247
 
231
248
  return node;
232
249
  }
250
+
251
+ mapFunction(nodeUnprocessed: HermesNode): HermesNode {
252
+ const node = this.mapNodeDefault(nodeUnprocessed);
253
+
254
+ switch (node.type) {
255
+ case 'FunctionDeclaration':
256
+ case 'FunctionExpression':
257
+ node.expression = false;
258
+ return node;
259
+
260
+ case 'ArrowFunctionExpression':
261
+ node.expression = node.body.type !== 'BlockStatement';
262
+ return node;
263
+ }
264
+
265
+ return node;
266
+ }
267
+
268
+ mapChainExpression(nodeUnprocessed: HermesNode): HermesNode {
269
+ /*
270
+ NOTE - In the below comments `MemberExpression` and `CallExpression`
271
+ are completely interchangable. For terseness we just reference
272
+ one each time.
273
+ */
274
+
275
+ /*
276
+ Hermes uses the old babel-style AST:
277
+ ```
278
+ (one?.two).three?.four;
279
+ ^^^^^^^^^^^^^^^^^^^^^^ OptionalMemberExpression
280
+ ^^^^^^^^^^^^^^^^ MemberExpression
281
+ ^^^^^^^^ OptionalMemberExpression
282
+ ```
283
+
284
+ We need to convert it to the ESTree representation:
285
+ ```
286
+ (one?.two).three?.four;
287
+ ^^^^^^^^^^^^^^^^^^^^^^ ChainExpression
288
+ ^^^^^^^^^^^^^^^^^^^^^^ MemberExpression[optional = true]
289
+ ^^^^^^^^^^^^^^^^ MemberExpression[optional = false]
290
+ ^^^^^^^^ ChainExpression
291
+ ^^^^^^^^ MemberExpression[optional = true]
292
+ ```
293
+
294
+ We do this by converting the AST and its children (depth first), and then unwrapping
295
+ the resulting AST as appropriate.
296
+
297
+ Put another way:
298
+ 1) traverse to the leaf
299
+ 2) if the current node is an `OptionalMemberExpression`:
300
+ a) if the `.object` is a `ChainExpression`:
301
+ i) unwrap the child (`node.object = child.expression`)
302
+ b) convert this node to a `MemberExpression[optional = true]`
303
+ c) wrap this node (`node = ChainExpression[expression = node]`)
304
+ 3) if the current node is a `MembedExpression`:
305
+ a) convert this node to a `MemberExpression[optional = true]`
306
+ */
307
+
308
+ const node = this.mapNodeDefault(nodeUnprocessed);
309
+
310
+ const {child, childKey, isOptional} = ((): {
311
+ child: HermesNode,
312
+ childKey: string,
313
+ isOptional: boolean,
314
+ } => {
315
+ const isOptional: boolean = node.optional === true;
316
+ if (node.type.endsWith('MemberExpression')) {
317
+ return {
318
+ child: node.object,
319
+ childKey: 'object',
320
+ isOptional,
321
+ };
322
+ } else if (node.type.endsWith('CallExpression')) {
323
+ return {
324
+ child: node.callee,
325
+ childKey: 'callee',
326
+ isOptional,
327
+ };
328
+ } else {
329
+ return {
330
+ child: node.expression,
331
+ childKey: 'expression',
332
+ isOptional: false,
333
+ };
334
+ }
335
+ })();
336
+
337
+ const isChildUnwrappable =
338
+ child.type === 'ChainExpression' &&
339
+ // (x?.y).z is semantically different to `x?.y.z`.
340
+ // In the un-parenthesised case `.z` is only executed if and only if `x?.y` returns a non-nullish value.
341
+ // In the parenthesised case, `.z` is **always** executed, regardless of the return of `x?.y`.
342
+ // As such the AST is different between the two cases.
343
+ //
344
+ // In the hermes AST - any member part of a non-short-circuited optional chain is represented with `OptionalMemberExpression`
345
+ // so if we see a `MemberExpression`, then we know we've hit a parenthesis boundary.
346
+ node.type !== 'MemberExpression' &&
347
+ node.type !== 'CallExpression';
348
+
349
+ if (node.type.startsWith('Optional')) {
350
+ node.type = node.type.replace('Optional', '');
351
+ node.optional = isOptional;
352
+ } else {
353
+ node.optional = false;
354
+ }
355
+
356
+ if (!isChildUnwrappable && !isOptional) {
357
+ return node;
358
+ }
359
+
360
+ if (isChildUnwrappable) {
361
+ const newChild = child.expression;
362
+ node[childKey] = newChild;
363
+ }
364
+
365
+ return {
366
+ type: 'ChainExpression',
367
+ expression: node,
368
+ loc: node.loc,
369
+ range: node.range,
370
+ };
371
+ }
372
+
373
+ mapClassProperty(nodeUnprocessed: HermesNode): HermesNode {
374
+ const node = this.mapNodeDefault(nodeUnprocessed);
375
+
376
+ const key = (() => {
377
+ if (node.type === 'ClassPrivateProperty') {
378
+ const key = this.mapNodeDefault(node.key);
379
+ return {
380
+ type: 'PrivateIdentifier',
381
+ name: key.name,
382
+ range: key.range,
383
+ loc: key.loc,
384
+ };
385
+ }
386
+
387
+ return node.key;
388
+ })();
389
+
390
+ return {
391
+ ...node,
392
+ computed: node.type === 'ClassPrivateProperty' ? false : node.computed,
393
+ key,
394
+ type: 'PropertyDefinition',
395
+ };
396
+ }
397
+
398
+ mapPrivateName(node: HermesNode): HermesNode {
399
+ return {
400
+ type: 'PrivateIdentifier',
401
+ name: node.id.name,
402
+ // estree the location refers to the entire string including the hash token
403
+ range: node.range,
404
+ loc: node.loc,
405
+ };
406
+ }
407
+
408
+ mapExportNamedDeclaration(nodeUnprocessed: HermesNode): HermesNode {
409
+ const node = super.mapExportNamedDeclaration(nodeUnprocessed);
410
+
411
+ const namespaceSpecifier = node.specifiers.find(
412
+ spec => spec.type === 'ExportNamespaceSpecifier',
413
+ );
414
+ if (namespaceSpecifier != null) {
415
+ if (node.specifiers.length !== 1) {
416
+ // this should already a hermes parser error - but let's be absolutely sure we're aligned with the spec
417
+ throw new Error('Cannot use an export all with any other specifiers');
418
+ }
419
+ return {
420
+ type: 'ExportAllDeclaration',
421
+ source: node.source,
422
+ exportKind: node.exportKind ?? 'value',
423
+ exported: namespaceSpecifier.exported,
424
+ range: node.range,
425
+ loc: node.loc,
426
+ };
427
+ }
428
+
429
+ return node;
430
+ }
431
+
432
+ mapExportAllDeclaration(nodeUnprocessed: HermesNode): HermesNode {
433
+ const node = super.mapExportAllDeclaration(nodeUnprocessed);
434
+ node.exported = node.exported ?? null;
435
+ return node;
436
+ }
233
437
  }
@@ -77,6 +77,9 @@ const HERMES_AST_VISITOR_KEYS = {
77
77
  param: 'Node',
78
78
  body: 'Node'
79
79
  },
80
+ ChainExpression: {
81
+ expression: 'Node'
82
+ },
80
83
  ClassBody: {
81
84
  body: 'NodeList'
82
85
  },
@@ -102,18 +105,6 @@ const HERMES_AST_VISITOR_KEYS = {
102
105
  id: 'Node',
103
106
  typeParameters: 'Node'
104
107
  },
105
- ClassPrivateProperty: {
106
- key: 'Node',
107
- value: 'Node',
108
- variance: 'Node',
109
- typeAnnotation: 'Node'
110
- },
111
- ClassProperty: {
112
- key: 'Node',
113
- value: 'Node',
114
- variance: 'Node',
115
- typeAnnotation: 'Node'
116
- },
117
108
  ConditionalExpression: {
118
109
  test: 'Node',
119
110
  alternate: 'Node',
@@ -212,6 +203,7 @@ const HERMES_AST_VISITOR_KEYS = {
212
203
  },
213
204
  ExistsTypeAnnotation: {},
214
205
  ExportAllDeclaration: {
206
+ exported: 'Node',
215
207
  source: 'Node'
216
208
  },
217
209
  ExportDefaultDeclaration: {
@@ -222,9 +214,6 @@ const HERMES_AST_VISITOR_KEYS = {
222
214
  specifiers: 'NodeList',
223
215
  source: 'Node'
224
216
  },
225
- ExportNamespaceSpecifier: {
226
- exported: 'Node'
227
- },
228
217
  ExportSpecifier: {
229
218
  exported: 'Node',
230
219
  local: 'Node'
@@ -449,22 +438,11 @@ const HERMES_AST_VISITOR_KEYS = {
449
438
  impltype: 'Node',
450
439
  supertype: 'Node'
451
440
  },
452
- OptionalCallExpression: {
453
- callee: 'Node',
454
- typeArguments: 'Node',
455
- arguments: 'NodeList'
456
- },
457
441
  OptionalIndexedAccessType: {
458
442
  objectType: 'Node',
459
443
  indexType: 'Node'
460
444
  },
461
- OptionalMemberExpression: {
462
- object: 'Node',
463
- property: 'Node'
464
- },
465
- PrivateName: {
466
- id: 'Node'
467
- },
445
+ PrivateIdentifier: {},
468
446
  Program: {
469
447
  body: 'NodeList'
470
448
  },
@@ -472,6 +450,12 @@ const HERMES_AST_VISITOR_KEYS = {
472
450
  key: 'Node',
473
451
  value: 'Node'
474
452
  },
453
+ PropertyDefinition: {
454
+ key: 'Node',
455
+ value: 'Node',
456
+ variance: 'Node',
457
+ typeAnnotation: 'Node'
458
+ },
475
459
  QualifiedTypeIdentifier: {
476
460
  qualification: 'Node',
477
461
  id: 'Node'
@@ -600,6 +584,30 @@ const HERMES_AST_VISITOR_KEYS = {
600
584
  returnType: 'Node',
601
585
  typeParameters: 'NodeList'
602
586
  },
603
- Import: {}
587
+ Import: {},
588
+ ClassProperty: {
589
+ key: 'Node',
590
+ value: 'Node',
591
+ variance: 'Node',
592
+ typeAnnotation: 'Node'
593
+ },
594
+ ClassPrivateProperty: {
595
+ key: 'Node',
596
+ value: 'Node',
597
+ variance: 'Node',
598
+ typeAnnotation: 'Node'
599
+ },
600
+ PrivateName: {
601
+ id: 'Node'
602
+ },
603
+ OptionalCallExpression: {
604
+ callee: 'Node',
605
+ typeArguments: 'Node',
606
+ arguments: 'NodeList'
607
+ },
608
+ OptionalMemberExpression: {
609
+ object: 'Node',
610
+ property: 'Node'
611
+ }
604
612
  };
605
613
  exports.HERMES_AST_VISITOR_KEYS = HERMES_AST_VISITOR_KEYS;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ *
8
+ * @format
9
+ */
10
+ 'use strict';
11
+
12
+ Object.defineProperty(exports, "__esModule", {
13
+ value: true
14
+ });
15
+ exports.getModuleDocblock = getModuleDocblock;
16
+ exports.parseDocblockString = parseDocblockString;
17
+ const DIRECTIVE_REGEX = /^\s*@([a-zA-Z0-9_-]+)( +.+)?$/;
18
+
19
+ function parseDocblockString(docblock) {
20
+ const directiveLines = docblock.split('\n') // remove the leading " *" from each line
21
+ .map(line => line.trimStart().replace(/^\* ?/, '').trim()).filter(line => line.startsWith('@'));
22
+ const directives = {};
23
+
24
+ for (const line of directiveLines) {
25
+ var _match$;
26
+
27
+ const match = DIRECTIVE_REGEX.exec(line);
28
+
29
+ if (match == null) {
30
+ continue;
31
+ }
32
+
33
+ const name = match[1]; // explicitly use an empty string if there's no value
34
+ // this way the array length tracks how many instances of the directive there was
35
+
36
+ const value = ((_match$ = match[2]) != null ? _match$ : '').trim();
37
+
38
+ if (directives[name]) {
39
+ directives[name].push(value);
40
+ } else {
41
+ directives[name] = [value];
42
+ }
43
+ }
44
+
45
+ return directives;
46
+ }
47
+
48
+ function getModuleDocblock(hermesProgram) {
49
+ const docblockNode = (() => {
50
+ if (hermesProgram.type !== 'Program') {
51
+ return null;
52
+ } // $FlowExpectedError[incompatible-type] - escape out of the unsafe hermes types
53
+
54
+
55
+ const program = hermesProgram;
56
+
57
+ if (program.comments.length === 0) {
58
+ return null;
59
+ }
60
+
61
+ const firstComment = (() => {
62
+ const first = program.comments[0];
63
+
64
+ if (first.type === 'Block') {
65
+ return first;
66
+ }
67
+
68
+ if (program.comments.length === 1) {
69
+ return null;
70
+ } // ESLint will always strip out the shebang comment from the code before passing it to the parser
71
+ // https://github.com/eslint/eslint/blob/21d647904dc30f9484b22acdd9243a6d0ecfba38/lib/linter/linter.js#L779
72
+ // this means that we're forced to parse it as a line comment :(
73
+ // this hacks around it by selecting the second comment in this case
74
+
75
+
76
+ const second = program.comments[1];
77
+
78
+ if (first.type === 'Line' && first.range[0] === 0 && second.type === 'Block') {
79
+ return second;
80
+ }
81
+
82
+ return null;
83
+ })();
84
+
85
+ if (firstComment == null) {
86
+ return null;
87
+ }
88
+ /*
89
+ Handle cases like this where the comment isn't actually the first thing in the code:
90
+ ```
91
+ const x = 1; /* docblock *./
92
+ ```
93
+ */
94
+
95
+
96
+ if (program.body.length > 0 && program.body[0].range[0] < firstComment.range[0]) {
97
+ return null;
98
+ }
99
+
100
+ return firstComment;
101
+ })();
102
+
103
+ if (docblockNode == null) {
104
+ return null;
105
+ }
106
+
107
+ return {
108
+ directives: parseDocblockString(docblockNode.value),
109
+ comment: docblockNode
110
+ };
111
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow strict
8
+ * @format
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ import type {HermesNode} from './HermesAST';
14
+ import type {DocblockDirectives, Program} from 'hermes-estree';
15
+
16
+ const DIRECTIVE_REGEX = /^\s*@([a-zA-Z0-9_-]+)( +.+)?$/;
17
+
18
+ export function parseDocblockString(docblock: string): DocblockDirectives {
19
+ const directiveLines = docblock
20
+ .split('\n')
21
+ // remove the leading " *" from each line
22
+ .map(line => line.trimStart().replace(/^\* ?/, '').trim())
23
+ .filter(line => line.startsWith('@'));
24
+
25
+ const directives: {
26
+ [string]: Array<string>,
27
+ } = {};
28
+
29
+ for (const line of directiveLines) {
30
+ const match = DIRECTIVE_REGEX.exec(line);
31
+ if (match == null) {
32
+ continue;
33
+ }
34
+ const name = match[1];
35
+ // explicitly use an empty string if there's no value
36
+ // this way the array length tracks how many instances of the directive there was
37
+ const value = (match[2] ?? '').trim();
38
+ if (directives[name]) {
39
+ directives[name].push(value);
40
+ } else {
41
+ directives[name] = [value];
42
+ }
43
+ }
44
+
45
+ return directives;
46
+ }
47
+
48
+ export function getModuleDocblock(
49
+ hermesProgram: HermesNode,
50
+ ): Program['docblock'] {
51
+ const docblockNode = (() => {
52
+ if (hermesProgram.type !== 'Program') {
53
+ return null;
54
+ }
55
+
56
+ // $FlowExpectedError[incompatible-type] - escape out of the unsafe hermes types
57
+ const program: Program = hermesProgram;
58
+
59
+ if (program.comments.length === 0) {
60
+ return null;
61
+ }
62
+
63
+ const firstComment = (() => {
64
+ const first = program.comments[0];
65
+ if (first.type === 'Block') {
66
+ return first;
67
+ }
68
+
69
+ if (program.comments.length === 1) {
70
+ return null;
71
+ }
72
+
73
+ // ESLint will always strip out the shebang comment from the code before passing it to the parser
74
+ // https://github.com/eslint/eslint/blob/21d647904dc30f9484b22acdd9243a6d0ecfba38/lib/linter/linter.js#L779
75
+ // this means that we're forced to parse it as a line comment :(
76
+ // this hacks around it by selecting the second comment in this case
77
+ const second = program.comments[1];
78
+ if (
79
+ first.type === 'Line' &&
80
+ first.range[0] === 0 &&
81
+ second.type === 'Block'
82
+ ) {
83
+ return second;
84
+ }
85
+
86
+ return null;
87
+ })();
88
+ if (firstComment == null) {
89
+ return null;
90
+ }
91
+
92
+ /*
93
+ Handle cases like this where the comment isn't actually the first thing in the code:
94
+ ```
95
+ const x = 1; /* docblock *./
96
+ ```
97
+ */
98
+ if (
99
+ program.body.length > 0 &&
100
+ program.body[0].range[0] < firstComment.range[0]
101
+ ) {
102
+ return null;
103
+ }
104
+
105
+ return firstComment;
106
+ })();
107
+
108
+ if (docblockNode == null) {
109
+ return null;
110
+ }
111
+
112
+ return {
113
+ directives: parseDocblockString(docblockNode.value),
114
+ comment: docblockNode,
115
+ };
116
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermes-parser",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "A JavaScript parser built from the Hermes engine",
5
5
  "main": "dist/index.js",
6
6
  "license": "MIT",
@@ -9,10 +9,12 @@
9
9
  "url": "git@github.com:facebook/hermes.git"
10
10
  },
11
11
  "dependencies": {
12
- "hermes-estree": "0.7.0"
12
+ "hermes-estree": "0.8.0"
13
13
  },
14
14
  "devDependencies": {
15
- "hermes-transform": "0.7.0"
15
+ "@babel/parser": "7.7.4",
16
+ "espree": "9.3.2",
17
+ "hermes-transform": "0.8.0"
16
18
  },
17
19
  "files": [
18
20
  "dist"