rubydex 0.4.0-aarch64-linux → 0.4.2-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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -14
  3. data/THIRD_PARTY_LICENSES.html +13 -24
  4. data/ext/rubydex/definition.c +34 -0
  5. data/ext/rubydex/definition.h +3 -0
  6. data/ext/rubydex/diagnostic.c +2 -49
  7. data/ext/rubydex/extconf.rb +4 -2
  8. data/ext/rubydex/graph.c +59 -11
  9. data/lib/rubydex/3.2/rubydex.so +0 -0
  10. data/lib/rubydex/3.3/rubydex.so +0 -0
  11. data/lib/rubydex/3.4/rubydex.so +0 -0
  12. data/lib/rubydex/4.0/rubydex.so +0 -0
  13. data/lib/rubydex/cli/command/dead_code.rb +59 -0
  14. data/lib/rubydex/cli/command/lint/explain.rb +8 -6
  15. data/lib/rubydex/cli/command/lint.rb +10 -4
  16. data/lib/rubydex/librubydex_sys.so +0 -0
  17. data/lib/rubydex/linter/rule_loader.rb +1 -1
  18. data/lib/rubydex/mcp_server/tools/codebase_stats_tool.rb +1 -1
  19. data/lib/rubydex/mcp_server/tools/find_constant_references_tool.rb +4 -4
  20. data/lib/rubydex/mcp_server/tools/find_dead_code_candidates_tool.rb +33 -0
  21. data/lib/rubydex/mcp_server/tools/get_declaration_tool.rb +2 -2
  22. data/lib/rubydex/mcp_server/tools/get_descendants_tool.rb +4 -4
  23. data/lib/rubydex/mcp_server/tools/get_file_declarations_tool.rb +2 -2
  24. data/lib/rubydex/mcp_server/tools/search_declarations_tool.rb +6 -6
  25. data/lib/rubydex/mcp_server.rb +24 -12
  26. data/lib/rubydex/rules/dynamic_ancestor.rb +29 -0
  27. data/lib/rubydex/rules/dynamic_constant_reference.rb +25 -0
  28. data/lib/rubydex/rules/dynamic_singleton_definition.rb +30 -0
  29. data/lib/rubydex/rules/invalid_constant_visibility.rb +28 -0
  30. data/lib/rubydex/rules/invalid_method_visibility.rb +28 -0
  31. data/lib/rubydex/rules/parse_error.rb +25 -0
  32. data/lib/rubydex/rules/parse_warning.rb +28 -0
  33. data/lib/rubydex/rules/top_level_mixin_self.rb +27 -0
  34. data/lib/rubydex/rules/undefined_constant_visibility_target.rb +27 -0
  35. data/lib/rubydex/rules/undefined_method_visibility_target.rb +27 -0
  36. data/lib/rubydex/skill.rb +1 -0
  37. data/lib/rubydex/version.rb +1 -1
  38. data/lib/rubydex.rb +2 -0
  39. data/lib/rubydex_linter/rules/rule_structure.rb +19 -4
  40. data/rbi/rubydex.rbi +32 -130
  41. data/rust/Cargo.lock +17 -11
  42. data/rust/Cargo.toml +3 -0
  43. data/rust/rubydex/Cargo.toml +6 -2
  44. data/rust/rubydex/src/bin/generate_ruby_rules.rs +94 -0
  45. data/rust/rubydex/src/compile_assertions.rs +2 -0
  46. data/rust/rubydex/src/config.rs +1 -1
  47. data/rust/rubydex/src/diagnostic.rs +142 -51
  48. data/rust/rubydex/src/indexing/rbs_indexer.rs +4 -4
  49. data/rust/rubydex/src/indexing/ruby_indexer.rs +26 -18
  50. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +42 -2
  51. data/rust/rubydex/src/indexing.rs +6 -6
  52. data/rust/rubydex/src/listing.rs +12 -12
  53. data/rust/rubydex/src/model/built_in.rs +1 -1
  54. data/rust/rubydex/src/model/comment.rs +4 -4
  55. data/rust/rubydex/src/model/declaration.rs +25 -17
  56. data/rust/rubydex/src/model/document.rs +11 -11
  57. data/rust/rubydex/src/model/graph.rs +3 -3
  58. data/rust/rubydex/src/offset.rs +1 -13
  59. data/rust/rubydex/src/operation/ruby_builder.rs +29 -19
  60. data/rust/rubydex/src/query.rs +193 -2
  61. data/rust/rubydex/src/test_utils/graph_test.rs +2 -2
  62. data/rust/rubydex/src/test_utils/local_graph_test.rs +13 -6
  63. data/rust/rubydex-sys/Cargo.toml +2 -2
  64. data/rust/rubydex-sys/src/definition_api.rs +69 -2
  65. data/rust/rubydex-sys/src/diagnostic_api.rs +0 -35
  66. data/rust/rubydex-sys/src/graph_api.rs +193 -45
  67. data/rust/rubydex-sys/src/name_api.rs +219 -48
  68. metadata +199 -187
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0ae408bab2d715dbc24c9788310c8387d479578b43f3ed91a5613694da7c7f3
4
- data.tar.gz: f91df4834446b385e52a407f3a45dea75c3cbb1e01f843a7a0176a3441a9c952
3
+ metadata.gz: 6ef406f89b33ab38d3e0cc4423e9f651003144c22c55feebfc0b5750878ec1e3
4
+ data.tar.gz: 719fa68c55a21101cf13115a6089c23cb145073d4bd9d87e6730335742e71ba1
5
5
  SHA512:
6
- metadata.gz: 59f82f4466af541704a9a8967b022234130ae4c12be48f1a7b7f50b8a1d58bf63c0c42ae3e90d8ed6f6068bd69fab3f5e8a08ecb388398b530a500effb9e4b85
7
- data.tar.gz: 896414c62dcac91658b49031afe905b74f3ef1d68d07205ff292805f2b05cf50dee1155305d45e83c82a172a8ea24f550cb7e590aa5a649c56abaa6890992f75
6
+ metadata.gz: 0e4e82d8b4a75b1eac0cb42b43e1c053cf3088167b6c742ee04ff8a3de8f4e97a278ba1f24cb08cfc150ea2244de26e02c84ab3435f641b0899040c8e54a5272
7
+ data.tar.gz: 0ff32e86e8eb8e5f11745e796f8e1a3834080f489b084c93c5a3eab8260d5fb9353890de4367f0a71bf758eef0db98344982144d788f0b045aa8544a8d3ec478
data/README.md CHANGED
@@ -114,7 +114,7 @@ Thread safety comes from an `RwLock` on the Rust side, **not** from Ruby's
114
114
 
115
115
  All built-in tools are experimental. These tools can change without deprecation warnings.
116
116
 
117
- ### `rdx query`
117
+ ### Querying
118
118
 
119
119
  Rubydex exposes the indexed graph through a read-only subset of the
