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.
- checksums.yaml +4 -4
- data/README.md +110 -6
- data/THIRD_PARTY_LICENSES.html +271 -2
- data/exe/rdx +2 -73
- data/ext/rubydex/config.c +140 -0
- data/ext/rubydex/config.h +16 -0
- data/ext/rubydex/declaration.c +1 -1
- data/ext/rubydex/definition.c +32 -4
- data/ext/rubydex/diagnostic.c +75 -1
- data/ext/rubydex/diagnostic.h +2 -0
- data/ext/rubydex/graph.c +27 -48
- data/ext/rubydex/graph.h +6 -0
- data/ext/rubydex/query.c +487 -0
- data/ext/rubydex/query.h +8 -0
- data/ext/rubydex/reference.c +60 -0
- data/ext/rubydex/rubydex.c +4 -0
- data/ext/rubydex/utils.c +23 -4
- data/ext/rubydex/utils.h +5 -0
- data/lib/ruby_lsp/rubydex/addon.rb +211 -0
- data/lib/rubydex/cli/command/console.rb +55 -0
- data/lib/rubydex/cli/command/lint/explain.rb +74 -0
- data/lib/rubydex/cli/command/lint.rb +202 -0
- data/lib/rubydex/cli/command/mcp.rb +30 -0
- data/lib/rubydex/cli/command/query.rb +70 -0
- data/lib/rubydex/cli/command/skill.rb +69 -0
- data/lib/rubydex/cli/command.rb +168 -0
- data/lib/rubydex/cli.rb +93 -0
- data/lib/rubydex/config.rb +59 -0
- data/lib/rubydex/diagnostic.rb +12 -3
- data/lib/rubydex/errors.rb +42 -1
- data/lib/rubydex/graph.rb +10 -3
- data/lib/rubydex/linter/custom_rule.rb +97 -0
- data/lib/rubydex/linter/helpers/path_helpers.rb +78 -0
- data/lib/rubydex/linter/helpers/source_access_helpers.rb +31 -0
- data/lib/rubydex/linter/rule_loader.rb +36 -0
- data/lib/rubydex/linter/rule_test_case.rb +343 -0
- data/lib/rubydex/linter/runner.rb +56 -0
- data/lib/rubydex/linter.rb +19 -0
- data/lib/rubydex/location.rb +3 -0
- data/lib/rubydex/mcp_server.rb +1 -2
- data/lib/rubydex/related_information.rb +17 -0
- data/lib/rubydex/rule.rb +33 -0
- data/lib/rubydex/severity.rb +70 -0
- data/lib/rubydex/skill.rb +88 -0
- data/lib/rubydex/skill_registry.rb +62 -0
- data/lib/rubydex/version.rb +1 -1
- data/lib/rubydex.rb +6 -0
- data/lib/rubydex_linter/rules/rule_structure.rb +125 -0
- data/rbi/rubydex.rbi +578 -15
- data/rust/Cargo.lock +7 -0
- data/rust/rubydex/Cargo.toml +1 -0
- data/rust/rubydex/benches/graph_memory.rs +20 -4
- data/rust/rubydex/src/compile_assertions.rs +15 -0
- data/rust/rubydex/src/config.rs +538 -157
- data/rust/rubydex/src/diagnostic.rs +66 -40
- data/rust/rubydex/src/errors.rs +0 -1
- data/rust/rubydex/src/indexing/local_graph.rs +6 -5
- data/rust/rubydex/src/indexing/rbs_indexer.rs +284 -8
- data/rust/rubydex/src/indexing/ruby_indexer.rs +59 -70
- data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +195 -86
- data/rust/rubydex/src/lib.rs +1 -0
- data/rust/rubydex/src/listing.rs +26 -1
- data/rust/rubydex/src/main.rs +9 -128
- data/rust/rubydex/src/model/declaration.rs +301 -229
- data/rust/rubydex/src/model/definitions.rs +27 -26
- data/rust/rubydex/src/model/document.rs +43 -7
- data/rust/rubydex/src/model/graph.rs +67 -68
- data/rust/rubydex/src/model/id.rs +55 -0
- data/rust/rubydex/src/model/ids.rs +21 -9
- data/rust/rubydex/src/model/name.rs +88 -19
- data/rust/rubydex/src/model/references.rs +16 -13
- data/rust/rubydex/src/operation/ruby_builder.rs +78 -104
- data/rust/rubydex/src/path_helpers.rs +77 -0
- data/rust/rubydex/src/query/cypher/schema.rs +853 -0
- data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
- data/rust/rubydex/src/query/cypher/tests.rs +253 -0
- data/rust/rubydex/src/query/cypher.rs +54 -0
- data/rust/rubydex/src/query.rs +125 -43
- data/rust/rubydex/src/resolution.rs +368 -395
- data/rust/rubydex/src/resolution_tests.rs +504 -78
- data/rust/rubydex/src/test_utils/context.rs +2 -1
- data/rust/rubydex/src/test_utils/graph_test.rs +26 -12
- data/rust/rubydex/src/test_utils/local_graph_test.rs +19 -0
- data/rust/rubydex/tests/cli.rs +4 -4
- data/rust/rubydex-sys/src/config_api.rs +205 -0
- data/rust/rubydex-sys/src/cypher_api.rs +791 -0
- data/rust/rubydex-sys/src/declaration_api.rs +6 -3
- data/rust/rubydex-sys/src/definition_api.rs +27 -7
- data/rust/rubydex-sys/src/diagnostic_api.rs +77 -8
- data/rust/rubydex-sys/src/graph_api.rs +31 -68
- data/rust/rubydex-sys/src/lib.rs +2 -0
- data/rust/rubydex-sys/src/name_api.rs +2 -6
- data/rust/rubydex-sys/src/reference_api.rs +58 -12
- data/rust/rubydex-sys/src/utils.rs +37 -0
- data/skills/send-private-method/SKILL.md +133 -0
- 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
|
data/lib/rubydex/version.rb
CHANGED
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
|