@appthreat/atom-parsetools 1.2.2 → 1.4.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.
Files changed (267) hide show
  1. package/README.md +118 -7
  2. package/astgen.js +297 -57
  3. package/package.json +9 -6
  4. package/plugins/composer/installed.php +6 -6
  5. package/plugins/rubyastgen/Gemfile +6 -4
  6. package/plugins/rubyastgen/Gemfile.lock +5 -8
  7. package/plugins/rubyastgen/bundle/ruby/4.0.0/bundler/gems/ruby_ast_gen-4ac84327b3af/Gemfile +11 -0
  8. package/plugins/rubyastgen/bundle/ruby/4.0.0/bundler/gems/ruby_ast_gen-4ac84327b3af/exe/ruby_ast_gen +55 -0
  9. package/plugins/rubyastgen/bundle/ruby/4.0.0/bundler/gems/ruby_ast_gen-4ac84327b3af/lib/ruby_ast_gen/cli.rb +133 -0
  10. package/plugins/rubyastgen/bundle/ruby/4.0.0/bundler/gems/ruby_ast_gen-4ac84327b3af/lib/ruby_ast_gen/node_handling.rb +449 -0
  11. package/plugins/rubyastgen/bundle/ruby/4.0.0/bundler/gems/{ruby_ast_gen-cc312ebf06b4 → ruby_ast_gen-4ac84327b3af}/lib/ruby_ast_gen/version.rb +1 -1
  12. package/plugins/rubyastgen/bundle/ruby/4.0.0/bundler/gems/ruby_ast_gen-4ac84327b3af/lib/ruby_ast_gen.rb +888 -0
  13. package/plugins/rubyastgen/bundle/ruby/4.0.0/bundler/gems/ruby_ast_gen-4ac84327b3af/ruby_ast_gen.gemspec +25 -0
  14. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/current.rb +1 -1
  15. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/version.rb +1 -1
  16. package/plugins/rubyastgen/bundle/ruby/4.0.0/specifications/ast-2.4.3.gemspec +29 -0
  17. package/plugins/rubyastgen/bundle/ruby/4.0.0/specifications/bundler-2.6.9.gemspec +25 -0
  18. package/plugins/rubyastgen/bundle/ruby/4.0.0/specifications/ostruct-0.6.3.gemspec +22 -0
  19. package/plugins/rubyastgen/bundle/ruby/4.0.0/specifications/parser-3.3.12.0.gemspec +37 -0
  20. package/rbastgen.js +102 -13
  21. package/scalasem.js +11 -2
  22. package/plugins/bin/bundler +0 -14
  23. package/plugins/bin/ruby_ast_gen +0 -14
  24. package/plugins/rubyastgen/bundle/bundler/setup.rb +0 -44
  25. package/plugins/rubyastgen/bundle/ruby/4.0.0/bundler/gems/ruby_ast_gen-cc312ebf06b4/exe/ruby_ast_gen +0 -63
  26. package/plugins/rubyastgen/bundle/ruby/4.0.0/bundler/gems/ruby_ast_gen-cc312ebf06b4/lib/ruby_ast_gen/node_handling.rb +0 -269
  27. package/plugins/rubyastgen/bundle/ruby/4.0.0/bundler/gems/ruby_ast_gen-cc312ebf06b4/lib/ruby_ast_gen.rb +0 -218
  28. package/plugins/rubyastgen/bundle/ruby/4.0.0/extensions/x86_64-linux/4.0.0/prism-1.9.0/gem.build_complete +0 -0
  29. package/plugins/rubyastgen/bundle/ruby/4.0.0/extensions/x86_64-linux/4.0.0/prism-1.9.0/gem_make.out +0 -44
  30. package/plugins/rubyastgen/bundle/ruby/4.0.0/extensions/x86_64-linux/4.0.0/prism-1.9.0/mkmf.log +0 -74
  31. package/plugins/rubyastgen/bundle/ruby/4.0.0/extensions/x86_64-linux/4.0.0/prism-1.9.0/prism/prism.so +0 -0
  32. package/plugins/rubyastgen/bundle/ruby/4.0.0/extensions/x86_64-linux/4.0.0/racc-1.8.1/gem.build_complete +0 -0
  33. package/plugins/rubyastgen/bundle/ruby/4.0.0/extensions/x86_64-linux/4.0.0/racc-1.8.1/gem_make.out +0 -18
  34. package/plugins/rubyastgen/bundle/ruby/4.0.0/extensions/x86_64-linux/4.0.0/racc-1.8.1/racc/cparse.so +0 -0
  35. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/BSDmakefile +0 -58
  36. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/Makefile +0 -116
  37. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/ext/prism/Makefile +0 -273
  38. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/ext/prism/api_node.c +0 -6945
  39. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/ext/prism/api_pack.c +0 -276
  40. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/ext/prism/extconf.rb +0 -127
  41. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/ext/prism/extension.c +0 -1427
  42. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/ext/prism/extension.h +0 -19
  43. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/ast.h +0 -8254
  44. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/defines.h +0 -260
  45. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/diagnostic.h +0 -458
  46. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/encoding.h +0 -283
  47. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/node.h +0 -129
  48. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/options.h +0 -488
  49. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/pack.h +0 -163
  50. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/parser.h +0 -936
  51. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/prettyprint.h +0 -34
  52. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/regexp.h +0 -43
  53. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/static_literals.h +0 -121
  54. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/util/pm_buffer.h +0 -236
  55. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/util/pm_char.h +0 -204
  56. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/util/pm_constant_pool.h +0 -218
  57. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/util/pm_integer.h +0 -130
  58. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/util/pm_list.h +0 -103
  59. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/util/pm_memchr.h +0 -29
  60. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/util/pm_newline_list.h +0 -113
  61. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/util/pm_string.h +0 -200
  62. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/util/pm_strncasecmp.h +0 -32
  63. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/util/pm_strpbrk.h +0 -46
  64. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism/version.h +0 -29
  65. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/include/prism.h +0 -408
  66. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/compiler.rb +0 -801
  67. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/desugar_compiler.rb +0 -392
  68. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/dispatcher.rb +0 -2210
  69. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/dot_visitor.rb +0 -4767
  70. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/dsl.rb +0 -1003
  71. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/ffi.rb +0 -578
  72. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/inspect_visitor.rb +0 -2393
  73. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/lex_compat.rb +0 -911
  74. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/mutation_compiler.rb +0 -772
  75. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/node.rb +0 -19966
  76. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/node_ext.rb +0 -511
  77. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/pack.rb +0 -230
  78. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/parse_result/comments.rb +0 -188
  79. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/parse_result/errors.rb +0 -66
  80. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/parse_result/newlines.rb +0 -155
  81. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/parse_result.rb +0 -907
  82. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/pattern.rb +0 -269
  83. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/polyfill/append_as_bytes.rb +0 -15
  84. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/polyfill/byteindex.rb +0 -13
  85. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/polyfill/scan_byte.rb +0 -14
  86. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/polyfill/unpack1.rb +0 -14
  87. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/polyfill/warn.rb +0 -36
  88. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/prism.so +0 -0
  89. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/reflection.rb +0 -416
  90. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/relocation.rb +0 -505
  91. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/serialize.rb +0 -2400
  92. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/string_query.rb +0 -31
  93. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/translation/parser/builder.rb +0 -62
  94. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/translation/parser/compiler.rb +0 -2234
  95. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/translation/parser/lexer.rb +0 -820
  96. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/translation/parser.rb +0 -376
  97. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/translation/parser_current.rb +0 -26
  98. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/translation/parser_versions.rb +0 -36
  99. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/translation/ripper/filter.rb +0 -53
  100. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/translation/ripper/lexer.rb +0 -135
  101. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/translation/ripper/sexp.rb +0 -126
  102. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/translation/ripper/shim.rb +0 -5
  103. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/translation/ripper.rb +0 -3520
  104. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/translation/ruby_parser.rb +0 -1964
  105. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/translation.rb +0 -18
  106. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism/visitor.rb +0 -813
  107. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/lib/prism.rb +0 -98
  108. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/rbi/prism/compiler.rbi +0 -12
  109. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/rbi/prism/dsl.rbi +0 -524
  110. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/rbi/prism/inspect_visitor.rbi +0 -12
  111. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/rbi/prism/node.rbi +0 -8750
  112. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/rbi/prism/node_ext.rbi +0 -107
  113. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/rbi/prism/parse_result.rbi +0 -404
  114. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/rbi/prism/reflection.rbi +0 -58
  115. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/rbi/prism/string_query.rbi +0 -12
  116. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/rbi/prism/translation/parser.rbi +0 -11
  117. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/rbi/prism/translation/parser_versions.rbi +0 -23
  118. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/rbi/prism/translation/ripper.rbi +0 -15
  119. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/rbi/prism/visitor.rbi +0 -473
  120. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/rbi/prism.rbi +0 -63
  121. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/sig/prism/compiler.rbs +0 -9
  122. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/sig/prism/dispatcher.rbs +0 -19
  123. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/sig/prism/dot_visitor.rbs +0 -6
  124. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/sig/prism/dsl.rbs +0 -351
  125. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/sig/prism/inspect_visitor.rbs +0 -22
  126. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/sig/prism/lex_compat.rbs +0 -10
  127. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/sig/prism/mutation_compiler.rbs +0 -159
  128. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/sig/prism/node.rbs +0 -4042
  129. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/sig/prism/node_ext.rbs +0 -149
  130. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/sig/prism/pack.rbs +0 -43
  131. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/sig/prism/parse_result/comments.rbs +0 -38
  132. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/sig/prism/parse_result.rbs +0 -197
  133. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/sig/prism/pattern.rbs +0 -13
  134. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/sig/prism/reflection.rbs +0 -50
  135. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/sig/prism/relocation.rbs +0 -185
  136. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/sig/prism/serialize.rbs +0 -8
  137. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/sig/prism/string_query.rbs +0 -11
  138. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/sig/prism/visitor.rbs +0 -169
  139. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/sig/prism.rbs +0 -272
  140. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/src/diagnostic.c +0 -854
  141. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/src/encoding.c +0 -5340
  142. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/src/node.c +0 -8685
  143. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/src/options.c +0 -338
  144. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/src/pack.c +0 -509
  145. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/src/prettyprint.c +0 -8957
  146. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/src/prism.c +0 -22679
  147. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/src/regexp.c +0 -790
  148. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/src/serialize.c +0 -2274
  149. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/src/static_literals.c +0 -617
  150. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/src/token_type.c +0 -703
  151. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/src/util/pm_buffer.c +0 -357
  152. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/src/util/pm_char.c +0 -318
  153. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/src/util/pm_constant_pool.c +0 -342
  154. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/src/util/pm_integer.c +0 -670
  155. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/src/util/pm_list.c +0 -49
  156. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/src/util/pm_memchr.c +0 -35
  157. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/src/util/pm_newline_list.c +0 -125
  158. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/src/util/pm_string.c +0 -381
  159. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/src/util/pm_strncasecmp.c +0 -36
  160. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/prism-1.9.0/src/util/pm_strpbrk.c +0 -206
  161. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/BSDL +0 -22
  162. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/COPYING +0 -56
  163. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/ChangeLog +0 -846
  164. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/README.ja.rdoc +0 -58
  165. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/README.rdoc +0 -60
  166. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/TODO +0 -5
  167. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/bin/racc +0 -320
  168. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/ext/racc/cparse/Makefile +0 -273
  169. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/ext/racc/cparse/cparse.c +0 -840
  170. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/ext/racc/cparse/extconf.rb +0 -8
  171. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/lib/racc/compat.rb +0 -33
  172. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/lib/racc/cparse.so +0 -0
  173. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/lib/racc/debugflags.rb +0 -60
  174. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/lib/racc/exception.rb +0 -16
  175. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/lib/racc/grammar.rb +0 -1191
  176. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/lib/racc/grammarfileparser.rb +0 -667
  177. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/lib/racc/info.rb +0 -18
  178. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/lib/racc/iset.rb +0 -92
  179. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/lib/racc/logfilegenerator.rb +0 -212
  180. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/lib/racc/parser-text.rb +0 -644
  181. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/lib/racc/parser.rb +0 -630
  182. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/lib/racc/parserfilegenerator.rb +0 -473
  183. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/lib/racc/sourcetext.rb +0 -35
  184. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/lib/racc/state.rb +0 -976
  185. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/lib/racc/statetransitiontable.rb +0 -311
  186. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/lib/racc/static.rb +0 -5
  187. package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/racc-1.8.1/lib/racc.rb +0 -6
  188. /package/plugins/rubyastgen/bundle/ruby/4.0.0/bundler/gems/{ruby_ast_gen-cc312ebf06b4 → ruby_ast_gen-4ac84327b3af}/.rspec +0 -0
  189. /package/plugins/rubyastgen/bundle/ruby/4.0.0/bundler/gems/{ruby_ast_gen-cc312ebf06b4 → ruby_ast_gen-4ac84327b3af}/bin/console +0 -0
  190. /package/plugins/rubyastgen/bundle/ruby/4.0.0/bundler/gems/{ruby_ast_gen-cc312ebf06b4 → ruby_ast_gen-4ac84327b3af}/bin/setup +0 -0
  191. /package/plugins/rubyastgen/bundle/ruby/4.0.0/bundler/gems/{ruby_ast_gen-cc312ebf06b4 → ruby_ast_gen-4ac84327b3af}/update_version.rb +0 -0
  192. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/bin/ruby-parse +0 -0
  193. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/bin/ruby-rewrite +0 -0
  194. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/gauntlet_parser.rb +0 -0
  195. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/all.rb +0 -0
  196. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/ast/node.rb +0 -0
  197. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/ast/processor.rb +0 -0
  198. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/base.rb +0 -0
  199. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/builders/default.rb +0 -0
  200. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/clobbering_error.rb +0 -0
  201. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/color.rb +0 -0
  202. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/context.rb +0 -0
  203. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/current_arg_stack.rb +0 -0
  204. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/deprecation.rb +0 -0
  205. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/diagnostic/engine.rb +0 -0
  206. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/diagnostic.rb +0 -0
  207. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/lexer/dedenter.rb +0 -0
  208. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/lexer/explanation.rb +0 -0
  209. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/lexer/literal.rb +0 -0
  210. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/lexer/stack_state.rb +0 -0
  211. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/lexer-F0.rb +0 -0
  212. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/lexer-F1.rb +0 -0
  213. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/lexer-strings.rb +0 -0
  214. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/macruby.rb +0 -0
  215. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/max_numparam_stack.rb +0 -0
  216. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/messages.rb +0 -0
  217. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/meta.rb +0 -0
  218. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/rewriter.rb +0 -0
  219. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/ruby18.rb +0 -0
  220. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/ruby19.rb +0 -0
  221. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/ruby20.rb +0 -0
  222. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/ruby21.rb +0 -0
  223. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/ruby22.rb +0 -0
  224. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/ruby23.rb +0 -0
  225. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/ruby24.rb +0 -0
  226. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/ruby25.rb +0 -0
  227. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/ruby26.rb +0 -0
  228. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/ruby27.rb +0 -0
  229. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/ruby30.rb +0 -0
  230. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/ruby31.rb +0 -0
  231. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/ruby32.rb +0 -0
  232. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/ruby33.rb +0 -0
  233. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/ruby34.rb +0 -0
  234. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/rubymotion.rb +0 -0
  235. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/runner/ruby_parse.rb +0 -0
  236. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/runner/ruby_rewrite.rb +0 -0
  237. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/runner.rb +0 -0
  238. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/buffer.rb +0 -0
  239. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/comment/associator.rb +0 -0
  240. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/comment.rb +0 -0
  241. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/map/collection.rb +0 -0
  242. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/map/condition.rb +0 -0
  243. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/map/constant.rb +0 -0
  244. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/map/definition.rb +0 -0
  245. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/map/for.rb +0 -0
  246. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/map/heredoc.rb +0 -0
  247. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/map/index.rb +0 -0
  248. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/map/keyword.rb +0 -0
  249. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/map/method_definition.rb +0 -0
  250. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/map/objc_kwarg.rb +0 -0
  251. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/map/operator.rb +0 -0
  252. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/map/rescue_body.rb +0 -0
  253. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/map/send.rb +0 -0
  254. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/map/ternary.rb +0 -0
  255. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/map/variable.rb +0 -0
  256. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/map.rb +0 -0
  257. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/range.rb +0 -0
  258. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/rewriter/action.rb +0 -0
  259. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/rewriter.rb +0 -0
  260. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/tree_rewriter/action.rb +0 -0
  261. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/source/tree_rewriter.rb +0 -0
  262. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/static_environment.rb +0 -0
  263. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/syntax_error.rb +0 -0
  264. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/tree_rewriter.rb +0 -0
  265. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/unknown_encoding_in_magic_comment_error.rb +0 -0
  266. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser/variables_stack.rb +0 -0
  267. /package/plugins/rubyastgen/bundle/ruby/4.0.0/gems/{parser-3.3.11.1 → parser-3.3.12.0}/lib/parser.rb +0 -0
