ASTParserFacade.ts 3.8 KB

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