rubydex 0.2.9 → 0.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +110 -6
  3. data/THIRD_PARTY_LICENSES.html +271 -2
  4. data/exe/rdx +2 -73
  5. data/ext/rubydex/config.c +140 -0
  6. data/ext/rubydex/config.h +16 -0
  7. data/ext/rubydex/declaration.c +1 -1
  8. data/ext/rubydex/definition.c +32 -4
  9. data/ext/rubydex/diagnostic.c +75 -1
  10. data/ext/rubydex/diagnostic.h +2 -0
  11. data/ext/rubydex/graph.c +27 -48
  12. data/ext/rubydex/graph.h +6 -0
  13. data/ext/rubydex/query.c +487 -0
  14. data/ext/rubydex/query.h +8 -0
  15. data/ext/rubydex/reference.c +60 -0
  16. data/ext/rubydex/rubydex.c +4 -0
  17. data/ext/rubydex/utils.c +23 -4
  18. data/ext/rubydex/utils.h +5 -0
  19. data/lib/ruby_lsp/rubydex/addon.rb +211 -0
  20. data/lib/rubydex/cli/command/console.rb +55 -0
  21. data/lib/rubydex/cli/command/lint/explain.rb +74 -0
  22. data/lib/rubydex/cli/command/lint.rb +202 -0
  23. data/lib/rubydex/cli/command/mcp.rb +30 -0
  24. data/lib/rubydex/cli/command/query.rb +70 -0
  25. data/lib/rubydex/cli/command/skill.rb +69 -0
  26. data/lib/rubydex/cli/command.rb +168 -0
  27. data/lib/rubydex/cli.rb +93 -0
  28. data/lib/rubydex/config.rb +59 -0
  29. data/lib/rubydex/diagnostic.rb +12 -3
  30. data/lib/rubydex/errors.rb +42 -1
  31. data/lib/rubydex/graph.rb +10 -3
  32. data/lib/rubydex/linter/custom_rule.rb +97 -0
  33. data/lib/rubydex/linter/helpers/path_helpers.rb +78 -0
  34. data/lib/rubydex/linter/helpers/source_access_helpers.rb +31 -0
  35. data/lib/rubydex/linter/rule_loader.rb +36 -0
  36. data/lib/rubydex/linter/rule_test_case.rb +343 -0
  37. data/lib/rubydex/linter/runner.rb +56 -0
  38. data/lib/rubydex/linter.rb +19 -0
  39. data/lib/rubydex/location.rb +3 -0
  40. data/lib/rubydex/mcp_server.rb +1 -2
  41. data/lib/rubydex/related_information.rb +17 -0
  42. data/lib/rubydex/rule.rb +33 -0
  43. data/lib/rubydex/severity.rb +70 -0
  44. data/lib/rubydex/skill.rb +88 -0
  45. data/lib/rubydex/skill_registry.rb +62 -0
  46. data/lib/rubydex/version.rb +1 -1
  47. data/lib/rubydex.rb +6 -0
  48. data/lib/rubydex_linter/rules/rule_structure.rb +125 -0
  49. data/rbi/rubydex.rbi +578 -15
  50. data/rust/Cargo.lock +7 -0
  51. data/rust/rubydex/Cargo.toml +1 -0
  52. data/rust/rubydex/benches/graph_memory.rs +20 -4
  53. data/rust/rubydex/src/compile_assertions.rs +15 -0
  54. data/rust/rubydex/src/config.rs +538 -157
  55. data/rust/rubydex/src/diagnostic.rs +66 -40
  56. data/rust/rubydex/src/errors.rs +0 -1
  57. data/rust/rubydex/src/indexing/local_graph.rs +6 -5
  58. data/rust/rubydex/src/indexing/rbs_indexer.rs +284 -8
  59. data/rust/rubydex/src/indexing/ruby_indexer.rs +59 -70
  60. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +195 -86
  61. data/rust/rubydex/src/lib.rs +1 -0
  62. data/rust/rubydex/src/listing.rs +26 -1
  63. data/rust/rubydex/src/main.rs +9 -128
  64. data/rust/rubydex/src/model/declaration.rs +301 -229
  65. data/rust/rubydex/src/model/definitions.rs +27 -26
  66. data/rust/rubydex/src/model/document.rs +43 -7
  67. data/rust/rubydex/src/model/graph.rs +67 -68
  68. data/rust/rubydex/src/model/id.rs +55 -0
  69. data/rust/rubydex/src/model/ids.rs +21 -9
  70. data/rust/rubydex/src/model/name.rs +88 -19
  71. data/rust/rubydex/src/model/references.rs +16 -13
  72. data/rust/rubydex/src/operation/ruby_builder.rs +78 -104
  73. data/rust/rubydex/src/path_helpers.rs +77 -0
  74. data/rust/rubydex/src/query/cypher/schema.rs +853 -0
  75. data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
  76. data/rust/rubydex/src/query/cypher/tests.rs +253 -0
  77. data/rust/rubydex/src/query/cypher.rs +54 -0
  78. data/rust/rubydex/src/query.rs +125 -43
  79. data/rust/rubydex/src/resolution.rs +368 -395
  80. data/rust/rubydex/src/resolution_tests.rs +504 -78
  81. data/rust/rubydex/src/test_utils/context.rs +2 -1
  82. data/rust/rubydex/src/test_utils/graph_test.rs +26 -12
  83. data/rust/rubydex/src/test_utils/local_graph_test.rs +19 -0
  84. data/rust/rubydex/tests/cli.rs +4 -4
  85. data/rust/rubydex-sys/src/config_api.rs +205 -0
  86. data/rust/rubydex-sys/src/cypher_api.rs +791 -0
  87. data/rust/rubydex-sys/src/declaration_api.rs +6 -3
  88. data/rust/rubydex-sys/src/definition_api.rs +27 -7
  89. data/rust/rubydex-sys/src/diagnostic_api.rs +77 -8
  90. data/rust/rubydex-sys/src/graph_api.rs +31 -68
  91. data/rust/rubydex-sys/src/lib.rs +2 -0
  92. data/rust/rubydex-sys/src/name_api.rs +2 -6
  93. data/rust/rubydex-sys/src/reference_api.rs +58 -12
  94. data/rust/rubydex-sys/src/utils.rs +37 -0
  95. data/skills/send-private-method/SKILL.md +133 -0
  96. metadata +37 -2
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubydex/errors"
4
+
5
+ module Rubydex
6
+ # A skill file loaded from disk. Skill files are Markdown documents with YAML frontmatter
7
+ # that declare a `name` and `description`. The body is the Markdown content after the frontmatter.
8
+ class Skill
9
+ #: String
10
+ attr_reader :name
11
+
12
+ #: String
13
+ attr_reader :description
14
+
15
+ #: String
16
+ attr_reader :body
17
+
18
+ class << self
19
+ #: (String path) -> Rubydex::Skill
20
+ def load(path)
21
+ from_content(File.read(path), path:)
22
+ rescue SystemCallError, IOError => e
23
+ raise SkillError, "Cannot read skill file #{path}: #{e.message}"
24
+ end
25
+
26
+ #: (String content, ?path: String?) -> Rubydex::Skill
27
+ def from_content(content, path: nil)
28
+ new(**parse(content, path:))
29
+ end
30
+
31
+ private
32
+
33
+ #: (String content, path: String?) -> { name: String, description: String, body: String }
34
+ def parse(content, path:)
35
+ lines = content.lines
36
+
37
+ unless lines.first&.strip == "---"
38
+ raise SkillError, "Missing YAML frontmatter delimiter#{location(path)}"
39
+ end
40
+
41
+ closing_index = (1...lines.length).find { |i| lines[i].strip == "---" }
42
+ unless closing_index
43
+ raise SkillError, "Unterminated YAML frontmatter#{location(path)}"
44
+ end
45
+
46
+ frontmatter = lines[1...closing_index].join
47
+ body = lines[(closing_index + 1)..].join.strip
48
+
49
+ yaml = parse_frontmatter(frontmatter, path)
50
+ unless yaml.is_a?(Hash)
51
+ raise SkillError, "Empty or malformed YAML frontmatter#{location(path)}"
52
+ end
53
+
54
+ name = yaml["name"]
55
+ unless name.is_a?(String)
56
+ raise SkillError, "Missing or non-string `name` in frontmatter#{location(path)}"
57
+ end
58
+
59
+ description = yaml["description"]
60
+ unless description.is_a?(String)
61
+ raise SkillError, "Missing or non-string `description` in frontmatter#{location(path)}"
62
+ end
63
+
64
+ { name:, description:, body: }
65
+ end
66
+
67
+ #: (String frontmatter, String? path) -> untyped
68
+ def parse_frontmatter(frontmatter, path)
69
+ YAML.safe_load(frontmatter)
70
+ rescue Psych::Exception => e
71
+ raise SkillError, "Malformed YAML frontmatter#{location(path)}: #{e.message}"
72
+ end
73
+
74
+ #: (String?) -> String
75
+ def location(path)
76
+ path ? " in #{path}" : ""
77
+ end
78
+ end
79
+
80
+ #: (name: String, description: String, body: String) -> void
81
+ def initialize(name:, description:, body:)
82
+ @name = name.freeze
83
+ @description = description.freeze
84
+ @body = body.freeze
85
+ freeze
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubydex/skill"
4
+
5
+ module Rubydex
6
+ # The skills available on disk, keyed by id. `load` scans a directory for `<id>/SKILL.md` entries
7
+ # and records their paths.
8
+ # A skill file is only read when {#fetch} asks for it. Listing every description reads all skill files.
9
+ #
10
+ # The directory name is the id: it is what `rdx skill <id>` takes as a parameter.
11
+ class SkillRegistry
12
+ class << self
13
+ #: (String directory) -> Rubydex::SkillRegistry
14
+ def load(directory)
15
+ unless File.directory?(directory)
16
+ raise UnknownSkillDirectoryError, "Skill directory does not exist: #{directory}"
17
+ end
18
+
19
+ paths = {}
20
+ Dir.foreach(directory) do |entry|
21
+ next if entry == "." || entry == ".."
22
+
23
+ skill_path = File.join(directory, entry, "SKILL.md")
24
+ next unless File.file?(skill_path)
25
+
26
+ paths[entry] = skill_path
27
+ end
28
+
29
+ new(paths)
30
+ end
31
+ end
32
+
33
+ #: (Hash[String, String] paths) -> void
34
+ def initialize(paths)
35
+ @paths = paths.freeze
36
+ # Deliberately mutable inside a frozen registry: `freeze` protects the set of skills, while
37
+ # the cache only remembers what was already read from those files.
38
+ @cache = {} #: Hash[String, Skill]
39
+ freeze
40
+ end
41
+
42
+ #: -> Array[String]
43
+ def ids
44
+ @paths.keys.sort
45
+ end
46
+
47
+ #: (String id) -> Rubydex::Skill
48
+ def fetch(id)
49
+ @cache[id] ||= begin
50
+ path = @paths[id]
51
+ raise UnknownSkillError, "Unknown skill: #{id}" unless path
52
+
53
+ skill = Skill.load(path)
54
+ unless skill.name == id
55
+ raise SkillError, "Skill in #{path} declares `name: #{skill.name}` but lives in `#{id}`"
56
+ end
57
+
58
+ skill
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubydex
4
- VERSION = "0.2.9"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/rubydex.rb CHANGED
@@ -2,8 +2,11 @@
2
2
 
