tree_haver 5.0.5 → 7.1.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 (59) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/LICENSE.md +13 -0
  4. data/README.md +299 -660
  5. data/lib/tree_haver/backend_api.rb +78 -35
  6. data/lib/tree_haver/backend_context.rb +28 -0
  7. data/lib/tree_haver/backend_registry.rb +119 -382
  8. data/lib/tree_haver/backends/citrus.rb +55 -53
  9. data/lib/tree_haver/backends/ffi.rb +105 -101
  10. data/lib/tree_haver/backends/java.rb +92 -76
  11. data/lib/tree_haver/backends/mri.rb +44 -39
  12. data/lib/tree_haver/backends/parslet.rb +53 -48
  13. data/lib/tree_haver/backends/prism.rb +140 -43
  14. data/lib/tree_haver/backends/psych.rb +30 -26
  15. data/lib/tree_haver/backends/rust.rb +21 -17
  16. data/lib/tree_haver/backends/tslp.rb +274 -0
  17. data/lib/tree_haver/base/comment.rb +320 -0
  18. data/lib/tree_haver/base/language.rb +2 -2
  19. data/lib/tree_haver/base/node.rb +39 -31
  20. data/lib/tree_haver/base/parser.rb +4 -0
  21. data/lib/tree_haver/base/point.rb +3 -3
  22. data/lib/tree_haver/base/tree.rb +1 -1
  23. data/lib/tree_haver/citrus_grammar_finder.rb +21 -26
  24. data/lib/tree_haver/contracts.rb +1025 -0
  25. data/lib/tree_haver/grammar_finder.rb +125 -70
  26. data/lib/tree_haver/kaitai_backend.rb +30 -0
  27. data/lib/tree_haver/language.rb +36 -37
  28. data/lib/tree_haver/language_pack.rb +41 -0
  29. data/lib/tree_haver/language_registry.rb +34 -3
  30. data/lib/tree_haver/library_path_utils.rb +7 -7
  31. data/lib/tree_haver/node.rb +40 -31
  32. data/lib/tree_haver/parser.rb +44 -37
  33. data/lib/tree_haver/parslet_grammar_finder.rb +19 -26
  34. data/lib/tree_haver/path_validator.rb +39 -36
  35. data/lib/tree_haver/peg_backends.rb +76 -0
  36. data/lib/tree_haver/rspec/dependency_tags.rb +23 -1363
  37. data/lib/tree_haver/rspec.rb +1 -31
  38. data/lib/tree_haver/tree.rb +16 -7
  39. data/lib/tree_haver/version.rb +5 -14
  40. data/lib/tree_haver.rb +536 -1240
  41. data/sig/tree_haver.rbs +1 -229
  42. data.tar.gz.sig +0 -0
  43. metadata +154 -70
  44. metadata.gz.sig +0 -0
  45. data/CHANGELOG.md +0 -1393
  46. data/CITATION.cff +0 -20
  47. data/CODE_OF_CONDUCT.md +0 -134
  48. data/CONTRIBUTING.md +0 -359
  49. data/FUNDING.md +0 -74
  50. data/LICENSE.txt +0 -21
  51. data/REEK +0 -0
  52. data/RUBOCOP.md +0 -71
  53. data/SECURITY.md +0 -21
  54. data/lib/tree_haver/base.rb +0 -12
  55. data/lib/tree_haver/compat.rb +0 -43
  56. data/lib/tree_haver/rspec/testable_node.rb +0 -217
  57. data/sig/tree_haver/backends.rbs +0 -352
  58. data/sig/tree_haver/grammar_finder.rbs +0 -29
  59. data/sig/tree_haver/path_validator.rbs +0 -32
@@ -35,17 +35,17 @@ module TreeHaver
35
35
  # Default directories that are generally trusted for system libraries
36
36
  # These are searched by the dynamic linker anyway
37
37
  DEFAULT_TRUSTED_DIRECTORIES = [
38
- "/usr/lib",
39
- "/usr/lib64",
40
- "/usr/lib/x86_64-linux-gnu",
41
- "/usr/lib/aarch64-linux-gnu",
42
- "/usr/local/lib",
43
- "/opt/homebrew/lib",
44
- "/opt/local/lib",
38
+ '/usr/lib',
39
+ '/usr/lib64',
40
+ '/usr/lib/x86_64-linux-gnu',
41
+ '/usr/lib/aarch64-linux-gnu',
42
+ '/usr/local/lib',
43
+ '/opt/homebrew/lib',
44
+ '/opt/local/lib'
45
45
  ].freeze
46
46
 
47
47
  # Environment variable for adding trusted directories (comma-separated)
48
- TRUSTED_DIRS_ENV_VAR = "TREE_HAVER_TRUSTED_DIRS"
48
+ TRUSTED_DIRS_ENV_VAR = 'TREE_HAVER_TRUSTED_DIRS'
49
49
 
