rubydex 0.1.0.beta12-aarch64-linux

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 (109) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +23 -0
  3. data/README.md +125 -0
  4. data/THIRD_PARTY_LICENSES.html +4562 -0
  5. data/exe/rdx +47 -0
  6. data/ext/rubydex/declaration.c +453 -0
  7. data/ext/rubydex/declaration.h +23 -0
  8. data/ext/rubydex/definition.c +284 -0
  9. data/ext/rubydex/definition.h +28 -0
  10. data/ext/rubydex/diagnostic.c +6 -0
  11. data/ext/rubydex/diagnostic.h +11 -0
  12. data/ext/rubydex/document.c +97 -0
  13. data/ext/rubydex/document.h +10 -0
  14. data/ext/rubydex/extconf.rb +138 -0
  15. data/ext/rubydex/graph.c +681 -0
  16. data/ext/rubydex/graph.h +10 -0
  17. data/ext/rubydex/handle.h +44 -0
  18. data/ext/rubydex/location.c +22 -0
  19. data/ext/rubydex/location.h +15 -0
  20. data/ext/rubydex/reference.c +123 -0
  21. data/ext/rubydex/reference.h +15 -0
  22. data/ext/rubydex/rubydex.c +22 -0
  23. data/ext/rubydex/utils.c +108 -0
  24. data/ext/rubydex/utils.h +34 -0
  25. data/lib/rubydex/3.2/rubydex.so +0 -0
  26. data/lib/rubydex/3.3/rubydex.so +0 -0
  27. data/lib/rubydex/3.4/rubydex.so +0 -0
  28. data/lib/rubydex/4.0/rubydex.so +0 -0
  29. data/lib/rubydex/comment.rb +17 -0
  30. data/lib/rubydex/diagnostic.rb +21 -0
  31. data/lib/rubydex/failures.rb +15 -0
  32. data/lib/rubydex/graph.rb +98 -0
  33. data/lib/rubydex/keyword.rb +17 -0
  34. data/lib/rubydex/keyword_parameter.rb +13 -0
  35. data/lib/rubydex/librubydex_sys.so +0 -0
  36. data/lib/rubydex/location.rb +90 -0
  37. data/lib/rubydex/mixin.rb +22 -0
  38. data/lib/rubydex/version.rb +5 -0
  39. data/lib/rubydex.rb +23 -0
  40. data/rbi/rubydex.rbi +422 -0
  41. data/rust/Cargo.lock +1851 -0
  42. data/rust/Cargo.toml +29 -0
  43. data/rust/about.hbs +78 -0
  44. data/rust/about.toml +10 -0
  45. data/rust/rubydex/Cargo.toml +42 -0
  46. data/rust/rubydex/src/compile_assertions.rs +13 -0
  47. data/rust/rubydex/src/diagnostic.rs +110 -0
  48. data/rust/rubydex/src/errors.rs +28 -0
  49. data/rust/rubydex/src/indexing/local_graph.rs +224 -0
  50. data/rust/rubydex/src/indexing/rbs_indexer.rs +1551 -0
  51. data/rust/rubydex/src/indexing/ruby_indexer.rs +2329 -0
  52. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +4962 -0
  53. data/rust/rubydex/src/indexing.rs +210 -0
  54. data/rust/rubydex/src/integrity.rs +279 -0
  55. data/rust/rubydex/src/job_queue.rs +205 -0
  56. data/rust/rubydex/src/lib.rs +17 -0
  57. data/rust/rubydex/src/listing.rs +371 -0
  58. data/rust/rubydex/src/main.rs +160 -0
  59. data/rust/rubydex/src/model/built_in.rs +83 -0
  60. data/rust/rubydex/src/model/comment.rs +24 -0
  61. data/rust/rubydex/src/model/declaration.rs +671 -0
  62. data/rust/rubydex/src/model/definitions.rs +1682 -0
  63. data/rust/rubydex/src/model/document.rs +222 -0
  64. data/rust/rubydex/src/model/encoding.rs +22 -0
  65. data/rust/rubydex/src/model/graph.rs +3754 -0
  66. data/rust/rubydex/src/model/id.rs +110 -0
  67. data/rust/rubydex/src/model/identity_maps.rs +58 -0
  68. data/rust/rubydex/src/model/ids.rs +60 -0
  69. data/rust/rubydex/src/model/keywords.rs +256 -0
  70. data/rust/rubydex/src/model/name.rs +298 -0
  71. data/rust/rubydex/src/model/references.rs +111 -0
  72. data/rust/rubydex/src/model/string_ref.rs +50 -0
  73. data/rust/rubydex/src/model/visibility.rs +41 -0
  74. data/rust/rubydex/src/model.rs +15 -0
  75. data/rust/rubydex/src/offset.rs +147 -0
  76. data/rust/rubydex/src/position.rs +6 -0
  77. data/rust/rubydex/src/query.rs +1841 -0
  78. data/rust/rubydex/src/resolution.rs +6517 -0
  79. data/rust/rubydex/src/stats/memory.rs +71 -0
  80. data/rust/rubydex/src/stats/orphan_report.rs +264 -0
  81. data/rust/rubydex/src/stats/timer.rs +127 -0
  82. data/rust/rubydex/src/stats.rs +11 -0
  83. data/rust/rubydex/src/test_utils/context.rs +226 -0
  84. data/rust/rubydex/src/test_utils/graph_test.rs +730 -0
  85. data/rust/rubydex/src/test_utils/local_graph_test.rs +602 -0
  86. data/rust/rubydex/src/test_utils.rs +52 -0
  87. data/rust/rubydex/src/visualization/dot.rs +192 -0
  88. data/rust/rubydex/src/visualization.rs +6 -0
  89. data/rust/rubydex/tests/cli.rs +185 -0
  90. data/rust/rubydex-mcp/Cargo.toml +28 -0
  91. data/rust/rubydex-mcp/src/main.rs +48 -0
  92. data/rust/rubydex-mcp/src/server.rs +1145 -0
  93. data/rust/rubydex-mcp/src/tools.rs +49 -0
  94. data/rust/rubydex-mcp/tests/mcp.rs +302 -0
  95. data/rust/rubydex-sys/Cargo.toml +20 -0
  96. data/rust/rubydex-sys/build.rs +14 -0
  97. data/rust/rubydex-sys/cbindgen.toml +12 -0
  98. data/rust/rubydex-sys/src/declaration_api.rs +485 -0
  99. data/rust/rubydex-sys/src/definition_api.rs +443 -0
  100. data/rust/rubydex-sys/src/diagnostic_api.rs +99 -0
  101. data/rust/rubydex-sys/src/document_api.rs +85 -0
  102. data/rust/rubydex-sys/src/graph_api.rs +948 -0
  103. data/rust/rubydex-sys/src/lib.rs +79 -0
  104. data/rust/rubydex-sys/src/location_api.rs +79 -0
  105. data/rust/rubydex-sys/src/name_api.rs +135 -0
  106. data/rust/rubydex-sys/src/reference_api.rs +267 -0
  107. data/rust/rubydex-sys/src/utils.rs +70 -0
  108. data/rust/rustfmt.toml +2 -0
  109. metadata +159 -0
