ASTParserFacade.ts 3.4 KB

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