50
50
  # Maximum reasonable path length (prevents DoS via extremely long paths)
51
51
  MAX_PATH_LENGTH = 4096
@@ -60,30 +60,39 @@ module TreeHaver
60
60
  # Pattern for valid symbol names (C identifier format)
61
61
  VALID_SYMBOL_PATTERN = /\A[a-zA-Z_][a-zA-Z0-9_]*\z/
62
62
 
63
- @custom_trusted_directories = [] # rubocop:disable ThreadSafety/MutableClassInstanceVariable
63
+ @custom_trusted_directories = []
64
64
  @mutex = Mutex.new
65
65
 
66
66
  module_function
67
67
 
68
- # Get all trusted directories (default + custom + from ENV)
68
+ # Get all trusted directories (default + user-local + custom + from ENV)
69
69
  #
70
70
  # @return [Array<String>] list of all trusted directory prefixes
71
71
  def trusted_directories
72
72
  dirs = DEFAULT_TRUSTED_DIRECTORIES.dup
73
73
 
74
+ # Add user-local XDG directories (computed at call time from HOME)
75
+ begin
76
+ home = Dir.home
77
+ dirs << File.join(home, '.local', 'lib', 'tree-sitter')
78
+ dirs << File.join(home, '.local', 'lib')
79
+ rescue ArgumentError
80
+ # HOME not set — skip user-local dirs
81
+ end
82
+
74
83
  # Add custom registered directories
75
84
  @mutex.synchronize { dirs.concat(@custom_trusted_directories) }
76
85
 
77
86
  # Add directories from environment variable
78
- ENV[TRUSTED_DIRS_ENV_VAR]&.split(",")&.each do |dir|
87
+ ENV[TRUSTED_DIRS_ENV_VAR]&.split(',')&.each do |dir|
79
88
  expanded = File.expand_path(dir.strip)
80
- # :nocov:
89
+ # simplecov:disable
81
90
  # File.expand_path always returns absolute paths on Unix/macOS.
82
91
  # This guard exists for defensive programming on exotic platforms
83
92
  # where expand_path might behave differently, but cannot be tested
84
93
  # in standard CI environments.
85
- dirs << expanded if expanded.start_with?("/")
86
- # :nocov:
94
+ dirs << expanded if expanded.start_with?('/')
95
+ # simplecov:enable
87
96
  end
88
97
 
89
98
  dirs.uniq
@@ -106,15 +115,15 @@ module TreeHaver
106
115
  def add_trusted_directory(directory)
107
116
  expanded = File.expand_path(directory)
108
117
 
109
- # :nocov:
118
+ # simplecov:disable
110
119
  # File.expand_path always returns absolute paths on Unix/macOS.
111
120
  # This guard exists for defensive programming on exotic platforms
112
121
  # where expand_path might behave differently, but cannot be tested
113
122
  # in standard CI environments.
114
- unless expanded.start_with?("/")
123
+ unless expanded.start_with?('/')
115
124
  raise ArgumentError, "Trusted directory must be an absolute path: #{directory.inspect}"
116
125
  end
117
- # :nocov:
126
+ # simplecov:enable
118
127
 
119
128
  @mutex.synchronize do
120
129
  @custom_trusted_directories << expanded unless @custom_trusted_directories.include?(expanded)
@@ -176,11 +185,11 @@ module TreeHaver
176
185
  return false if path.include?("\0") # Null byte injection
177
186
 
178
187
  # Must be absolute path (prevents relative path traversal)
179
- return false unless path.start_with?("/") || windows_absolute_path?(path)
188
+ return false unless path.start_with?('/') || windows_absolute_path?(path)
180
189
 
181
190
  # Check for path traversal attempts
182
- return false if path.include?("/../") || path.end_with?("/..")
183
- return false if path.include?("/./") || path.end_with?("/.")
191
+ return false if path.include?('/../') || path.end_with?('/..')
192
+ return false if path.include?('/./') || path.end_with?('/.')
184
193
 
185
194
  # Validate extension
186
195
  # Allow versioned .so files like .so.0, .so.14, etc. (common on Linux)
@@ -191,9 +200,7 @@ module TreeHaver
191
200
  return false unless filename.match?(VALID_FILENAME_PATTERN)
192
201
 
193
202
  # Optionally require the path to be in a trusted directory
194
- if require_trusted_dir
195
- return false unless in_trusted_directory?(path)
196
- end
203
+ return false if require_trusted_dir && !in_trusted_directory?(path)
197
204
 
198
205
  true
199
206
  end
@@ -293,7 +300,7 @@ module TreeHaver
293
300
  def sanitize_language_name(name)
294
301
  return if name.nil?
295
302
 
296
- sanitized = name.to_s.downcase.gsub(/[^a-z0-9_]/, "")
303
+ sanitized = name.to_s.downcase.gsub(/[^a-z0-9_]/, '')
297
304
  return if sanitized.empty?