@@ -0,0 +1,888 @@
1
+ require 'fileutils'
2
+ require 'json'
3
+ require 'thread'
4
+ require 'time'
5
+
6
+ require_relative 'ruby_ast_gen/version'
7
+ require_relative 'ruby_ast_gen/node_handling'
8
+ require_relative 'ruby_ast_gen/cli'
9
+
10
+ module RubyAstGen
11
+
12
+ # All human-facing output goes through here, on stdout (chen captures it). Messages print when
13
+ # their rank is at or above the configured level's rank, so the default :info keeps DEBUG quiet
14
+ # instead of one line per file (plan 03 §10).
15
+ module Logger
16
+
17
+ LEVEL_RANKS = { debug: 0, info: 1, warn: 2, error: 3 }.freeze
18
+ private_constant :LEVEL_RANKS
19
+
20
+ def self.level
21
+ @level ||= :info
22
+ end
23
+
24
+ def self.level=(value)
25
+ normalized = value.to_s.downcase.to_sym
26
+ if LEVEL_RANKS.key?(normalized)
27
+ @level = normalized
28
+ else
29
+ # A mistyped level is a configuration mistake, not a usage error: fall back to :info
30
+ # rather than failing the run.
31
+ @level = :info
32
+ warn "Unknown log level #{value.inspect}; using :info."
33
+ end
34
+ end
35
+
36
+ # Restores the default level. The level is process-global mutable state, so the test suite
37
+ # resets it after every example.
38
+ def self.reset_level!
39
+ @level = nil
40
+ end
41
+
42
+ def self.debug(message)
43
+ puts "[DEBUG] #{message}" if enabled?(:debug)
44
+ end
45
+
46
+ def self.info(message)
47
+ puts "[INFO] #{message}" if enabled?(:info)
48
+ end
49
+
50
+ def self.warn(message)
51
+ puts "[WARN] #{message}" if enabled?(:warn)
52
+ end
53
+
54
+ def self.error(message)
55
+ puts "[ERR] #{message}" if enabled?(:error)
56
+ end
57
+
58
+ def self.enabled?(message_level)
59
+ LEVEL_RANKS[level] <= LEVEL_RANKS[message_level]
60
+ end
61
+ private_class_method :enabled?
62
+ end
63
+
64
+ # Main method to parse the input and generate the AST output
65
+ # Errors a single file may raise without ending the run: the consumer reads a non-zero exit as
66
+ # "no files were parsed at all", so a per-file problem must never become one.
67
+ #
68
+ # SystemStackError is listed deliberately — it is not a StandardError, and raised inside a worker
69
+ # thread it kills the thread and silently abandons the rest of its queue. JRuby needs one more:
70
+ # when the stack runs out inside a *Java* frame (prism's node loading, which is where a deeply
71
+ # nested literal lands) it raises java.lang.StackOverflowError, which is neither of the two. That
72
+ # escaped every rescue here, aborted the run and exited non-zero — the failure mode this list
73
+ # exists to prevent. Measured ceilings vary by runtime and by thread: the prism translation gives
74
+ # up at ~5000 nested literals on CRuby's main thread but at ~250 in a JRuby worker with a 512 KB
75
+ # stack, and serialization has its own ceiling (see .serialize) — all of them degrade to
76
+ # log + skip.
77
+ PER_FILE_ERRORS = begin
78
+ errors = [StandardError, SystemStackError]
79
+ errors << Java::JavaLang::StackOverflowError if RUBY_PLATFORM == "java"
80
+ errors.freeze
81
+ rescue NameError
82
+ [StandardError, SystemStackError].freeze
83
+ end
84
+
85
+ # Side-records of a run. Both live *inside* the output directory and both end in `.jsonl`
86
+ # on purpose: chen feeds every file ending in `.json` to its JSON AST reader, which requires
87
+ # a top-level file_path (plan 04 §10.2), so a `.json` side-record would silently miscount
88
+ # every run (plan 03 §3/§4).
89
+ DIAGNOSTICS_FILENAME = "ruby_ast_gen_diagnostics.jsonl"
90
+ MANIFEST_FILENAME = "ruby_ast_gen_manifest.jsonl"
91
+
92
+ # Per-file warning entries kept in the top-level diagnostics array before the rest are cut
93
+ # (diagnostics_truncated marks the cut). The full list stays on stdout for -l debug readers.
94
+ DIAGNOSTICS_CAP = 50
95
+
96
+ def self.parse(opts)
97
+ # Apply the level before anything can log, and specifically before the worker threads in
98
+ # process_directory are spawned: they inherit whatever is configured here.
99
+ Logger.level = :debug if opts[:debug]
100
+ Logger.level = opts[:log_level] if opts[:log_level]
101
+
102
+ Logger.debug "CLI Arguments received: #{opts}"
103
+
104
+ input_path = opts[:input]
105
+ output_dir = opts[:output]
106
+ exclude_regex = validated_exclude(opts[:exclude])
107
+ parser_target = validated_parser_target(opts[:parser_target])
108
+ max_depth = validated_max_depth(opts[:max_depth])
109
+ threads = validated_threads(opts[:threads])
110
+
111
+ raise ArgumentError, "Error: '-i' or '--input' is required." if input_path.nil?
112
+
113
+ Logger.debug "Exclude Regex Received: #{exclude_regex}"
114
+
115
+ FileUtils.mkdir_p(output_dir)
116
+
117
+ # Per-run aggregate, shared by the worker threads. The mutex guards every update: each
118
+ # counter would otherwise race across the 10 workers, and the truncation counter in
119
+ # NodeHandling is per-file state for exactly this reason. No module-level state, so
120
+ # concurrent or repeated parses cannot pollute each other.
121
+ state = {
122
+ mutex: Mutex.new, parsed: 0, failed: 0, skipped_nonruby: 0, excluded: 0,
123
+ truncated_files: 0, parse_errors: []
124
+ }
125
+
126
+ if File.file?(input_path)
127
+ process_file(input_path, output_dir, exclude_regex, input_path, parser_target: parser_target,
128
+ max_depth: max_depth, state: state)
129
+ elsif File.directory?(input_path)
130
+ process_directory(input_path, output_dir, exclude_regex, threads, parser_target: parser_target,
131
+ max_depth: max_depth, state: state)
132
+ else
133
+ # Usage errors are raised, not logged-and-exited: the caller decides the exit status, and
134
+ # the message must not be silenceable by --log (it is the only output of a failed run).
135
+ raise ArgumentError, "#{input_path} is neither a file nor a directory."
136
+ end
137
+
138
+ # The manifest is both written and returned: the CLI reads files_failed off it to implement
139
+ # --fail-on-error.
140
+ manifest = manifest_for(input_path, output_dir, state, threads: threads, max_depth: max_depth,
141
+ parser_target: parser_target)
142
+ write_run_records(output_dir, state, manifest)
143
+ manifest
144
+ end
145
+
146
+ private
147
+
148
+ # Raises rather than exiting: this is a library entry point, so a bad target is the caller's
149
+ # error to surface. The CLI rescues this into the documented usage error (exit 1).
150
+ def self.validated_parser_target(parser_target)
151
+ return unless parser_target
152
+
153
+ Gem::Version.new(parser_target)
154
+ rescue ArgumentError
155
+ raise ArgumentError, "Invalid --parser-target '#{parser_target}'. Expected a version like 3.4 or 4.0."
156
+ end
157
+
158
+ # An unusable exclusion regex is a usage error, like an invalid --parser-target: ignoring it
159
+ # would silently parse everything the caller meant to skip, so it fails with a message rather
160
+ # than a RegexpError backtrace. A missing value falls back to the documented default so the
161
+ # library API can be called without it.
162
+ def self.validated_exclude(pattern)
163
+ Regexp.new(pattern || CLI::DEFAULTS[:exclude])
164
+ rescue RegexpError, TypeError => e
165
+ raise ArgumentError, "Invalid --exclude '#{pattern}': #{e.message}"
166
+ end
167
+
168
+ def self.validated_threads(value)
169
+ return CLI::DEFAULT_THREADS unless value
170
+
171
+ count = Integer(value.to_s, 10)
172
+ return count if count >= 1
173
+
174
+ raise ArgumentError
175
+ rescue ArgumentError, TypeError
176
+ Logger.warn "Invalid --threads '#{value}'; using the default of #{CLI::DEFAULT_THREADS}."
177
+ CLI::DEFAULT_THREADS
178
+ end
179
+
180
+ # A bad depth is a configuration mistake, not a usage error: warn and fall back to the
181
+ # default rather than failing the run (the same policy as an unknown --log level).
182
+ def self.validated_max_depth(value)
183
+ return NodeHandling::MAX_NESTING_DEPTH unless value
184
+
185
+ depth = Integer(value.to_s, 10)
186
+ return depth if depth >= 1
187
+
188
+ raise ArgumentError
189
+ rescue ArgumentError, TypeError
190
+ Logger.warn "Invalid --max-depth '#{value}'; using the default of #{NodeHandling::MAX_NESTING_DEPTH}."
191
+ NodeHandling::MAX_NESTING_DEPTH
192
+ end
193
+ # Process a single file and generate its AST. +state+ is the per-run aggregate (see .parse);
194
+ # every outcome is recorded there so the manifest can report what a run actually did.
195
+ def self.process_file(file_path, output_dir, exclude_regex, base_dir, parser_target: nil,
196
+ max_depth: NodeHandling::MAX_NESTING_DEPTH, state: nil)
197
+ # Get the relative path of the file to apply exclusion rules
198
+ relative_path = file_path.sub(%r{^.*\/}, '')
199
+ # In single-file mode the caller passes the file itself as base_dir, so the sub below would be
200
+ # a no-op and both the exclusion match and rel_file_path would see the *absolute* path: `-i
201
+ # /tmp/x/keep.rb -e tmp` dropped the file because "tmp" appears in a directory component
202
+ # (plan 03 §5). Only the path relative to the input is ever matched or reported.
203
+ relative_input_path = if base_dir == file_path
204
+ File.basename(file_path)
205
+ else
206
+ file_path.sub("#{base_dir}/", '')
207
+ end
208
+ # Skip if the file matches the exclusion regex
209
+ if exclude_regex && exclude_regex.match?(relative_input_path)
210
+ RubyAstGen::Logger.debug "Excluding: #{relative_input_path}"
211
+ # Directory runs filter at scan time, so this only fires (and counts) for single-file inputs.
212
+ count_state(state, :excluded)
213
+ return
214
+ end
215
+
216
+ unless ruby_file?(file_path) # Skip if it's not a Ruby-related file
217
+ count_state(state, :skipped_nonruby)
218
+ return
219
+ end
220
+
221
+ file_result = {outcome: :empty, parse_error: nil}
222
+ begin
223
+ ast = parse_file(file_path, relative_input_path, parser_target: parser_target,
224
+ max_depth: max_depth, result: file_result)
225
+ if ast.nil?
226
+ # A failed parse carries its diagnosis in file_result; a nil AST from a clean parse is a
227
+ # comments-only file, which parsed fine but produces no JSON (counted as parsed-empty).
228
+ record_failure(state, file_path, relative_input_path, file_result[:parse_error]) if file_result[:outcome] == :failed
229
+ count_state(state, :parsed) unless file_result[:outcome] == :failed
230
+ return
231
+ end
232
+
233
+ output_path = File.join(output_dir, "#{relative_path}.json")
234
+
235
+ File.write(output_path, serialize(ast, max_depth))
236
+ count_state(state, :parsed)
237
+ count_state(state, :truncated_files) if ast[:truncated_nodes] > 0
238
+ rescue *PER_FILE_ERRORS => e
239
+ RubyAstGen::Logger.info "'#{relative_input_path}' - #{describe_error(e)}"
240
+ # Not a syntax error but the file still produced nothing: report it with the error class as
241
+ # the reason, since there is no parser diagnostic to point at.
242
+ record_failure(state, file_path, relative_input_path,
243
+ {message: describe_error(e), line: nil, column: nil, diagnostic_reason: e.class.to_s})
244
+ end
245
+ end
246
+
247
+ # The stdlib generator, with a depth-safe fallback.
248
+ #
249
+ # max_nesting must exceed what a tree truncated at max_depth can reach: the default of 100 fails
250
+ # at an AST depth of ~49 and the resulting JSON::NestingError used to drop the whole file (logged
251
+ # as a per-file problem, exit 0) even though the AST itself was fine.
252
+ #
253
+ # How deep JSON.generate can go is a property of the runtime rather than of the JSON: CRuby's C
254
+ # extension manages ~100_000 levels, but on TruffleRuby it runs through Sulong and a tree at the
255
+ # default cap of 250 AST levels exhausts a worker thread's stack — which cost the whole file,
256
+ # the one outcome the depth cap exists to prevent, and made the output depend on the interpreter.
257
+ # Rather than guess each runtime's ceiling, fall back to the iterative writer when the recursive
258
+ # one runs out of room: it is ~11x slower on a corpus of real files, so it earns its keep only
259
+ # for the pathological ones. NodeHandling.dump_json is byte-identical to JSON.generate.
260
+ def self.serialize(ast, max_depth)
261
+ JSON.generate(ast, max_nesting: NodeHandling.json_nesting_limit(max_depth))
262
+ rescue SystemStackError, JSON::NestingError
263
+ NodeHandling.dump_json(ast)
264
+ end
265
+
266
+ # Java errors arrive with an empty message (JRuby's java.lang.StackOverflowError has none), so
267
+ # the class name is the only thing that would tell the reader what happened.
268
+ def self.describe_error(error)
269
+ message = error.message.to_s
270
+ message.empty? ? error.class.to_s : message
271
+ end
272
+
273
+ def self.process_directory(dir_path, output_dir, exclude_regex, max_threads = CLI::DEFAULT_THREADS,
274
+ parser_target: nil, max_depth: NodeHandling::MAX_NESTING_DEPTH, state: nil)
275
+ threads = []
276
+ queue = Queue.new
277
+
278
+ # FNM_DOTMATCH: without it Dir.glob skips dotfiles and dot-directories entirely, so `.ci/*.rb`
279
+ # and `.irbrc` were never parsed (plan 03 §5). SKIPPED_DIRECTORIES keeps that from pulling in
280
+ # tool caches — a repo with `bundle config path .bundle` would otherwise hand us every
281
+ # vendored gem, which the default exclusion regex does not cover.
282
+ Dir.glob("#{dir_path}/**/*", File::FNM_DOTMATCH).each do |path|
283
+ next unless File.file?(path)
284
+ relative_dir = path.sub("#{dir_path}/", '')
285
+ if skipped_directory?(relative_dir)
286
+ count_state(state, :excluded)
287
+ next
288
+ end
289
+ unless ruby_file?(path)
290
+ count_state(state, :skipped_nonruby)
291
+ next
292
+ end
293
+ if exclude_regex.match?(relative_dir)
294
+ count_state(state, :excluded)
295
+ next
296
+ end
297
+
298
+ queue << path
299
+ end
300
+
301
+ max_threads.times do
302
+ threads << Thread.new do
303
+ until queue.empty?
304
+ begin
305
+ path = queue.pop(true) rescue nil # Non-blocking pop
306
+ next unless path
307
+
308
+ relative_path = path.sub(dir_path, '')
309
+ output_subdir = File.join(output_dir, File.dirname(relative_path))
310
+ FileUtils.mkdir_p(output_subdir)
311
+
312
+ process_file(path, output_subdir, exclude_regex, dir_path, parser_target: parser_target,
313
+ max_depth: max_depth, state: state)
314
+ rescue *PER_FILE_ERRORS => e
315
+ # Raised here rather than inside process_file's own rescue, this would kill the worker
316
+ # and silently abandon the rest of its queue — join does not re-raise (plan 03 §7).
317
+ RubyAstGen::Logger.info "Error processing #{path}: #{describe_error(e)}"
318
+ end
319
+ end
320
+ end
321
+ end
322
+
323
+ threads.each(&:join)
324
+ end
325
+
326
+ # All state mutations from worker threads funnel through here (and .record_failure), guarded by
327
+ # the run's mutex.
328
+ def self.count_state(state, key)
329
+ return unless state
330
+
331
+ state[:mutex].synchronize { state[key] += 1 }
332
+ end
333
+
334
+ def self.record_failure(state, file_path, relative_input_path, parse_error)
335
+ return unless state
336
+
337
+ state[:mutex].synchronize do
338
+ state[:failed] += 1
339
+ state[:parse_errors] << {file_path: file_path, rel_file_path: relative_input_path,
340
+ parse_error: parse_error}
341
+ end
342
+ end
343
+
344
+ # The run manifest (plan 03 §4): one JSON object naming what this run saw and what it did, so a
345
+ # consumer can tell "parsed nothing" apart from "was never run".
346
+ def self.manifest_for(input_path, output_dir, state, threads:, max_depth:, parser_target:)
347
+ {
348
+ input: input_path,
349
+ output: output_dir,
350
+ ruby_version: RUBY_VERSION,
351
+ # The *selected* backend, so --parser-target is reflected here rather than the newest
352
+ # grammar available. A file whose retry swapped the grammar records its own backend in its
353
+ # own JSON; this key describes the run's configuration.
354
+ parser_backend: parser_for_current_ruby(log: false, parser_target: parser_target).to_s,
355
+ generator_version: VERSION,
356
+ generated_at: Time.now.utc.iso8601,
357
+ files_parsed: state[:parsed],
358
+ files_failed: state[:failed],
359
+ files_skipped_nonruby: state[:skipped_nonruby],
360
+ files_excluded: state[:excluded],
361
+ truncated_files: state[:truncated_files],
362
+ threads: threads,
363
+ max_depth: max_depth,
364
+ parser_target: parser_target&.to_s
365
+ }
366
+ end
367
+
368
+ # Writes the two side-records into the output directory: the per-failure diagnostics JSONL (only
369
+ # when at least one file failed — a stale file from a previous run into the same directory is
370
+ # removed, so the record always describes *this* run) and the manifest. A write failure here is
371
+ # logged and otherwise ignored: the AST output is already on disk, and losing the summary must
372
+ # not turn a finished run into a failed one. PER_FILE_ERRORS rather than StandardError because
373
+ # serialization here runs out of stack the same way per-file serialization can.
374
+ def self.write_run_records(output_dir, state, manifest)
375
+ diagnostics_path = File.join(output_dir, DIAGNOSTICS_FILENAME)
376
+ if state[:parse_errors].any?
377
+ File.write(diagnostics_path, state[:parse_errors].map { |record| JSON.generate(record) }.join("\n") + "\n")
378
+ elsif File.exist?(diagnostics_path)
379
+ File.delete(diagnostics_path)
380
+ end
381
+ File.write(File.join(output_dir, MANIFEST_FILENAME), JSON.generate(manifest) + "\n")
382
+ rescue *PER_FILE_ERRORS => e
383
+ Logger.error "Failed to write #{DIAGNOSTICS_FILENAME}/#{MANIFEST_FILENAME}: #{describe_error(e)}"
384
+ end
385
+
386
+ PARSER_CACHE_MUTEX = Mutex.new
387
+ private_constant :PARSER_CACHE_MUTEX
388
+
389
+ # Resolves the parser class for a run. Parsing does not depend on the running Ruby VM, so the
390
+ # grammar is picked by capability rather than by RUBY_VERSION: the newest grammar available by
391
+ # default, or the one matching +parser_target+ when a target is requested. Memoized per target
392
+ # because worker threads resolve this once per file.
393
+ def self.parser_for_current_ruby(log: true, parser_target: nil)
394
+ require 'parser/source/buffer'
395
+
396
+ key = parser_target.nil? ? :newest : version_pair(parser_target)
397
+ PARSER_CACHE_MUTEX.synchronize do
398
+ cached = parser_cache[key]
399
+ next cached if cached
400
+
401
+ parser = key == :newest ? newest_available_parser : parser_for_target(key, log: log)
402
+ RubyAstGen::Logger.debug "Using parser: #{parser}" if log
403
+ parser_cache[key] = parser
404
+ end
405
+ end
406
+
407
+ def self.parser_cache
408
+ @parser_cache ||= {}
409
+ end
410
+
411
+ # Drops the memoized parser selections; only needed by tests that flip capabilities.
412
+ def self.reset_parser_cache!
413
+ PARSER_CACHE_MUTEX.synchronize { @parser_cache = {} }
414
+ end
415
+
416
+ # Grammar selection for an explicit target, in order of fidelity:
417
+ # 1. the prism translation grammar for exactly that version
418
+ # 2. the `parser` gem grammar for exactly that version (covers 1.8 - 3.4, which prism's
419
+ # translation layer does not expose)
420
+ # 3. the closest older prism grammar
421
+ # 4. the newest grammar available
422
+ def self.parser_for_target(target, log: true)
423
+ exact_prism = prism_translation_parser_named("Parser#{target[0]}#{target[1]}")
424
+ return exact_prism if exact_prism
425
+
426
+ exact_parser_gem = parser_gem_grammar_for(target)
427
+ if exact_parser_gem
428
+ RubyAstGen::Logger.debug "No prism grammar for Ruby #{target.join('.')}; using #{exact_parser_gem}" if log
429
+ return exact_parser_gem
430
+ end
431
+
432
+ fallback = closest_prism_translation_parser(target) || newest_available_parser
433
+ RubyAstGen::Logger.info "No grammar for Ruby #{target.join('.')}; using #{fallback}" if log
434
+ fallback
435
+ end
436
+
437
+ # Newest grammar the installed gems can translate; the `parser` gem's runtime-version grammar
438
+ # is the last resort, for runtimes without prism.
439
+ def self.newest_available_parser
440
+ newest_prism_translation_parser || current_ruby_parser
441
+ end
442
+
443
+ def self.current_ruby_parser
444
+ require 'parser/current'
445
+ ::Parser::CurrentRuby
446
+ end
447
+
448
+ def self.prism_available?
449
+ return @prism_available unless @prism_available.nil?
450
+
451
+ @prism_available =
452
+ begin
453
+ require 'prism'
454
+ require 'prism/translation'
455
+ true
456
+ rescue LoadError
457
+ false
458
+ end
459
+ end
460
+
461
+ # The Ruby grammar level the selected parser implements, which is what decides whether a
462
+ # given syntax parses — not RUBY_VERSION.
463
+ def self.parser_grammar_version(parser = parser_for_current_ruby(log: false))
464
+ match = parser.to_s.match(/(\d)(\d+)\z/)
465
+ return Gem::Version.new(RUBY_VERSION) unless match
466
+
467
+ Gem::Version.new("#{match[1]}.#{match[2]}")
468
+ end
469
+
470
+ # `Gem.loaded_specs` is empty when the libraries come from a `--standalone` bundle instead of
471
+ # being activated through RubyGems — which is how atom-parsetools ships this tool, so
472
+ # `--parser-info` used to report "unavailable" for the very gem it was parsing with. The loaded
473
+ # library's own VERSION constant is the fallback; nil (reported as "unavailable") now means the
474
+ # library really is not loaded.
475
+ def self.library_version(gem_name, version_constant)
476
+ spec_version = Gem.loaded_specs[gem_name]&.version&.to_s
477
+ return spec_version if spec_version
478
+ return nil unless Object.const_defined?(version_constant)
479
+
480
+ Object.const_get(version_constant).to_s
481
+ end
482
+
483
+ def self.parser_info(parser_target: nil)
484
+ parser = parser_for_current_ruby(log: false, parser_target: parser_target)
485
+
486
+ {
487
+ ruby_engine: defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby",
488
+ ruby_version: RUBY_VERSION,
489
+ ruby_platform: RUBY_PLATFORM,
490
+ parser_target: parser_target&.to_s,
491
+ parser_backend: parser.to_s,
492
+ grammar_version: parser_grammar_version(parser).to_s,
493
+ parser_gem_version: library_version("parser", "Parser::VERSION"),
494
+ prism_gem_version: library_version("prism", "Prism::VERSION"),
495
+ prism_translation_parsers: prism_translation_parser_names
496
+ }
497
+ end
498
+
499
+ def self.parser_info_text(parser_target: nil)
500
+ info = parser_info(parser_target: parser_target)
501
+ [
502
+ "Ruby engine: #{info[:ruby_engine]}",
503
+ "Ruby version: #{info[:ruby_version]}",
504
+ "Ruby platform: #{info[:ruby_platform]}",
505
+ "Parser target: #{info[:parser_target] || 'newest available'}",
506
+ "Parser backend: #{info[:parser_backend]}",
507
+ "Grammar version: #{info[:grammar_version]}",
508
+ "Parser gem: #{info[:parser_gem_version] || 'unavailable'}",
509
+ "Prism gem: #{info[:prism_gem_version] || 'unavailable'}",
510
+ "Prism translation parsers: #{info[:prism_translation_parsers].empty? ? 'none' : info[:prism_translation_parsers].join(', ')}"
511
+ ].join("\n")
512
+ end
513
+
514
+ # [major, minor] for anything version-ish; a missing minor is treated as 0 ("4" -> [4, 0]).
515
+ def self.version_pair(version)
516
+ version = Gem::Version.new(version.to_s) unless version.is_a?(Gem::Version)
517
+ [version.segments[0].to_i, (version.segments[1] || 0).to_i]
518
+ end
519
+
520
+ def self.prism_translation_parser_named(name)
521
+ return nil unless prism_available?
522
+ return nil unless ::Prism::Translation.const_defined?(name, false)
523
+
524
+ ::Prism::Translation.const_get(name, false)
525
+ end
526
+
527
+ def self.parser_gem_grammar_for(target)
528
+ require "parser/ruby#{target[0]}#{target[1]}"
529
+ ::Parser.const_get("Ruby#{target[0]}#{target[1]}", false)
530
+ rescue LoadError, NameError
531
+ nil
532
+ end
533
+
534
+ # Highest grammar that is not newer than the target, else nil.
535
+ def self.closest_prism_translation_parser(target)
536
+ candidates = prism_translation_parser_candidates
537
+ not_newer = candidates.select { |major, minor, _parser| ([major, minor] <=> target) <= 0 }
538
+ (not_newer.max_by { |major, minor, _parser| [major, minor] } ||
539
+ candidates.max_by { |major, minor, _parser| [major, minor] })&.last
540
+ end
541
+
542
+ def self.prism_translation_parser_candidates
543
+ return [] unless prism_available?
544
+
545
+ ::Prism::Translation.constants.filter_map do |const_name|
546
+ match = const_name.to_s.match(/\AParser(\d)(\d+)\z/)
547
+ next unless match
548
+
549
+ [match[1].to_i, match[2].to_i, ::Prism::Translation.const_get(const_name, false)]
550
+ end
551
+ end
552
+
553
+ def self.newest_prism_translation_parser
554
+ prism_translation_parser_candidates.max_by { |major, minor, _parser| [major, minor] }&.last
555
+ end
556
+
557
+ def self.prism_translation_parser_names
558
+ return [] unless prism_available?
559
+
560
+ ::Prism::Translation.constants.grep(/\AParser\d+\z/).map(&:to_s).sort
561
+ end
562
+
563
+ # Parses one file into the JSON hash to emit. +result+, when given, receives the per-file
564
+ # outcome for the run manifest: :parsed (JSON emitted), :empty (clean parse of a comments-only
565
+ # file, which produces no JSON but did not fail) or :failed (+parse_error+ carrying the
566
+ # diagnosis that lands in the diagnostics JSONL).
567
+ def self.parse_file(file_path, relative_input_path, parser_target: nil, max_depth: NodeHandling::MAX_NESTING_DEPTH,
568
+ result: nil)
569
+ parser_class = parser_for_current_ruby(parser_target: parser_target)
570
+ buffer, source_scrubbed = read_source_buffer(file_path)
571
+ ast, comments, parser_class, diagnostics, syntax_error = parse_with_retry(parser_class, buffer, file_path)
572
+ if syntax_error
573
+ result&.merge!(outcome: :failed, parse_error: parse_error_record(syntax_error))
574
+ return nil
575
+ end
576
+ unless ast
577
+ result&.merge!(outcome: :empty)
578
+ return nil
579
+ end
580
+ result&.merge!(outcome: :parsed)
581
+
582
+ # The truncation count is per-file state threaded through the traversal: module state would
583
+ # race across the 10 worker threads and misattribute truncations between files.
584
+ state = {truncated: 0, first_type: nil}
585
+ json_ast = NodeHandling::ast_to_json(ast, buffer.source, file_path: relative_input_path,
586
+ max_depth: max_depth, state: state)
587
+ # Assigned before the UTF-8 walk so comment text from oddly-encoded sources is normalized
588
+ # like every other payload. The comments come from the same parse as the AST, so a scrubbed
589
+ # file's report matches what was actually parsed.
590
+ json_ast[:magic_comments] = magic_comments(comments, ast)
591
+ payloads_scrubbed = force_utf8_payloads!(json_ast)
592
+ json_ast[:file_path] = file_path
593
+ json_ast[:rel_file_path] = relative_input_path
594
+ json_ast[:generator_version] = VERSION
595
+ json_ast[:parser_backend] = parser_class.to_s
596
+ json_ast[:ruby_version] = RUBY_VERSION
597
+ json_ast[:truncated_nodes] = state[:truncated]
598
+ json_ast[:encoding_scrubbed] = true if source_scrubbed || payloads_scrubbed
599
+ # Prism's error-tolerant warnings as data (plan 03 §3). The key is omitted when the parse was
600
+ # silent, like call_operator and heredoc — absence means "nothing to report".
601
+ records, truncated = diagnostics_records(diagnostics)
602
+ if records.any?
603
+ json_ast[:diagnostics] = records
604
+ json_ast[:diagnostics_truncated] = true if truncated
605
+ end
606
+ if state[:truncated] > 0
607
+ # One warning per file: the count and the node types live in the JSON, so a line per
608
+ # truncated node is noise.
609
+ Logger.warn "#{relative_input_path}: truncated #{state[:truncated]} node(s) at max depth " \
610
+ "#{max_depth} (first: #{state[:first_type]})"
611
+ end
612
+ json_ast
613
+ end
614
+
615
+ # Reads the file as bytes and lets a parser source buffer decode it the way Ruby does, so a
616
+ # `# coding:` magic comment (or BOM) is honoured instead of everything being force-read as
617
+ # UTF-8. Undecodable content is degraded rather than dropped, in three tiers:
618
+ #
619
+ # 1. decode faithfully (magic comment / BOM respected);
620
+ # 2. bytes invalid under the declared encoding (EncodingError) -> decode a scrubbed copy;
621
+ # 3. encoding name unusable (Parser::UnknownEncodingInMagicComment, an ArgumentError) ->
622
+ # skip detection altogether and take the scrubbed bytes as UTF-8.
623
+ #
624
+ # The second return value marks tiers 2 and 3 as degraded. I/O errors are not caught here:
625
+ # an unreadable file is the caller's problem to log, and swallowing them would report a
626
+ # missing file as an encoding problem.
627
+ def self.read_source_buffer(file_path)
628
+ raw = File.binread(file_path)
629
+
630
+ begin
631
+ [decoded_source_buffer(file_path, raw), false]
632
+ rescue EncodingError, ArgumentError
633
+ begin
634
+ [decoded_source_buffer(file_path, scrub_to_utf8(raw)), true]
635
+ rescue EncodingError, ArgumentError
636
+ [undecoded_source_buffer(file_path, scrub_to_utf8(raw)), true]
637
+ end
638
+ end
639
+ end
640
+
641
+ # Buffers built with +source=+ run the parser gem's encoding detection. The bytes are tagged
642
+ # UTF-8 first so that a file *without* a magic comment keeps the encoding (and therefore the
643
+ # character offsets) of the String this replaced; detection re-encodes from there when the
644
+ # file says otherwise.
645
+ def self.decoded_source_buffer(file_path, bytes)
646
+ Parser::Source::Buffer.new(file_path, source: bytes.dup.force_encoding(Encoding::UTF_8))
647
+ end
648
+
649
+ # +raw_source=+ skips detection entirely, which is the only way to read a file whose declared
650
+ # encoding cannot be resolved. It still normalises CRLF, so offsets stay consistent.
651
+ def self.undecoded_source_buffer(file_path, utf8_source)
652
+ buffer = Parser::Source::Buffer.new(file_path)
653
+ buffer.raw_source = utf8_source
654
+ buffer
655
+ end
656
+
657
+ def self.scrub_to_utf8(bytes)
658
+ bytes.dup.force_encoding(Encoding::UTF_8).scrub
659
+ end
660
+
661
+ # Rewrites every String/Symbol payload in the emitted structure as valid UTF-8 so that
662
+ # JSON.generate cannot fail, and reports whether anything had to be replaced. Needed even
663
+ # when the source decoded cleanly: a `# coding: ASCII-8BIT` file is decoded faithfully (any
664
+ # byte is valid in that encoding) and yields binary node values and code snippets, which
665
+ # JSON.generate rejects. Top-level path keys are assigned after this walk, so file_path stays
666
+ # byte-for-byte what the caller passed in.
667
+ def self.force_utf8_payloads!(json_ast)
668
+ state = { scrubbed: false }
669
+ sanitize_utf8!(json_ast, state)
670
+ state[:scrubbed]
671
+ end
672
+
673
+ # Returns the sanitized value so containers can re-assign it. The scrub flag is accumulated
674
+ # in +state+ rather than folded out of the return values: combining them with `||=` would
675
+ # short-circuit the recursive call and silently skip whole subtrees once one string had been
676
+ # replaced (which shipped once, emitting JSON that still failed to serialize).
677
+ # Walks the built JSON with an explicit stack rather than by recursion. The tree is as deep as
678
+ # the AST (up to --max-depth), and recursion here cost ~2 frames per level: on runtimes with
679
+ # smaller thread stacks (JRuby, TruffleRuby, Windows CRuby, and any worker thread) a deeply
680
+ # nested file raised SystemStackError *after* the AST walk had already truncated correctly, so
681
+ # the file was skipped by process_file — the one outcome the depth cap exists to prevent.
682
+ # Iterating has no depth limit at all, so the scrub is no longer a reason to lose a file.
683
+ def self.sanitize_utf8!(object, state)
684
+ root = [object]
685
+ pending = [root]
686
+
687
+ until pending.empty?
688
+ container = pending.pop
689
+ case container
690
+ when Hash
691
+ container.each do |key, value|
692
+ replacement, descend = sanitize_payload(value, state)
693
+ container[key] = replacement
694
+ pending << replacement if descend
695
+ end
696
+ when Array
697
+ container.each_index do |index|
698
+ replacement, descend = sanitize_payload(container[index], state)
699
+ container[index] = replacement
700
+ pending << replacement if descend
701
+ end
702
+ end
703
+ end
704
+
705
+ root.first
706
+ end
707
+
708
+ # Returns the value to store and whether it is a container the walk must descend into.
709
+ def self.sanitize_payload(value, state)
710
+ case value
711
+ when Hash, Array
712
+ [value, true]
713
+ when String
714
+ utf8, scrubbed = to_utf8(value)
715
+ state[:scrubbed] = true if scrubbed
716
+ [utf8, false]
717
+ when Symbol
718
+ utf8, scrubbed = to_utf8(value.to_s)
719
+ state[:scrubbed] = true if scrubbed
720
+ [scrubbed ? utf8.to_sym : value, false]
721
+ else
722
+ [value, false]
723
+ end
724
+ end
725
+
726
+ def self.to_utf8(string)
727
+ return [string, false] if string.encoding == Encoding::UTF_8 && string.valid_encoding?
728
+ return [string.scrub, true] if string.encoding == Encoding::UTF_8
729
+
730
+ [string.encode(Encoding::UTF_8), false]
731
+ rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
732
+ [string.encode(Encoding::UTF_8, invalid: :replace, undef: :replace), true]
733
+ end
734
+
735
+ # Returns the parsed AST, the raw comment list, the parser that produced them, that parse's
736
+ # non-fatal diagnostics, and the syntax error when the file could not be parsed — so callers
737
+ # can record the effective backend, derive magic comments from the parse that actually
738
+ # succeeded, and report failures as data. Comments are collected in the same lexing pass as
739
+ # the AST (parse_with_comments), so there is no second parse.
740
+ #
741
+ # Each attempt parses with a fresh parser (see .instantiate_parser) and its own diagnostics
742
+ # sink: the retry's warnings must not be mixed with the failed grammar's view of the file, so
743
+ # only the attempt that produced the returned AST contributes.
744
+ def self.parse_with_retry(parser_class, buffer, file_path)
745
+ parser = instantiate_parser(parser_class, diagnostics = [])
746
+ ast, comments = parser.parse_with_comments(buffer)
747
+ [ast, comments, parser_class, diagnostics, nil]
748
+ rescue ::Parser::SyntaxError => e
749
+ retry_class = syntax_retry_parser(parser_class)
750
+ if retry_class.nil?
751
+ RubyAstGen::Logger.info "Failed to parse #{file_path}: #{e.message}"
752
+ return [nil, [], parser_class, [], e]
753
+ end
754
+
755
+ RubyAstGen::Logger.info "Retrying #{file_path} with #{retry_class} after: #{e.message.lines.first&.strip}"
756
+ begin
757
+ parser = instantiate_parser(retry_class, diagnostics = [])
758
+ ast, comments = parser.parse_with_comments(buffer)
759
+ [ast, comments, retry_class, diagnostics, nil]
760
+ rescue ::Parser::SyntaxError => retry_error
761
+ RubyAstGen::Logger.info "Failed to parse #{file_path}: #{retry_error.message}"
762
+ [nil, [], retry_class, [], retry_error]
763
+ end
764
+ end
765
+
766
+ # A fresh parser per parse call, with warnings routed into +sink+ instead of dropped. The
767
+ # memoized default_parser is shared by the worker threads, so installing a per-file consumer
768
+ # there would race one file's warnings into another's report. Error behaviour is unchanged:
769
+ # all_errors_are_fatal is what default_parser configures too. ignore_warnings=false is the one
770
+ # deliberate divergence — collecting warnings is the point (plan 03 §3).
771
+ def self.instantiate_parser(parser_class, sink)
772
+ parser = parser_class.new
773
+ parser.diagnostics.all_errors_are_fatal = true
774
+ parser.diagnostics.ignore_warnings = false
775
+ parser.diagnostics.consumer = ->(diagnostic) { sink << diagnostic }
776
+ parser
777
+ end
778
+
779
+ # The syntax error as data for the diagnostics JSONL: the rendered message, where it sits in
780
+ # the file, and the parser's own reason symbol ("unexpected_token").
781
+ def self.parse_error_record(error)
782
+ diagnostic = error.diagnostic
783
+ location = diagnostic.location
784
+ {
785
+ message: diagnostic.message,
786
+ line: location.line,
787
+ column: location.column,
788
+ diagnostic_reason: diagnostic.reason.to_s
789
+ }
790
+ rescue NoMethodError
791
+ # A SyntaxError without a diagnostic is not something the parser gem emits; degrade to the
792
+ # message rather than losing the failure.
793
+ {message: describe_error(error), line: nil, column: nil, diagnostic_reason: nil}
794
+ end
795
+
796
+ # Non-fatal diagnostics (prism warnings: ambiguous argument prefix, unused variable, ...) as
797
+ # data, capped at DIAGNOSTICS_CAP entries. The second return value reports whether the cap cut
798
+ # anything, so the file can carry diagnostics_truncated.
799
+ def self.diagnostics_records(diagnostics)
800
+ records = diagnostics.filter_map do |diagnostic|
801
+ location = diagnostic.location
802
+ {severity: diagnostic.level.to_s, message: diagnostic.message,
803
+ line: location&.line, column: location&.column}
804
+ end
805
+ truncated = records.size > DIAGNOSTICS_CAP
806
+ [truncated ? records.first(DIAGNOSTICS_CAP) : records, truncated]
807
+ end
808
+
809
+ # Magic comments ("# key: value" comments) reported as data, in source order. The rule runs
810
+ # over the comment tokens of the parse that produced the AST: the whole comment must be the
811
+ # `key: value` pair (a trailing word makes it an ordinary comment), whitespace around the colon
812
+ # is optional, and the comment must appear in the file prologue — before the first line of
813
+ # code. Only `#` comments qualify: =begin/=end documents come back as type :document, never
814
+ # magic comments, on both backends. Double-quoted values keep only their content; single quotes
815
+ # are left as written.
816
+ #
817
+ # The prologue restriction is where this deliberately diverges from
818
+ # `Prism.parse(...).magic_comments`, which is a lexical scan with no position rule and
819
+ # therefore reports RDoc and commented-out code as magic comments (`# https://memcached.org`
820
+ # → `{https => //memcached.org}`, `# See: docs`, `# attr_reader: foo`). Ruby only honours these
821
+ # comments before any code — a `frozen_string_literal` after code is ignored with a warning,
822
+ # and Sorbet's `typed:` sigil must precede code as well — so the prologue is what "magic"
823
+ # means. Measured over 7 104 gem files: the restriction drops 1 472 of 7 094 reported entries
824
+ # while keeping every one of the 5 430 real magic comments (frozen_string_literal, coding,
825
+ # encoding, typed).
826
+ MAGIC_COMMENT_PATTERN = /\A#\s*([A-Za-z0-9_-]+)\s*:\s*(\S+)[ \t]*\z/.freeze
827
+ private_constant :MAGIC_COMMENT_PATTERN
828
+
829
+ def self.magic_comments(comments, ast = nil)
830
+ # Comments at or after the first byte of code are ordinary comments. When the root node has
831
+ # no location to compare against (never seen in practice), position is not enforced rather
832
+ # than dropping everything.
833
+ first_code = ast&.location&.expression&.begin_pos
834
+
835
+ comments.filter_map do |comment|
836
+ next unless comment.is_a?(::Parser::Source::Comment) && comment.type == :inline
837
+ next if first_code && comment.loc.expression.begin_pos >= first_code
838
+
839
+ match = comment.text.match(MAGIC_COMMENT_PATTERN)
840
+ next unless match
841
+
842
+ value = match[2]
843
+ # Double-quoted values keep only their content, matching how prism reports
844
+ # `# coding: "utf-8"`.
845
+ value = value[1..-2] if value.length >= 2 && value.start_with?('"') && value.end_with?('"')
846
+ {name: match[1], value: value, line: comment.loc.line}
847
+ end
848
+ end
849
+
850
+ # A file rejected by an older grammar may still be valid newer Ruby, so retry once with the
851
+ # newest grammar available. Skipped when that is the grammar that just failed.
852
+ def self.syntax_retry_parser(failed_parser)
853
+ newest = newest_prism_translation_parser
854
+ newest if newest && newest != failed_parser
855
+ end
856
+
857
+ # Files parsed as Ruby: well-known basenames of Ruby DSL files, in any directory, plus
858
+ # Ruby-ish extensions. Both are matched case-insensitively (rake itself accepts `rakefile`),
859
+ # but basename matching is exact rather than prefix-based, so "Gemfile" is parsed while
860
+ # "Gemfile.lock" is not.
861
+ RUBY_FILE_BASENAMES = %w[
862
+ Rakefile Gemfile Capfile Thorfile Guardfile Berksfile Podfile Vagrantfile
863
+ Steepfile Puppetfile Dangerfile Fastfile Appfile Pluginfile Matchfile Scanfile
864
+ Snapfile Gymfile Deliverfile Brewfile
865
+ .irbrc .pryrc .simplecov
866
+ ].map(&:downcase).freeze
867
+ private_constant :RUBY_FILE_BASENAMES
868
+
869
+ # Dot-directories that hold tool state or vendored code rather than the project's own sources.
870
+ # Matched by exact path component, so a project directory named `.ci` is still scanned.
871
+ SKIPPED_DIRECTORIES = %w[.git .svn .hg .bundle .gem .cache .venv .tox .idea .vscode].freeze
872
+ private_constant :SKIPPED_DIRECTORIES
873
+
874
+ def self.skipped_directory?(relative_path)
875
+ relative_path.split(File::SEPARATOR).any? { |part| SKIPPED_DIRECTORIES.include?(part) }
876
+ end
877
+
878
+ RUBY_FILE_EXTENSIONS = %w[.rb .gemspec .rake .ru .rbi .thor .jbuilder .axlsx .rabl].freeze
879
+ private_constant :RUBY_FILE_EXTENSIONS
880
+
881
+ def self.ruby_file?(file_path)
882
+ basename = File.basename(file_path).downcase
883
+ return true if RUBY_FILE_BASENAMES.include?(basename)
884
+
885
+ RUBY_FILE_EXTENSIONS.include?(File.extname(basename))
886
+ end
887
+
888
+ end