@@ -0,0 +1,110 @@
1
+ use std::{marker::PhantomData, num::NonZeroU64, ops::Deref};
2
+ use xxhash_rust::xxh3;
3
+
4
+ /// Maps a u64 hash to a `NonZeroU64` by replacing 0 with `u64::MAX`.
5
+ /// The probability of a 64-bit hash being exactly 0 is 2^-64 (~5.4e-20),
6
+ /// and remapping 0 → MAX just means those two inputs collide — the same
7
+ /// risk as any other hash collision, which the system already handles.
8
+ const MAX_NONZERO: NonZeroU64 = NonZeroU64::new(u64::MAX).unwrap();
9
+
10
+ #[inline]
11
+ fn to_non_zero(value: u64) -> NonZeroU64 {
12
+ NonZeroU64::new(value).unwrap_or(MAX_NONZERO)
13
+ }
14
+
15
+ /// A deterministic type-safe ID representation.
16
+ ///
17
+ /// Uses `NonZeroU64` internally so that `Option<Id<T>>` is 8 bytes (same as `Id<T>`)
18
+ /// via niche optimization, instead of 16 bytes with a plain `u64`.
19
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
20
+ pub struct Id<T> {
21
+ value: NonZeroU64,
22
+ _marker: PhantomData<T>,
23
+ }
24
+
25
+ impl<T> Id<T> {
26
+ #[must_use]
27
+ pub fn new(value: u64) -> Self {
28
+ Self {
29
+ value: to_non_zero(value),
30
+ _marker: PhantomData,
31
+ }
32
+ }
33
+
34
+ /// Returns the underlying `u64` value.
35
+ #[must_use]
36
+ pub fn get(&self) -> u64 {
37
+ self.value.get()
38
+ }
39
+ }
40
+
41
+ impl<T> Deref for Id<T> {
42
+ type Target = u64;
43
+
44
+ fn deref(&self) -> &Self::Target {
45
+ // SAFETY: NonZeroU64 is #[repr(transparent)] over u64.
46
+ // Deref requires returning &u64, but NonZeroU64::get() returns by value,
47
+ // so the pointer cast is unavoidable here. Prefer Id::get() when a u64
48
+ // value (not a reference) is sufficient.
49
+ unsafe { &*std::ptr::from_ref(&self.value).cast::<u64>() }
50
+ }
51
+ }
52
+
53
+ impl<T> std::fmt::Display for Id<T> {
54
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55
+ write!(f, "{}", self.value)
56
+ }
57
+ }
58
+
59
+ impl<T> From<&str> for Id<T> {
60
+ fn from(value: &str) -> Self {
61
+ let hash = xxh3::xxh3_64(value.as_bytes());
62
+ Self::new(hash)
63
+ }
64
+ }
65
+
66
+ impl<T> From<&String> for Id<T> {
67
+ fn from(value: &String) -> Self {
68
+ let hash = xxh3::xxh3_64(value.as_bytes());
69
+ Self::new(hash)
70
+ }
71
+ }
72
+
73
+ #[cfg(test)]
74
+ mod tests {
75
+ use super::*;
76
+
77
+ #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
78
+ pub struct Marker;
79
+ pub type TestId = Id<Marker>;
80
+
81
+ #[test]
82
+ fn test_create_hash() {
83
+ // Same input should produce same hash (deterministic)
84
+ let id1 = TestId::from("test_input");
85
+ let id2 = TestId::from("test_input");
86
+ assert_eq!(id1, id2);
87
+
88
+ // Different inputs should produce different hashes (unique)
89
+ let id3 = TestId::from("different_input");
90
+ assert_ne!(id1, id3);
91
+ }
92
+
93
+ #[test]
94
+ fn get_returns_value() {
95
+ let id = TestId::new(123);
96
+ assert_eq!(id.get(), 123);
97
+ }
98
+
99
+ #[test]
100
+ fn optional_id_is_still_8_bytes() {
101
+ assert_eq!(std::mem::size_of::<Option<TestId>>(), 8);
102
+ assert_eq!(std::mem::size_of::<TestId>(), 8);
103
+ }
104
+
105
+ #[test]
106
+ fn zero_hash_maps_to_nonzero() {
107
+ let id = TestId::new(0);
108
+ assert_eq!(id.get(), u64::MAX);
109
+ }
110
+ }
@@ -0,0 +1,58 @@
1
+ //! This module contains identity maps that use externally hashed IDs as keys. They are used to avoid hashing the same
2
+ //! value twice, simply using the given key directly
3
+
4
+ use std::{
5
+ collections::{HashMap, HashSet},
6
+ hash::{BuildHasher, Hasher},
7
+ };
8
+
9
+ #[derive(Default)]
10
+ pub struct IdentityHasher {
11
+ hash: u64,
12
+ }
13
+
14
+ impl Hasher for IdentityHasher {
15
+ fn write(&mut self, _bytes: &[u8]) {
16
+ unreachable!("IdentityHasher only supports write_u64");
17
+ }
18
+
19
+ fn write_u32(&mut self, i: u32) {
20
+ self.hash = u64::from(i);
21
+ }
22
+
23
+ fn write_u64(&mut self, i: u64) {
24
+ self.hash = i;
25
+ }
26
+
27
+ fn finish(&self) -> u64 {
28
+ self.hash
29
+ }
30
+ }
31
+
32
+ #[derive(Default)]
33
+ pub struct IdentityHashBuilder;
34
+
35
+ impl BuildHasher for IdentityHashBuilder {
36
+ type Hasher = IdentityHasher;
37
+
38
+ fn build_hasher(&self) -> Self::Hasher {
39
+ IdentityHasher::default()
40
+ }
41
+ }
42
+
43
+ pub type IdentityHashMap<K, V> = HashMap<K, V, IdentityHashBuilder>;
44
+ pub type IdentityHashSet<T> = HashSet<T, IdentityHashBuilder>;
45
+
46
+ #[cfg(test)]
47
+ mod tests {
48
+ use super::*;
49
+
50
+ #[test]
51
+ fn identity_hasher_uses_value_as_is() {
52
+ let builder = IdentityHashBuilder;
53
+ let mut hasher = builder.build_hasher();
54
+
55
+ hasher.write_u64(42);
56
+ assert_eq!(hasher.finish(), 42);
57
+ }
58
+ }
@@ -0,0 +1,60 @@
1
+ use crate::{assert_mem_size, model::id::Id};
2
+
3
+ #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
4
+ pub struct DeclarationMarker;
5
+ /// `DeclarationId` represents the ID of a fully qualified name. For example, `Foo::Bar` or `Foo#my_method`
6
+ pub type DeclarationId = Id<DeclarationMarker>;
7
+
8
+ #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
9
+ pub struct DefinitionMarker;
10
+
11
+ // DefinitionId represents the ID of a definition found in a specific file
12
+ pub type DefinitionId = Id<DefinitionMarker>;
13
+ assert_mem_size!(DefinitionId, 8);
14
+
15
+ #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
16
+ pub struct UriMarker;
17
+ // UriId represents the ID of a URI, which is the unique identifier for a document
18
+ pub type UriId = Id<UriMarker>;
19
+ assert_mem_size!(UriId, 8);
20
+
21
+ #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
22
+ pub struct StringMarker;
23
+ /// `StringId` represents an ID for an interned string value
24
+ pub type StringId = Id<StringMarker>;
25
+ assert_mem_size!(StringId, 8);
26
+
27
+ #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
28
+ pub struct NameMarker;
29
+ /// `NameId` represents an ID for any constant name that we find as part of a reference or definition
30
+ pub type NameId = Id<NameMarker>;
31
+ assert_mem_size!(NameId, 8);
32
+
33
+ // Reference IDs
34
+ //
35
+ // This section is for specialized IDs for each type of declaration reference
36
+
37
+ #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
38
+ pub struct ConstantMarker;
39
+ pub type ConstantReferenceId = Id<ConstantMarker>;
40
+ assert_mem_size!(ConstantReferenceId, 8);
41
+
42
+ #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
43
+ pub struct MethodMarker;
44
+ pub type MethodReferenceId = Id<MethodMarker>;
45
+ assert_mem_size!(MethodReferenceId, 8);
46
+
47
+ #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
48
+ pub struct GlobalVariableMarker;
49
+ pub type GlobalVariableReferenceId = Id<GlobalVariableMarker>;
50
+ assert_mem_size!(GlobalVariableReferenceId, 8);
51
+
52
+ #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
53
+ pub struct ClassVariableMarker;
54
+ pub type ClassVariableReferenceId = Id<ClassVariableMarker>;
55
+ assert_mem_size!(ClassVariableReferenceId, 8);
56
+
57
+ #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
58
+ pub struct InstanceVariableMarker;
59
+ pub type InstanceVariableReferenceId = Id<InstanceVariableMarker>;
60
+ assert_mem_size!(InstanceVariableReferenceId, 8);
@@ -0,0 +1,256 @@
1
+ /// A Ruby keyword with its documentation.
2
+ #[derive(Debug)]
3
+ pub struct Keyword {
4
+ /// The keyword text as it appears in source (e.g., "yield", "defined?", "__FILE__")
5
+ name: &'static str,
6
+ /// Documentation string for hover display
7
+ documentation: &'static str,
8
+ }
9
+
10
+ impl Keyword {
11
+ #[must_use]
12
+ pub const fn name(&self) -> &'static str {
13
+ self.name
14
+ }
15
+
16
+ #[must_use]
17
+ pub const fn documentation(&self) -> &'static str {
18
+ self.documentation
19
+ }
20
+ }
21
+
22
+ /// Looks up a keyword by its exact name. Returns `None` if the name is not a keyword.
23
+ ///
24
+ /// Uses binary search on the sorted `KEYWORDS` array for O(log n) lookup.
25
+ #[must_use]
26
+ pub fn get(name: &str) -> Option<&'static Keyword> {
27
+ KEYWORDS
28
+ .binary_search_by_key(&name, |k| k.name)
29
+ .ok()
30
+ .map(|i| &KEYWORDS[i])
31
+ }
32
+
33
+ /// All Ruby keywords, sorted lexicographically by name for binary search.
34
+ pub static KEYWORDS: &[Keyword] = &[
35
+ Keyword {
36
+ name: "BEGIN",
37
+ documentation: "Registers a block of code to be executed before the program starts. Syntax: `BEGIN { ... }`.",
38
+ },
39
+ Keyword {
40
+ name: "END",
41
+ documentation: "Registers a block of code to be executed after the program finishes. Syntax: `END { ... }`.",
42
+ },
43
+ Keyword {
44
+ name: "__ENCODING__",
45
+ documentation: "Returns the `Encoding` object representing the encoding of the current source file.",
46
+ },
47
+ Keyword {
48
+ name: "__FILE__",
49
+ documentation: "Returns the path of the current source file as a `String`.",
50
+ },
51
+ Keyword {
52
+ name: "__LINE__",
53
+ documentation: "Returns the current line number in the source file as an `Integer`.",
54
+ },
55
+ Keyword {
56
+ name: "alias",
57
+ documentation: "Creates an alias between two methods or global variables. Syntax: `alias new_name old_name`.",
58
+ },
59
+ Keyword {
60
+ name: "and",
61
+ documentation: "Low-precedence logical AND operator. Unlike `&&`, it has lower precedence than assignment.",
62
+ },
63
+ Keyword {
64
+ name: "begin",
65
+ documentation: "Opens an exception handling block. Can be followed by `rescue`, `else`, `ensure`, and closed with `end`.",
66
+ },
67
+ Keyword {
68
+ name: "break",
69
+ documentation: "Exits from a loop or block, optionally returning a value. Syntax: `break` or `break value`.",
70
+ },
71
+ Keyword {
72
+ name: "case",
73
+ documentation: "Starts a case expression for pattern matching. Used with `when` clauses and closed with `end`.",
74
+ },
75
+ Keyword {
76
+ name: "class",
77
+ documentation: "Defines a new class or opens an existing one. Syntax: `class Name < Superclass; end`.",
78
+ },
79
+ Keyword {
80
+ name: "def",
81
+ documentation: "Defines a method. Syntax: `def method_name(params); end`.",
82
+ },
83
+ Keyword {
84
+ name: "defined?",
85
+ documentation: "Returns a string describing the type of an expression, or `nil` if it is not defined. The argument is not evaluated.",
86
+ },
87
+ Keyword {
88
+ name: "do",
89
+ documentation: "Starts a block of code, typically following an iterator method call. Paired with `end`.",
90
+ },
91
+ Keyword {
92
+ name: "else",
93
+ documentation: "Provides an alternative branch in `if`, `unless`, `case`, or `begin/rescue` expressions.",
94
+ },
95
+ Keyword {
96
+ name: "elsif",
97
+ documentation: "Provides an additional conditional branch within an `if` expression.",
98
+ },
99
+ Keyword {
100
+ name: "end",
101
+ documentation: "Closes a `class`, `module`, `def`, `if`, `unless`, `case`, `while`, `until`, `for`, `begin`, or `do` block.",
102
+ },
103
+ Keyword {
104
+ name: "ensure",
105
+ documentation: "Defines a block of code within `begin`/`def` that always runs, whether an exception was raised or not.",
106
+ },
107
+ Keyword {
108
+ name: "false",
109
+ documentation: "The singleton instance of `FalseClass`. One of two falsy values in Ruby, along with `nil`.",
110
+ },
111
+ Keyword {
112
+ name: "for",
113
+ documentation: "Iterates over a collection. Syntax: `for variable in collection; end`. Prefer `.each` in idiomatic Ruby.",
114
+ },
115
+ Keyword {
116
+ name: "if",
117
+ documentation: "Conditional branch. Can be used as a statement (`if cond; end`) or a modifier (`expr if cond`).",
118
+ },
119
+ Keyword {
120
+ name: "in",
121
+ documentation: "Used with `for` loops (`for x in collection`) and pattern matching (`case value; in pattern`).",
122
+ },
123
+ Keyword {
124
+ name: "module",
125
+ documentation: "Defines a new module or opens an existing one. Modules provide namespacing and mixins via `include`/`extend`.",
126
+ },
127
+ Keyword {
128
+ name: "next",
129
+ documentation: "Skips to the next iteration of a loop or block, optionally returning a value. Syntax: `next` or `next value`.",
130
+ },
131
+ Keyword {
132
+ name: "nil",
133
+ documentation: "The singleton instance of `NilClass`. Represents the absence of a value. Falsy in boolean context.",
134
+ },
135
+ Keyword {
136
+ name: "not",
137
+ documentation: "Low-precedence logical NOT operator. Unlike `!`, it has lower precedence than most operators.",
138
+ },
139
+ Keyword {
140
+ name: "or",
141
+ documentation: "Low-precedence logical OR operator. Unlike `||`, it has lower precedence than assignment.",
142
+ },
143
+ Keyword {
144
+ name: "redo",
145
+ documentation: "Restarts the current iteration of a loop or block without re-evaluating the condition.",
146
+ },
147
+ Keyword {
148
+ name: "rescue",
149
+ documentation: "Catches exceptions in a `begin`/`def` block. Can also be used inline: `expr rescue default`.",
150
+ },
151
+ Keyword {
152
+ name: "retry",
153
+ documentation: "Re-executes the `begin` block from the start. Only valid inside a `rescue` clause.",
154
+ },
155
+ Keyword {
156
+ name: "return",
157
+ documentation: "Exits from the current method, optionally returning a value. Syntax: `return` or `return value`.",
158
+ },
159
+ Keyword {
160
+ name: "self",
161
+ documentation: "References the current object. Inside a method, it is the receiver. Inside a class/module body, it is the class/module itself.",
162
+ },
163
+ Keyword {
164
+ name: "super",
165
+ documentation: "Calls the next occurrence of the method in the ancestor chain. Forwards all arguments if called without parentheses.",
166
+ },
167
+ Keyword {
168
+ name: "then",
169
+ documentation: "Optional separator after the condition in `if`, `unless`, `when`, or `in` clauses.",
170
+ },
171
+ Keyword {
172
+ name: "true",
173
+ documentation: "The singleton instance of `TrueClass`. Truthy in boolean context.",
174
+ },
175
+ Keyword {
176
+ name: "undef",
177
+ documentation: "Removes a method definition from the current class. Syntax: `undef method_name`.",
178
+ },
179
+ Keyword {
180
+ name: "unless",
181
+ documentation: "Inverted conditional. Executes the body when the condition is falsy. Can be used as statement or modifier.",
182
+ },
183
+ Keyword {
184
+ name: "until",
185
+ documentation: "Loops while the condition is falsy. Can be used as a statement or modifier.",
186
+ },
187
+ Keyword {
188
+ name: "when",
189
+ documentation: "Defines a branch in a `case` expression. Syntax: `when value; ...`.",
190
+ },
191
+ Keyword {
192
+ name: "while",
193
+ documentation: "Loops while the condition is truthy. Can be used as a statement or modifier.",
194
+ },
195
+ Keyword {
196
+ name: "yield",
197
+ documentation: "Calls the block passed to the current method, passing optional arguments. Returns the block's return value.",
198
+ },
199
+ ];
200
+
201
+ #[cfg(test)]
202
+ mod tests {
203
+ use super::*;
204
+
205
+ #[test]
206
+ fn all_keywords_have_documentation() {
207
+ for keyword in KEYWORDS {
208
+ assert!(
209
+ !keyword.documentation().is_empty(),
210
+ "keyword '{}' has empty documentation",
211
+ keyword.name()
212
+ );
213
+ }
214
+ }
215
+
216
+ #[test]
217
+ fn get_returns_none_for_non_keywords() {
218
+ assert!(get("puts").is_none());
219
+ assert!(get("require").is_none());
220
+ assert!(get("").is_none());
221
+ assert!(get("Foo").is_none());
222
+ }
223
+
224
+ #[test]
225
+ fn get_returns_keyword_with_correct_data() {
226
+ let kw = get("yield").unwrap();
227
+ assert_eq!(kw.name(), "yield");
228
+ assert!(kw.documentation().contains("block"));
229
+
230
+ let kw = get("defined?").unwrap();
231
+ assert_eq!(kw.name(), "defined?");
232
+
233
+ let kw = get("__FILE__").unwrap();
234
+ assert_eq!(kw.name(), "__FILE__");
235
+ }
236
+
237
+ #[test]
238
+ fn keyword_names_are_unique() {
239
+ let mut seen = std::collections::HashSet::new();
240
+ for keyword in KEYWORDS {
241
+ assert!(seen.insert(keyword.name()), "duplicate keyword: '{}'", keyword.name());
242
+ }
243
+ }
244
+
245
+ #[test]
246
+ fn keywords_are_sorted() {
247
+ for window in KEYWORDS.windows(2) {
248
+ assert!(
249
+ window[0].name() < window[1].name(),
250
+ "KEYWORDS not sorted: '{}' should come before '{}'",
251
+ window[1].name(),
252
+ window[0].name()
253
+ );
254
+ }
255
+ }
256
+ }