298
305
  return unless sanitized.match?(/\A[a-z]/)
299
306
 
@@ -308,24 +315,20 @@ module TreeHaver
308
315
  errors = []
309
316
 
310
317
  if path.nil? || path.empty?
311
- errors << "Path is nil or empty"
318
+ errors << 'Path is nil or empty'
312
319
  return errors
313
320
  end
314
321
 
315
322
  errors << "Path exceeds maximum length (#{MAX_PATH_LENGTH})" if path.length > MAX_PATH_LENGTH
316
- errors << "Path contains null byte" if path.include?("\0")
317
- errors << "Path is not absolute" unless path.start_with?("/") || windows_absolute_path?(path)
318
- errors << "Path contains traversal sequence (/../)" if path.include?("/../") || path.end_with?("/..")
319
- errors << "Path contains traversal sequence (/./)" if path.include?("/./") || path.end_with?("/.")
323
+ errors << 'Path contains null byte' if path.include?("\0")
324
+ errors << 'Path is not absolute' unless path.start_with?('/') || windows_absolute_path?(path)
325
+ errors << 'Path contains traversal sequence (/../)' if path.include?('/../') || path.end_with?('/..')
326
+ errors << 'Path contains traversal sequence (/./)' if path.include?('/./') || path.end_with?('/.')
320
327
 
321
- unless has_valid_extension?(path)
322
- errors << "Path does not have allowed extension (.so, .so.X, .dylib, .dll)"
323
- end
328
+ errors << 'Path does not have allowed extension (.so, .so.X, .dylib, .dll)' unless has_valid_extension?(path)
324
329
 
325
330
  filename = File.basename(path)
326
- unless filename.match?(VALID_FILENAME_PATTERN)
327
- errors << "Filename contains invalid characters"
328
- end
331
+ errors << 'Filename contains invalid characters' unless filename.match?(VALID_FILENAME_PATTERN)
329
332
 
330
333
  errors
331
334
  end
@@ -333,7 +336,7 @@ module TreeHaver
333
336
  # @api private
334
337
  def windows_absolute_path?(path)
335
338
  # Match Windows absolute paths like C:\path or D:/path
336
- path.match?(/\A[A-Za-z]:[\\\/]/)
339
+ path.match?(%r{\A[A-Za-z]:[\\/]})
337
340
  end
338
341
 
339
342
  # @api private
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeHaver
4
+ CITRUS_BACKEND = BackendReference.new(
5
+ id: 'citrus',
6
+ family: 'peg'
7
+ ).freeze
8
+
9
+ PARSLET_BACKEND = BackendReference.new(
10
+ id: 'parslet',
11
+ family: 'peg'
12
+ ).freeze
13
+
14
+ BackendRegistry.register(CITRUS_BACKEND)
15
+ BackendRegistry.register(PARSLET_BACKEND)
16
+
17
+ module_function
18
+
19
+ def peg_adapter_info(backend_ref)
20
+ AdapterInfo.new(
21
+ backend: backend_ref.id,
22
+ backend_ref: backend_ref,
23
+ supports_dialects: false,
24
+ supported_policies: []
25
+ )
26
+ end
27
+
28
+ def peg_feature_profile(backend_ref)
29
+ FeatureProfile.new(
30
+ backend: backend_ref.id,
31
+ backend_ref: backend_ref,
32
+ supports_dialects: false,
33
+ supported_policies: []
34
+ )
35
+ end
36
+
37
+ def parse_with_citrus(source, grammar_module:)
38
+ raw = grammar_module.parse(source)
39
+ if raw&.respond_to?(:captures)
40
+ {
41
+ ok: true,
42
+ backend_ref: CITRUS_BACKEND,
43
+ raw: raw,
44
+ diagnostics: []
45
+ }
46
+ else
47
+ {
48
+ ok: false,
49
+ backend_ref: CITRUS_BACKEND,
50
+ diagnostics: [{ severity: 'error', category: 'parse_error', message: 'Citrus parse failed.' }]
51
+ }
52
+ end
53
+ rescue StandardError => e
54
+ {
55
+ ok: false,
56
+ backend_ref: CITRUS_BACKEND,
57
+ diagnostics: [{ severity: 'error', category: 'parse_error', message: e.message }]
58
+ }
59
+ end
60
+
61
+ def parse_with_parslet(source, grammar_class:)
62
+ raw = grammar_class.new.parse(source)
63
+ {
64
+ ok: true,
65
+ backend_ref: PARSLET_BACKEND,
66
+ raw: raw,
67
+ diagnostics: []
68
+ }
69
+ rescue StandardError => e
70
+ {
71
+ ok: false,
72
+ backend_ref: PARSLET_BACKEND,
73
+ diagnostics: [{ severity: 'error', category: 'parse_error', message: e.message }]
74
+ }
75
+ end
76
+ end