ASTParserFacade.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import * as acorn from 'acorn';
  2. import * as ESTree from 'estree';
  3. import chalk, { Chalk } from 'chalk';
  4. /**
  5. * Facade over AST parser `acorn`
  6. */
  7. export class ASTParserFacade {
  8. /**
  9. * @type {Chalk}
  10. */
  11. private static readonly colorError: Chalk = chalk.red;
  12. /**
  13. * @type {number}
  14. */
  15. private static readonly nearestSymbolsCount: number = 15;
  16. /**
  17. * @type {acorn.Options['sourceType'][]}
  18. */
  19. private static readonly sourceTypes: acorn.Options['sourceType'][] = [
  20. 'script',
  21. 'module'
  22. ];
  23. /**
  24. * @param {string} sourceCode
  25. * @param {Options} config
  26. * @returns {Program}
  27. */
  28. public static parse (sourceCode: string, config: acorn.Options): ESTree.Program | never {
  29. const sourceTypeLength: number = ASTParserFacade.sourceTypes.length;
  30. for (let i: number = 0; i < sourceTypeLength; i++) {
  31. try {
  32. return ASTParserFacade.parseType(sourceCode, config, ASTParserFacade.sourceTypes[i]);
  33. } catch (error) {
  34. if (i < sourceTypeLength - 1) {
  35. continue;
  36. }
  37. throw new Error(ASTParserFacade.processParsingError(
  38. sourceCode,
  39. error.message,
  40. error.loc
  41. ));
  42. }
  43. }
  44. throw new Error('Acorn parsing error');
  45. }
  46. /**
  47. * @param {string} sourceCode
  48. * @param {acorn.Options} inputConfig
  49. * @param {acorn.Options["sourceType"]} sourceType
  50. * @returns {Program}
  51. */
  52. private static parseType (
  53. sourceCode: string,
  54. inputConfig: acorn.Options,
  55. sourceType: acorn.Options['sourceType']
  56. ): ESTree.Program {
  57. const comments: ESTree.Comment[] = [];
  58. const config: acorn.Options = {
  59. ...inputConfig,
  60. allowAwaitOutsideFunction: true,
  61. onComment: comments,
  62. sourceType
  63. };
  64. const program: acorn.Node & ESTree.Program = <acorn.Node & ESTree.Program>acorn
  65. .parse(sourceCode, config);
  66. if (comments.length) {
  67. program.comments = comments;
  68. }
  69. return program;
  70. }
  71. /**
  72. * @param {string} sourceCode
  73. * @param {string} errorMessage
  74. * @param {Position | null} position
  75. * @returns {never}
  76. */
  77. private static processParsingError (
  78. sourceCode: string,
  79. errorMessage: string,
  80. position: ESTree.Position | null
  81. ): never {
  82. const isMissingLocationData = !position || !position.line || !position.column;
  83. if (isMissingLocationData) {
  84. throw new Error(errorMessage);
  85. }
  86. const sourceCodeLines: string[] = sourceCode.split(/\r?\n/);
  87. const errorLine: string | undefined = sourceCodeLines[position.line - 1];
  88. if (!errorLine) {
  89. throw new Error(errorMessage);
  90. }
  91. const startErrorIndex: number = Math.max(0, position.column - ASTParserFacade.nearestSymbolsCount);
  92. const endErrorIndex: number = Math.min(errorLine.length, position.column + ASTParserFacade.nearestSymbolsCount);
  93. const formattedPointer: string = ASTParserFacade.colorError('>');
  94. const formattedCodeSlice: string = `...${
  95. errorLine.slice(startErrorIndex, endErrorIndex).replace(/^\s+/, '')
  96. }...`;
  97. throw new Error(
  98. `ERROR at line ${position.line}: ${errorMessage}\n${formattedPointer} ${formattedCodeSlice}`
  99. );
  100. }
  101. }