120
120
  [Cypher](https://opencypher.org/) query language. Only read clauses (`MATCH`,
@@ -153,27 +153,21 @@ puts result.render("json")
153
153
  puts Rubydex::Query.schema("table")
154
154
  ```
155
155
 
156
- ### `rdx lint`
156
+ ### Linter
157
157
 
158
- Put rule files under `rubydex_linter/rules`. Each rule must inherit from `Rubydex::Linter::CustomRule`.
158
+ See the [linter docs](docs/linter.md).
159
159
 
160
- Run the linter:
160
+ ### Dead code candidates
161
161
 
162
162
  ```bash
163
- bundle exec rdx lint
163
+ bundle exec rdx dead_code
164
164
  ```
165
165
 
166
- Rubydex uses the bundle root, or the current directory when no bundle is available.
166
+ Prints a list of potentially dead code candidates. The list is composed of all declarations for which the analysis found no references for in the codebase.
167
167
 
168
- Configure a rule in `rubydex.toml`:
168
+ Since some declarations may be used through meta-programming or otherwise untyped code, there may be false positives and the removal of candidates must be done carefully with context about the codebase.
169
169
 
170
- ```toml
171
- [linter.rules.<Rule name>]
172
- enabled = true
173
- exclude = ["path_to_skip/**"]
174
- ```
175
-
176
- ### `rdx mcp`
170
+ ### MCP server
177
171
 
178
172
  Rubydex can run as an MCP (Model Context Protocol) server, enabling AI assistants
179
173
  like Claude to semantically query your Ruby codebase.
@@ -214,6 +208,7 @@ like Claude to semantically query your Ruby codebase.
214
208
  | `get_declaration` | Full details by fully qualified name with docs, ancestors, members |
215
209
  | `get_descendants` | What classes/modules inherit from or include this one |
216
210
  | `find_constant_references` | All precise, resolved constant references across the codebase |
211
+ | `find_dead_code_candidates` | List of potentially unused code |
217
212
  | `get_file_declarations` | List declarations defined in a specific file |
218
213
  | `codebase_stats` | High-level statistics about the indexed codebase |
219
214
 
@@ -50,7 +50,7 @@
50
50
 
51
51
  <h2>Overview of licenses:</h2>
52
52
  <ul class="licenses-overview">
53
- <li><a href="#Apache-2.0">Apache License 2.0</a> (119)</li>
53
+ <li><a href="#Apache-2.0">Apache License 2.0</a> (120)</li>
54
54
  <li><a href="#Unicode-3.0">Unicode License v3</a> (19)</li>
55
55
  <li><a href="#MIT">MIT License</a> (17)</li>
56
56
  <li><a href="#BSD-2-Clause">BSD 2-Clause &quot;Simplified&quot; License</a> (2)</li>
@@ -2515,6 +2515,9 @@ limitations under the License.
2515
2515
  <li><a
2516
2516
  href=" https://github.com/comex/rust-shlex ">shlex
2517
2517
  1.3.0</a></li>
2518
+ <li><a
2519
+ href=" https://github.com/comex/rust-shlex ">shlex
2520
+ 2.0.1</a></li>
2518
2521
  <li><a
2519
2522
  href=" https://github.com/dtolnay/syn ">syn
2520
2523
  2.0.104</a></li>
@@ -2670,7 +2673,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2670
2673
  <ul class="license-used-by">
2671
2674
  <li><a
2672
2675
  href=" https://github.com/DoumanAsh/xxhash-rust ">xxhash-rust
2673
- 0.8.15</a></li>
2676
+ 0.8.16</a></li>
2674
2677
  </ul>
2675
2678
  <pre class="license-text">Boost Software License - Version 1.0 - August 17th, 2003
2676
2679
 
@@ -2821,26 +2824,6 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
2821
2824
 
2822
2825
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
2823
2826
 
2824
- THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2825
- </pre>
2826
- </li>
2827
- <li class="license">
2828
- <h3 id="MIT">MIT License</h3>
2829
- <h4>Used by:</h4>
2830
- <ul class="license-used-by">
2831
- <li><a
2832
- href=" https://github.com/ruby/prism ">ruby-prism-sys
2833
- 1.9.0</a></li>
2834
- <li><a
2835
- href=" https://github.com/ruby/prism ">ruby-prism
2836
- 1.9.0</a></li>
2837
- </ul>
2838
- <pre class="license-text">Copyright 2022-present, Shopify Inc.
2839
-
2840
- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the &quot;Software&quot;), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
2841
-
2842
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
2843
-
2844
2827
  THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2845
2828
  </pre>
2846
2829
  </li>
@@ -2881,13 +2864,19 @@ SOFTWARE.
2881
2864
  <ul class="license-used-by">
2882
2865
  <li><a
2883
2866
  href=" https://github.com/Shopify/rubydex ">rubydex
2884
- 0.2.6</a></li>
2867
+ 0.4.2</a></li>
2885
2868
  <li><a
2886
2869
  href=" https://github.com/Shopify/rubydex ">rubydex-sys
2887
- 0.2.6</a></li>
2870
+ 0.4.2</a></li>
2888
2871
  <li><a
2889
2872
  href=" https://github.com/DimaKudosh/difflib ">difflib
2890
2873
  0.4.0</a></li>
2874
+ <li><a
2875
+ href=" https://github.com/ruby/prism ">ruby-prism-sys
2876
+ 2.0.0-rc.2</a></li>
2877
+ <li><a
2878
+ href=" https://github.com/ruby/prism ">ruby-prism
2879
+ 2.0.0-rc.2</a></li>
2891
2880
  </ul>
2892
2881
  <pre class="license-text">MIT License
2893
2882
 
@@ -38,6 +38,17 @@ VALUE cClassVariableDefinition;
38
38
  VALUE cMethodAliasDefinition;
39
39
  VALUE cGlobalVariableAliasDefinition;
40
40
 
41
+ uint64_t rdxi_definition_id_for_graph(VALUE definition, VALUE graph_obj) {
42
+ HandleData *data;
43
+ TypedData_Get_Struct(definition, HandleData, &handle_type, data);
44
+
45
+ if (data->graph_obj != graph_obj) {
46
+ rb_raise(rb_eArgError, "definition must belong to this graph");
47
+ }
48
+
49
+ return data->id;
50
+ }
51
+
41
52
  // Keep this in sync with definition.rs
42
53
  VALUE rdxi_definition_class_for_kind(DefinitionKind kind) {
43
54
  switch (kind) {
@@ -153,6 +164,25 @@ static VALUE rdxr_definition_name(VALUE self) {
153
164
  return rdxi_owned_c_string_to_ruby(name);
154
165
  }
155
166
 
167
+ /*
168
+ * call-seq:
169
+ * raw_name -> String
170
+ *
171
+ * Returns the class, module, or constant name as written in source, preserving explicit parent scopes like "Foo::Bar"
172
+ * and rooted paths like "::Bar".
173
+ */
174
+ static VALUE rdxr_definition_raw_name(VALUE self) {
175
+ HandleData *data;
176
+ void *graph = rdxi_graph_from_handle(self, &data);
177
+
178
+ const char *name = rdx_definition_raw_name(graph, data->id);
179
+ if (name == NULL) {
180
+ rb_raise(rb_eRuntimeError, "Definition not found");
181
+ }
182
+
183
+ return rdxi_owned_c_string_to_ruby(name);
184
+ }
185
+
156
186
  /*
157
187
  * call-seq:
158
188
  * deprecated? -> bool
@@ -442,6 +472,7 @@ void rdxi_initialize_definition(VALUE mod) {
442
472
  rb_define_method(cDefinition, "document", rdxr_definition_document, 0);
443
473
 
444
474
  cClassDefinition = rb_define_class_under(mRubydex, "ClassDefinition", cDefinition);
475
+ rb_define_method(cClassDefinition, "raw_name", rdxr_definition_raw_name, 0);
445
476
  rb_define_method(cClassDefinition, "superclass", rdxr_class_definition_superclass, 0);
446
477
  rb_define_method(cClassDefinition, "mixins", rdxr_definition_mixins, 0);
447
478
 
@@ -449,10 +480,13 @@ void rdxi_initialize_definition(VALUE mod) {
449
480
  rb_define_method(cSingletonClassDefinition, "mixins", rdxr_definition_mixins, 0);
450
481
 
451
482
  cModuleDefinition = rb_define_class_under(mRubydex, "ModuleDefinition", cDefinition);
483
+ rb_define_method(cModuleDefinition, "raw_name", rdxr_definition_raw_name, 0);
452
484
  rb_define_method(cModuleDefinition, "mixins", rdxr_definition_mixins, 0);
453
485
 
454
486
  cConstantDefinition = rb_define_class_under(mRubydex, "ConstantDefinition", cDefinition);
487
+ rb_define_method(cConstantDefinition, "raw_name", rdxr_definition_raw_name, 0);
455
488
  cConstantAliasDefinition = rb_define_class_under(mRubydex, "ConstantAliasDefinition", cDefinition);
489
+ rb_define_method(cConstantAliasDefinition, "raw_name", rdxr_definition_raw_name, 0);
456
490
  cConstantVisibilityDefinition = rb_define_class_under(mRubydex, "ConstantVisibilityDefinition", cDefinition);
457
491
  cMethodVisibilityDefinition = rb_define_class_under(mRubydex, "MethodVisibilityDefinition", cDefinition);
458
492
  cMethodDefinition = rb_define_class_under(mRubydex, "MethodDefinition", cDefinition);
@@ -22,6 +22,9 @@ extern VALUE cGlobalVariableAliasDefinition;
22
22
 
23
23
  void rdxi_initialize_definition(VALUE mRubydex);
24
24
 
25
+ // Returns the definition ID after verifying that the definition belongs to `graph_obj`.
26
+ uint64_t rdxi_definition_id_for_graph(VALUE definition, VALUE graph_obj);
27
+
25
28
  // Returns the Ruby class for a given DefinitionKind without calling back into Rust
26
29
  VALUE rdxi_definition_class_for_kind(DefinitionKind kind);
27
30
 
@@ -6,11 +6,8 @@
6
6
  * mRubydex = rb_define_module("Rubydex")
7
7
  */
8
8
 
9
- static VALUE mRubydex;
10
9
  static VALUE mRules;
11
10
  VALUE cDiagnostic;
12
- static VALUE cRule;
13
- static ID id_default_severity;
14
11
 
15
12
  VALUE rdxi_build_diagnostic_severity_value(VALUE mRubydex, DiagnosticSeverity severity) {
16
13
  VALUE mSeverity = rb_const_get(mRubydex, rb_intern("Severity"));
@@ -31,55 +28,11 @@ VALUE rdxi_build_diagnostic_severity_value(VALUE mRubydex, DiagnosticSeverity se
31
28
  return Qnil;
32
29
  }
33
30
 
34
- /*
35
- * call-seq:
36
- * default_severity -> Rubydex::Severity::Base
37
- *
38
- * Returns the Rubydex::Severity subclass the diagnostics of this rule get unless the configuration overrides it.
39
- */
40
- static VALUE rdxr_generated_rule_default_severity(VALUE self) {
41
- VALUE severity = rb_attr_get(self, id_default_severity);
42
-
43
- // Class-level instance variables are not inherited, so a class inheriting from a generated rule reaches this without
44
- // a severity of its own.
45
- if (NIL_P(severity)) {
46
- rb_raise(rb_eRuntimeError, "Rule %s has no default severity", rb_class2name(self));
47
- }
48
-
49
- return severity;
50
- }
51
-
52
- // Generates a rule class for every rule the graph can report. Severities are resolved here rather than when they are
53
- // read, which is why Rubydex::Severity is loaded before this extension (see `lib/rubydex.rb`).
54
- static void define_generated_rules(void) {
55
- CRuleArray rule_array = rdx_rules();
56
- VALUE rules = rb_ary_new_capa((long)rule_array.len);
57
-
58
- for (size_t i = 0; i < rule_array.len; i++) {
59
- CRule built_in_rule = rule_array.items[i];
60
- VALUE name = rb_utf8_str_new(built_in_rule.name, (long)built_in_rule.name_length);
61
- VALUE rule = rb_define_class_under(mRules, StringValueCStr(name), cRule);
62
- VALUE severity = rdxi_build_diagnostic_severity_value(mRubydex, built_in_rule.default_severity);
63
-
64
- rb_ivar_set(rule, id_default_severity, severity);
65
- rb_define_singleton_method(rule, "default_severity", rdxr_generated_rule_default_severity, 0);
66
- rb_ary_push(rules, rule);
67
- }
68
-
69
- rdx_rules_free(rule_array);
70
- rb_define_const(mRules, "ALL", rb_obj_freeze(rules));
71
- }
72
-
73
31
  VALUE rdxi_rule_class_from_name(const char *name, size_t length) {
74
32
  return rb_const_get_at(mRules, rb_intern2(name, (long)length));
75
33
  }
76
34
 
77
35
  void rdxi_initialize_diagnostic(VALUE moduleRubydex) {
78
- mRubydex = moduleRubydex;
79
- id_default_severity = rb_intern("@default_severity");
80
-
81
- cDiagnostic = rb_define_class_under(mRubydex, "Diagnostic", rb_cObject);
82
- cRule = rb_define_class_under(mRubydex, "Rule", rb_cObject);
83
- mRules = rb_define_module_under(mRubydex, "Rules");
84
- define_generated_rules();
36
+ cDiagnostic = rb_define_class_under(moduleRubydex, "Diagnostic", rb_cObject);
37
+ mRules = rb_define_module_under(moduleRubydex, "Rules");
85
38
  }
@@ -23,12 +23,12 @@ release = ENV["RELEASE"] || !developing_rubydex
23
23
  root_dir = gem_dir.join("rust")
24
24
  target_dir = root_dir.join("target")
25
25
  target_dir = target_dir.join("x86_64-pc-windows-gnu") if Gem.win_platform?
26
- target_dir = target_dir.join(release ? "release" : "debug")
26
+ target_dir = target_dir.join("$(RUST_PROFILE)")
27
27
 
28
28
  bindings_path = root_dir.join("rubydex-sys").join("rustbindings.h")
29
29
 
30
30
  cargo_args = ["--manifest-path #{root_dir.join("Cargo.toml")}"]
31
- cargo_args << "--release" if release
31
+ cargo_args << "$(CARGO_PROFILE_FLAG)"
32
32
 
33
33
  if Gem.win_platform?
34
34
  cargo_args << "--target x86_64-pc-windows-gnu"
@@ -102,6 +102,8 @@ makefile = File.read("Makefile")
102
102
 
103
103
  new_makefile = makefile.gsub("$(OBJS): $(HDRS) $(ruby_headers)", <<~MAKEFILE.chomp)
104
104
  .PHONY: compile_rust
105
+ RUST_PROFILE = $(if $(strip $(RELEASE)),release,#{developing_rubydex ? "debug" : "release"})
106
+ CARGO_PROFILE_FLAG = $(if $(filter release,$(RUST_PROFILE)),--release)
105
107
  RUST_SRCS = #{File.expand_path("Cargo.toml", root_dir)} #{File.expand_path("Cargo.lock", root_dir)} #{rust_srcs.join(" ")}
106
108
 
107
109
  .rust_built: $(RUST_SRCS)
data/ext/rubydex/graph.c CHANGED
@@ -1,6 +1,7 @@
1
1
  #include "graph.h"
2
2
  #include "config.h"
3
3
  #include "declaration.h"
4
+ #include "definition.h"
4
5
  #include "diagnostic.h"
5
6
  #include "document.h"
6
7
  #include "location.h"
@@ -256,6 +257,30 @@ static VALUE rdxr_graph_fuzzy_search(int argc, VALUE *argv, VALUE self) {
256
257
  return rdxr_graph_yield_search_results(self, iter);
257
258
  }
258
259
 
260
+ /*
261
+ * call-seq:
262
+ * dead_code_candidates -> Enumerator[Rubydex::Declaration]
263
+ *
264
+ * Returns an enumerator over declarations that may be unused because the analysis could not find any references to
265
+ * them. This misses metaprogramming or untyped code, which is why the term candidates is used.
266
+ *
267
+ * Currently, only supports constants.
268
+ */
269
+ static VALUE rdxr_graph_dead_code_candidates(VALUE self) {
270
+ if (!rb_block_given_p()) {
271
+ return rb_enumeratorize(self, rb_str_new2("dead_code_candidates"), 0, NULL);
272
+ }
273
+
274
+ void *graph;
275
+ TypedData_Get_Struct(self, void *, &graph_type, graph);
276
+
277
+ void *iter = rdx_graph_dead_code_candidates(graph);
278
+ VALUE args = rb_ary_new_from_args(2, self, ULL2NUM((uintptr_t)iter));
279
+ rb_ensure(rdxi_declarations_yield, args, rdxi_declarations_ensure, args);
280
+
281
+ return self;
282
+ }
283
+
259
284
  // Body function for rb_ensure in Graph#documents
260
285
  static VALUE graph_documents_yield(VALUE args) {
261
286
  VALUE self = rb_ary_entry(args, 0);
@@ -486,26 +511,48 @@ static VALUE rdxr_graph_set_encoding(VALUE self, VALUE encoding) {
486
511
 
487
512
  /*
488
513
  * call-seq:
489
- * resolve_constant(name, nesting) -> Rubydex::Declaration?
514
+ * resolve_constant(name, context) -> Rubydex::Declaration?
490
515
  *
491
- * Runs the resolver on a single constant reference to determine what it points to.
516
+ * Runs the resolver on a single constant reference using an array of nesting names or a namespace definition as its
517
+ * context.
492
518
  */
493
- static VALUE rdxr_graph_resolve_constant(VALUE self, VALUE const_name, VALUE nesting) {
519
+ static VALUE rdxr_graph_resolve_constant(VALUE self, VALUE const_name, VALUE context) {
494
520
  Check_Type(const_name, T_STRING);
495
- rdxi_check_array_of_strings(nesting);
496
521
  const char *const_name_string = StringValueCStr(const_name);
497
522
 
498
- // Convert the given file paths into a char** array, so that we can pass to Rust
499
- size_t length = RARRAY_LEN(nesting);
500
- char **converted_file_paths = rdxi_str_array_to_char(nesting, length);
501
-
502
523
  void *graph;
503
524
  TypedData_Get_Struct(self, void *, &graph_type, graph);
504
525
 
505
- const CDeclaration *decl =
506
- rdx_graph_resolve_constant(graph, const_name_string, (const char **)converted_file_paths, length);
526
+ const CDeclaration *decl;
527
+ if (RB_TYPE_P(context, T_ARRAY)) {
528
+ rdxi_check_array_of_strings(context);
529
+
530
+ size_t length = RARRAY_LEN(context);
531
+ char **converted_names = rdxi_str_array_to_char(context, length);
532
+ decl = rdx_graph_resolve_constant(graph, const_name_string, (const char **)converted_names, length);
533
+ rdxi_free_str_array(converted_names, length);
534
+ } else if (rb_obj_is_kind_of(context, cClassDefinition) ||
535
+ rb_obj_is_kind_of(context, cSingletonClassDefinition) ||
536
+ rb_obj_is_kind_of(context, cModuleDefinition)) {
537
+ uint64_t definition_id = rdxi_definition_id_for_graph(context, self);
538
+ CDefinitionConstantResolutionResult result =
539
+ rdx_graph_resolve_constant_from_definition(graph, const_name_string, definition_id);
540
+
541
+ if (result.status == CDefinitionConstantResolution_DefinitionNotFound) {
542
+ rb_raise(rb_eRuntimeError, "Definition not found");
543
+ }
544
+ if (result.status == CDefinitionConstantResolution_InvalidDefinitionKind) {
545
+ // This is unreachable because the rb_obj_is_kind_of checks above only accept supported definition kinds.
546
+ rb_raise(rb_eRuntimeError, "Unexpected definition kind");
547
+ }
507
548
 
508
- rdxi_free_str_array(converted_file_paths, length);
549
+ decl = result.declaration;
550
+ } else {
551
+ rb_raise(
552
+ rb_eTypeError,
553
+ "context must be an Array of Strings, ClassDefinition, SingletonClassDefinition, or ModuleDefinition"
554
+ );
555
+ }
509
556
 
510
557
  if (decl == NULL) {
511
558
  return Qnil;
@@ -938,6 +985,7 @@ void rdxi_initialize_graph(VALUE moduleRubydex) {
938
985
  rb_define_method(cGraph, "[]", rdxr_graph_aref, 1);
939
986
  rb_define_method(cGraph, "search", rdxr_graph_search, -1);
940
987
  rb_define_method(cGraph, "fuzzy_search", rdxr_graph_fuzzy_search, -1);
988
+ rb_define_method(cGraph, "dead_code_candidates", rdxr_graph_dead_code_candidates, 0);
941
989
  rb_define_method(cGraph, "encoding=", rdxr_graph_set_encoding, 1);
942
990
  rb_define_method(cGraph, "resolve_require_path", rdxr_graph_resolve_require_path, 2);
943
991
  rb_define_method(cGraph, "require_paths", rdxr_graph_require_paths, 1);
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubydex/cli/command"
4
+
5
+ module Rubydex
6
+ module CLI
7
+ # `rdx dead_code` — lists dead code candidates in the current workspace.
8
+ class Command
9
+ class DeadCode < Command
10
+ command "dead_code"
11
+ summary "List dead code candidates in the current workspace"
12
+
13
+ #: -> void
14
+ def run
15
+ parse_options!
16
+ abort_with_usage("unexpected argument: #{argv.first}") unless argv.empty?
17
+
18
+ require "rubydex/linter/helpers/path_helpers"
19
+
20
+ $stderr.puts("Looking for potentially dead code...")
21
+ graph = build_graph($stderr, workspace_path: current_workspace_path)
22
+ candidates = graph.dead_code_candidates.sort_by(&:name)
23
+
24
+ candidates.each do |candidate|
25
+ puts(candidate.name)
26
+ candidate.definitions.map(&:location).sort.each do |location|
27
+ display = location.to_display
28
+ path = Rubydex::Linter::Helpers::PathHelpers.display_path(display, workspace: graph.workspace_path)
29
+ puts(" #{path}:#{display.start_line},#{display.start_column}-#{display.end_line},#{display.end_column}")
30
+ end
31
+ puts
32
+ end
33
+
34
+ print_summary(graph.documents.count, candidates.length)
35
+ end
36
+
37
+ private
38
+
39
+ #: (Integer file_count, Integer candidate_count) -> void
40
+ def print_summary(file_count, candidate_count)
41
+ if candidate_count.zero?
42
+ puts("#{file_count} #{pluralize("file", file_count)} inspected, no dead code candidates found")
43
+ return
44
+ end
45
+
46
+ puts(
47
+ "#{file_count} #{pluralize("file", file_count)} inspected, " \
48
+ "#{candidate_count} dead code #{pluralize("candidate", candidate_count)} found",
49
+ )
50
+ end
51
+
52
+ #: (String word, Integer count) -> String
53
+ def pluralize(word, count)
54
+ count == 1 ? word : "#{word}s"
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -9,8 +9,10 @@ module Rubydex
9
9
  class Lint
10
10
  # `rdx lint explain <RULE>` — prints documentation for rules with a matching name.
11
11
  class Explain < Command
12
- BASE_RULE_NAME = "Rubydex::Linter::CustomRule" #: String
13
- BASE_RULE_PATH = File.expand_path("../../../linter/custom_rule.rb", __dir__) #: String
12
+ BASE_PATHS = [
13
+ File.expand_path("../../../linter/custom_rule.rb", __dir__),
14
+ File.expand_path("../../../rule.rb", __dir__),
15
+ ] #: Array[String]
14
16
 
15
17
  class << self
16
18
  #: -> String
@@ -30,17 +32,17 @@ module Rubydex
30
32
  workspace_path = current_workspace_path
31
33
 
32
34
  graph = Rubydex::Graph.configure_for_workspace(workspace_path)
33
- graph.index_all([BASE_RULE_PATH, *Rubydex::Linter::RuleLoader.paths(workspace_path)])
35
+ graph.index_all([*BASE_PATHS, *Rubydex::Linter::RuleLoader.paths(workspace_path)])
34
36
  graph.resolve
35
37
 
36
- base_rule = graph[BASE_RULE_NAME]
37
- abort("Base rule class #{BASE_RULE_NAME} is not found. This is likely an issue in Rubydex itself") unless base_rule.is_a?(Rubydex::Class)
38
+ base_rule = graph["Rubydex::Rule"]
39
+ abort("Base rule class is not found. This is an issue in Rubydex itself") unless base_rule.is_a?(Rubydex::Class)
38
40
 
39
41
  rule_declarations = base_rule.descendants.grep(Rubydex::Class) #: as Array[Rubydex::Class]
40
42
  matched_rule_names = rule_declarations.filter_map do |declaration|
41
43
  declaration_name = declaration.name
42
44
  next unless declaration_name
43
- next if declaration_name == BASE_RULE_NAME
45
+ next if declaration_name == "Rubydex::Rule" || declaration_name == "Rubydex::Linter::CustomRule"
44
46
  next unless declaration_name.end_with?(rule_name)
45
47
 
46
48
  declaration
@@ -35,7 +35,7 @@ module Rubydex
35
35
  require "rubydex/linter"
36
36
 
37
37
  custom_rules = load_linter_rules(workspace_path)
38
- warn_unknown_rules(custom_rules)
38
+ warn_unknown_rules
39
39
 
40
40
  graph = build_graph($stderr, workspace_path:, config:)
41
41
  runner = Rubydex::Linter::Runner.new(graph, custom_rules:, config: linter_config)
@@ -64,9 +64,15 @@ module Rubydex
64
64
  @linter_config ||= config.linter #: Rubydex::LinterConfig?
65
65
  end
66
66
 
67
- #: (Array[singleton(Rubydex::Linter::CustomRule)] custom_rule_classes) -> void
68
- def warn_unknown_rules(custom_rule_classes)
69
- known_rule_names = (Rubydex::Rules::ALL + custom_rule_classes).map(&:rule_name).uniq.sort
67
+ #: () -> void
68
+ def warn_unknown_rules
69
+ known_rule_names = (Rule.subclasses + Rubydex::Linter::CustomRule.subclasses).filter_map do |rule|
70
+ name = rule.rule_name
71
+ name unless name == "CustomRule"
72
+ end
73
+ known_rule_names.uniq!
74
+ known_rule_names.sort!
75
+
70
76
  unknown_rule_names = linter_config.rules.keys.reject { |name| known_rule_names.include?(name) }.sort
71
77
  return if unknown_rule_names.empty?
72
78
 
Binary file
@@ -4,7 +4,7 @@ module Rubydex
4
4
  module Linter
5
5
  # Loads project and bundled-gem rules using the Rubydex linter path convention.
6
6
  class RuleLoader
7
- BUILT_IN_RULE_GLOB = File.expand_path("../../rubydex_linter/rules/**/*.rb", __dir__) #: String
7
+ BUILT_IN_RULE_GLOB = File.expand_path("../../{rubydex_linter,rubydex}/rules/**/*.rb", __dir__) #: String
8
8
  RULE_GLOB = "rubydex_linter/rules/**/*.rb" #: String
9
9
 
10
10
  class << self
@@ -4,7 +4,7 @@ module Rubydex
4
4
  module MCPServer
5
5
  class CodebaseStatsTool < BaseTool
6
6
  tool_name "codebase_stats"
7
- description "Get an overview of the indexed Ruby codebase: total file count, declaration counts, and breakdown by kind (classes, modules, methods, constants). Use this to understand codebase size and composition, or to verify that indexing completed successfully."
7
+ description "Count indexed files, declarations, definitions, and references, with declarations grouped by kind."
8
8
  input_schema(properties: {})
9
9
 
10
10
  #: -> Tool::Response
@@ -4,12 +4,12 @@ module Rubydex
4
4
  module MCPServer
5
5
  class FindConstantReferencesTool < BaseTool
6
6
  tool_name "find_constant_references"
7
- description "Find all resolved references to a Ruby class, module, or constant across the codebase. Returns file paths, line numbers, and columns for each usage. Results are paginated: the response includes `total`. If `total` exceeds the number of returned results, use `offset` to fetch subsequent pages."
7
+ description "Locate resolved references to a Ruby class, module, or constant."
8
8
  input_schema(
9
9
  properties: {
10
- name: { type: "string", description: "Fully qualified name of the class, module, or constant to find references for" },
11
- limit: { type: "integer", description: "Maximum number of references to return (default 50, max 200)" },
12
- offset: { type: "integer", description: "Number of references to skip for pagination (default 0)" },
10
+ name: { type: "string", description: "Fully qualified class, module, or constant name" },
11
+ limit: { type: "integer", description: "Page size (default 50, capped at 200)" },
12
+ offset: { type: "integer", description: "Number of results to skip (default 0)" },
13
13
  },
14
14
  required: ["name"],
15
15
  )
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubydex
4
+ module MCPServer
5
+ class FindDeadCodeCandidatesTool < BaseTool
6
+ tool_name "find_dead_code_candidates"
7
+ description "Find Ruby classes, modules, and constants with no detected references, including dependency code. Dynamic usage through DSLs or metaprogramming may not be detected."
8
+ input_schema(
9
+ properties: {
10
+ limit: { type: "integer", description: "Page size (default 50, capped at 100)" },
11
+ offset: { type: "integer", description: "Number of results to skip (default 0)" },
12
+ },
13
+ )
14
+
15
+ #: (?limit: Integer, ?offset: Integer) -> Tool::Response
16
+ def call(limit: nil, offset: nil)
17
+ declarations = @graph.dead_code_candidates.sort_by(&:name)
18
+ page, total = paginate(declarations, offset, limit, 100)
19
+ candidates = page.map do |declaration|
20
+ {
21
+ name: declaration.name,
22
+ kind: declaration_kind(declaration),
23
+ locations: declaration.definitions.sort_by(&:location).map do |definition|
24
+ display_location(definition.location)
25
+ end,
26
+ }
27
+ end
28
+
29
+ response(candidates: candidates, total: total)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -4,10 +4,10 @@ module Rubydex
4
4
  module MCPServer
5
5
  class GetDeclarationTool < BaseTool
6
6
  tool_name "get_declaration"
7
- description 'Get complete information about a Ruby class, module, method, or constant by its exact fully qualified name. Returns file locations, documentation comments, ancestor chain, and members with locations. FQN format: "Foo::Bar" for classes/modules/constants, "Foo::Bar#method_name" for instance methods, "Foo::Bar::<Bar>" for singleton classes, and "Foo::Bar::<Bar>#method_name" for class methods.'
7
+ description "Get a Ruby declaration's definition locations, documentation, ancestors, and members."
8
8
  input_schema(
9
9
  properties: {
10
- name: { type: "string", description: "Fully qualified name of the declaration (e.g. 'Foo::Bar', 'Foo::Bar#baz')" },
10
+ name: { type: "string", description: 'Exact fully qualified name: "Foo::Bar", "Foo::Bar#method_name()" (instance method), "Foo::Bar::<Bar>" (singleton class), or "Foo::Bar::<Bar>#method_name()" (class method)' },
11
11
  },
12
12
  required: ["name"],
13
13
  )