3
3
  require "bundler"
4
4
  require "uri"
5
+
6
+ # Files that must be loaded before the C extension
5
7
  require "rubydex/version"
6
8
  require "rubydex/mixin"
9
+ require "rubydex/severity"
7
10
 
8
11
  begin
9
12
  # Load the precompiled version of the library
@@ -16,8 +19,11 @@ end
16
19
 
17
20
  require "rubydex/errors"
18
21
  require "rubydex/failures"
22
+ require "rubydex/config"
19
23
  require "rubydex/location"
20
24
  require "rubydex/comment"
25
+ require "rubydex/rule"
26
+ require "rubydex/related_information"
21
27
  require "rubydex/diagnostic"
22
28
  require "rubydex/keyword"
23
29
  require "rubydex/keyword_parameter"
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubydex
4
+ module Linter
5
+ module Rules
6
+ # Ensures discovered workspace linter rules follow these conventions:
7
+ #
8
+ # - A rule file does not define more than one linter rule.
9
+ # - Each rule subclass outside a test directory is in a rule directory.
10
+ # - Each checked rule subclass is in the `Rubydex::Linter::Rules` namespace.
11
+ #
12
+ # The rule directories are `rubydex_linter/rules/` and `lib/rubydex_linter/rules/`.
13
+ # This rule does not report files in those directories that define no rule subclass.
14
+ class RuleStructure < CustomRule
15
+ include Helpers::SourceAccessHelpers
16
+
17
+ BASE_RULE_NAME = "Rubydex::Linter::CustomRule" #: String
18
+ RULE_NAMESPACE = "Rubydex::Linter::Rules" #: String
19
+ RULE_FILE_PATTERNS = [
20
+ "rubydex_linter/rules/**/*.rb",
21
+ "lib/rubydex_linter/rules/**/*.rb",
22
+ ].freeze #: Array[String]
23
+ TEST_FILE_PATTERNS = ["test/**/*", "**/test/**/*"].freeze #: Array[String]
24
+
25
+ class << self
26
+ # @override
27
+ #: -> singleton(Severity::Base)
28
+ def default_severity
29
+ Severity::Error
30
+ end
31
+ end
32
+
33
+ # @override
34
+ #: -> void
35
+ def lint
36
+ rules = child_classes(BASE_RULE_NAME)
37
+ rule_definitions_by_file = {} #: Hash[String, Hash[Rubydex::Class, Definition]]
38
+
39
+ rules.each do |rule|
40
+ rule.definitions.each do |rule_definition|
41
+ uri = rule_definition.document.uri
42
+ path = path_for_uri(uri)
43
+ next unless path_in_workspace?(path)
44
+
45
+ if rule_file?(path)
46
+ rule_definitions = (rule_definitions_by_file[uri] ||= {}) #: Hash[Rubydex::Class, Definition]
47
+ rule_definitions[rule] ||= rule_definition
48
+ elsif !test_file?(path)
49
+ report_wrong_rule_directory(rule.name, rule_definition)
50
+ end
51
+ end
52
+
53
+ rule_definition = rule.definitions.find do |definition|
54
+ path = path_for_uri(definition.document.uri)
55
+ path_in_workspace?(path) && (rule_file?(path) || !test_file?(path))
56
+ end
57
+ next unless rule_definition
58
+ next if rule.name.start_with?("#{RULE_NAMESPACE}::")
59
+
60
+ report_wrong_rule_namespace(rule.name, rule_definition)
61
+ end
62
+
63
+ rule_definitions_by_file.each do |uri, rule_definitions|
64
+ next if rule_definitions.length <= 1
65
+
66
+ add_diagnostic(
67
+ "Each rule file must define only one linter rule; found #{rule_definitions.length}.",
68
+ file_location(uri),
69
+ related_information: rule_definitions.map do |rule, rule_definition|
70
+ RelatedInformation.new(
71
+ "`#{rule.name}` is defined here.",
72
+ diagnostic_location(rule_definition),
73
+ )
74
+ end,
75
+ )
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ #: (String, Definition) -> void
82
+ def report_wrong_rule_directory(rule_name, rule_definition)
83
+ add_diagnostic(
84
+ "`#{rule_name}` must be defined under `rubydex_linter/rules/` or `lib/rubydex_linter/rules/`.",
85
+ diagnostic_location(rule_definition),
86
+ )
87
+ end
88
+
89
+ #: (String, Definition) -> void
90
+ def report_wrong_rule_namespace(rule_name, rule_definition)
91
+ add_diagnostic(
92
+ "`#{rule_name}` must be defined under `#{RULE_NAMESPACE}`.",
93
+ diagnostic_location(rule_definition),
94
+ )
95
+ end
96
+
97
+ #: (String) -> bool
98
+ def path_in_workspace?(path)
99
+ workspace = graph.workspace_path
100
+ path == workspace || path.start_with?("#{workspace}/")
101
+ end
102
+
103
+ #: (String) -> bool
104
+ def rule_file?(path)
105
+ path_matches_patterns?(path, RULE_FILE_PATTERNS)
106
+ end
107
+
108
+ #: (String) -> bool
109
+ def test_file?(path)
110
+ path_matches_patterns?(path, TEST_FILE_PATTERNS)
111
+ end
112
+
113
+ #: (String, Array[String]) -> bool
114
+ def path_matches_patterns?(path, patterns)
115
+ Helpers::PathHelpers.path_matches_patterns?(
116
+ path,
117
+ patterns,
118
+ workspace: graph.workspace_path,
119
+ flags: Helpers::PathHelpers::EXCLUDE_FNMATCH_FLAGS,
120
+ )
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end