rubydex 0.3.0 → 0.4.1
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 +29 -7
- data/THIRD_PARTY_LICENSES.html +238 -2
- data/exe/rdx +2 -149
- data/ext/rubydex/config.c +140 -0
- data/ext/rubydex/config.h +16 -0
- data/ext/rubydex/definition.c +34 -0
- data/ext/rubydex/definition.h +3 -0
- data/ext/rubydex/diagnostic.c +28 -1
- data/ext/rubydex/diagnostic.h +2 -0
- data/ext/rubydex/extconf.rb +4 -2
- data/ext/rubydex/graph.c +71 -55
- data/ext/rubydex/graph.h +6 -0
- data/ext/rubydex/query.c +398 -16
- data/ext/rubydex/rubydex.c +2 -0
- data/ext/rubydex/utils.c +11 -4
- 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 +76 -0
- data/lib/rubydex/cli/command/lint.rb +208 -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/rules/dynamic_ancestor.rb +29 -0
- data/lib/rubydex/rules/dynamic_constant_reference.rb +25 -0
- data/lib/rubydex/rules/dynamic_singleton_definition.rb +30 -0
- data/lib/rubydex/rules/invalid_constant_visibility.rb +28 -0
- data/lib/rubydex/rules/invalid_method_visibility.rb +28 -0
- data/lib/rubydex/rules/parse_error.rb +25 -0
- data/lib/rubydex/rules/parse_warning.rb +28 -0
- data/lib/rubydex/rules/top_level_mixin_self.rb +27 -0
- data/lib/rubydex/rules/undefined_constant_visibility_target.rb +27 -0
- data/lib/rubydex/rules/undefined_method_visibility_target.rb +27 -0
- data/lib/rubydex/severity.rb +70 -0
- data/lib/rubydex/skill.rb +89 -0
- data/lib/rubydex/skill_registry.rb +62 -0
- data/lib/rubydex/version.rb +1 -1
- data/lib/rubydex.rb +8 -0
- data/lib/rubydex_linter/rules/rule_structure.rb +140 -0
- data/rbi/rubydex.rbi +464 -21
- data/rust/Cargo.lock +2 -2
- data/rust/rubydex/Cargo.toml +5 -1
- data/rust/rubydex/benches/graph_memory.rs +3 -5
- data/rust/rubydex/src/bin/generate_ruby_rules.rs +94 -0
- data/rust/rubydex/src/config.rs +538 -157
- data/rust/rubydex/src/diagnostic.rs +162 -45
- 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 +270 -6
- data/rust/rubydex/src/indexing/ruby_indexer.rs +10 -12
- data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +134 -81
- data/rust/rubydex/src/lib.rs +1 -0
- data/rust/rubydex/src/listing.rs +26 -1
- data/rust/rubydex/src/main.rs +7 -4
- data/rust/rubydex/src/model/declaration.rs +302 -219
- data/rust/rubydex/src/model/graph.rs +27 -40
- data/rust/rubydex/src/model/name.rs +58 -17
- data/rust/rubydex/src/operation/ruby_builder.rs +30 -45
- data/rust/rubydex/src/path_helpers.rs +77 -0
- data/rust/rubydex/src/query/cypher/schema.rs +63 -0
- data/rust/rubydex/src/query/cypher/tests.rs +26 -1
- data/rust/rubydex/src/query/cypher.rs +8 -11
- data/rust/rubydex/src/query.rs +314 -44
- data/rust/rubydex/src/resolution.rs +139 -187
- data/rust/rubydex/src/resolution_tests.rs +247 -19
- 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/definition_api.rs +68 -1
- data/rust/rubydex-sys/src/diagnostic_api.rs +42 -8
- data/rust/rubydex-sys/src/graph_api.rs +125 -205
- data/rust/rubydex-sys/src/lib.rs +2 -0
- data/rust/rubydex-sys/src/name_api.rs +49 -17
- data/rust/rubydex-sys/src/utils.rs +37 -0
- data/skills/send-private-method/SKILL.md +133 -0
- metadata +42 -2
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# This file is generated by `generate_ruby_rules.rs`. Do not edit this manually.
|
|
5
|
+
|
|
6
|
+
module Rubydex
|
|
7
|
+
module Rules
|
|
8
|
+
# Method visibility operations that depend on dynamic values cannot be reasoned about statically because they
|
|
9
|
+
# depend on runtime values. The program may still be valid, but the quality of the analysis degrades.
|
|
10
|
+
#
|
|
11
|
+
# ```ruby
|
|
12
|
+
# var.private :foo
|
|
13
|
+
# ^^^ Invalid method visibility. This might be correct, but it cannot be understood by the analysis.
|
|
14
|
+
#
|
|
15
|
+
# private(var)
|
|
16
|
+
# ^^^ Invalid method visibility.
|
|
17
|
+
# ```
|
|
18
|
+
class InvalidMethodVisibility < Rule
|
|
19
|
+
class << self
|
|
20
|
+
# @override
|
|
21
|
+
#: -> singleton(Severity::Base)
|
|
22
|
+
def default_severity
|
|
23
|
+
Severity::Warning
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# This file is generated by `generate_ruby_rules.rs`. Do not edit this manually.
|
|
5
|
+
|
|
6
|
+
module Rubydex
|
|
7
|
+
module Rules
|
|
8
|
+
# A parse error represents invalid Ruby syntax and a program that will fail to execute. For example, a missing
|
|
9
|
+
# `end`, an unterminated string, a missing parenthesis.
|
|
10
|
+
#
|
|
11
|
+
# ```ruby
|
|
12
|
+
# class Foo
|
|
13
|
+
# ^^^^^ Syntax error. Missing end token
|
|
14
|
+
# ```
|
|
15
|
+
class ParseError < Rule
|
|
16
|
+
class << self
|
|
17
|
+
# @override
|
|
18
|
+
#: -> singleton(Severity::Base)
|
|
19
|
+
def default_severity
|
|
20
|
+
Severity::Error
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# This file is generated by `generate_ruby_rules.rs`. Do not edit this manually.
|
|
5
|
+
|
|
6
|
+
module Rubydex
|
|
7
|
+
module Rules
|
|
8
|
+
# Parse warnings represent code that has valid syntax, but may not do what the developer expects. For example,
|
|
9
|
+
# local variables that are completely unused or usage in a void context (creating a line of code that does
|
|
10
|
+
# nothing).
|
|
11
|
+
#
|
|
12
|
+
# ```ruby
|
|
13
|
+
# CONST = 1
|
|
14
|
+
# CONST
|
|
15
|
+
# ^^^^^ Constant used in void context (the expression does nothing).
|
|
16
|
+
# puts CONST + 2
|
|
17
|
+
# ```
|
|
18
|
+
class ParseWarning < Rule
|
|
19
|
+
class << self
|
|
20
|
+
# @override
|
|
21
|
+
#: -> singleton(Severity::Base)
|
|
22
|
+
def default_severity
|
|
23
|
+
Severity::Warning
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# This file is generated by `generate_ruby_rules.rs`. Do not edit this manually.
|
|
5
|
+
|
|
6
|
+
module Rubydex
|
|
7
|
+
module Rules
|
|
8
|
+
# The top level of a Ruby program is the special <main> object. It is not possible to use `include self` or
|
|
9
|
+
# `extend self` on that object.
|
|
10
|
+
#
|
|
11
|
+
# ```ruby
|
|
12
|
+
# include self
|
|
13
|
+
# ^^^^ Cannot include self at the top level
|
|
14
|
+
# extend self
|
|
15
|
+
# ^^^^ Cannot extend self at the top level
|
|
16
|
+
# ```
|
|
17
|
+
class TopLevelMixinSelf < Rule
|
|
18
|
+
class << self
|
|
19
|
+
# @override
|
|
20
|
+
#: -> singleton(Severity::Base)
|
|
21
|
+
def default_severity
|
|
22
|
+
Severity::Information
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# This file is generated by `generate_ruby_rules.rs`. Do not edit this manually.
|
|
5
|
+
|
|
6
|
+
module Rubydex
|
|
7
|
+
module Rules
|
|
8
|
+
# Undefined constant visibility target means the analysis couldn't find the definition for the constant the code is
|
|
9
|
+
# attempting to change visibility of. It could be defined through meta-programming or it indeed does not exist.
|
|
10
|
+
#
|
|
11
|
+
# ```ruby
|
|
12
|
+
# class Foo
|
|
13
|
+
# private_constant :Bar
|
|
14
|
+
# ^^^ Undefined constant visibility target. The constant `Bar` is not defined.
|
|
15
|
+
# end
|
|
16
|
+
# ```
|
|
17
|
+
class UndefinedConstantVisibilityTarget < Rule
|
|
18
|
+
class << self
|
|
19
|
+
# @override
|
|
20
|
+
#: -> singleton(Severity::Base)
|
|
21
|
+
def default_severity
|
|
22
|
+
Severity::Warning
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# This file is generated by `generate_ruby_rules.rs`. Do not edit this manually.
|
|
5
|
+
|
|
6
|
+
module Rubydex
|
|
7
|
+
module Rules
|
|
8
|
+
# Undefined method visibility target means the analysis couldn't find the definition for the method the code is
|
|
9
|
+
# attempting to change visibility of. It could be defined through meta-programming or it indeed does not exist.
|
|
10
|
+
#
|
|
11
|
+
# ```ruby
|
|
12
|
+
# class Foo
|
|
13
|
+
# private :bar
|
|
14
|
+
# ^^^ Undefined method visibility target. The method `bar` is not defined.
|
|
15
|
+
# end
|
|
16
|
+
# ```
|
|
17
|
+
class UndefinedMethodVisibilityTarget < Rule
|
|
18
|
+
class << self
|
|
19
|
+
# @override
|
|
20
|
+
#: -> singleton(Severity::Base)
|
|
21
|
+
def default_severity
|
|
22
|
+
Severity::Warning
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubydex
|
|
4
|
+
module Severity
|
|
5
|
+
# @abstract
|
|
6
|
+
class Base
|
|
7
|
+
class << self
|
|
8
|
+
# @abstract
|
|
9
|
+
#: () -> Symbol
|
|
10
|
+
def value
|
|
11
|
+
raise NotImplementedError, "Subclasses must implement the value method"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class Error < Base
|
|
17
|
+
class << self
|
|
18
|
+
# @override
|
|
19
|
+
#: () -> Symbol
|
|
20
|
+
def value
|
|
21
|
+
:error
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class Warning < Base
|
|
27
|
+
class << self
|
|
28
|
+
# @override
|
|
29
|
+
#: () -> Symbol
|
|
30
|
+
def value
|
|
31
|
+
:warning
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class Information < Base
|
|
37
|
+
class << self
|
|
38
|
+
# @override
|
|
39
|
+
#: () -> Symbol
|
|
40
|
+
def value
|
|
41
|
+
:information
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class Hint < Base
|
|
47
|
+
class << self
|
|
48
|
+
# @override
|
|
49
|
+
#: () -> Symbol
|
|
50
|
+
def value
|
|
51
|
+
:hint
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# All severities order based on their importance.
|
|
57
|
+
ALL = [Error, Warning, Information, Hint].freeze #: Array[singleton(Base)]
|
|
58
|
+
VALUE_MAP = ALL.to_h { |severity| [severity.value, severity] }.freeze #: Hash[Symbol, singleton(Base)]
|
|
59
|
+
|
|
60
|
+
class << self
|
|
61
|
+
#: (Symbol) -> singleton(Base)
|
|
62
|
+
def from_value(value)
|
|
63
|
+
severity = VALUE_MAP[value]
|
|
64
|
+
return severity if severity
|
|
65
|
+
|
|
66
|
+
raise ArgumentError, "Invalid severity #{value.inspect}. Expected one of: #{VALUE_MAP.keys.join(", ")}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "rubydex/errors"
|
|
5
|
+
|
|
6
|
+
module Rubydex
|
|
7
|
+
# A skill file loaded from disk. Skill files are Markdown documents with YAML frontmatter
|
|
8
|
+
# that declare a `name` and `description`. The body is the Markdown content after the frontmatter.
|
|
9
|
+
class Skill
|
|
10
|
+
#: String
|
|
11
|
+
attr_reader :name
|
|
12
|
+
|
|
13
|
+
#: String
|
|
14
|
+
attr_reader :description
|
|
15
|
+
|
|
16
|
+
#: String
|
|
17
|
+
attr_reader :body
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
#: (String path) -> Rubydex::Skill
|
|
21
|
+
def load(path)
|
|
22
|
+
from_content(File.read(path), path:)
|
|
23
|
+
rescue SystemCallError, IOError => e
|
|
24
|
+
raise SkillError, "Cannot read skill file #{path}: #{e.message}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
#: (String content, ?path: String?) -> Rubydex::Skill
|
|
28
|
+
def from_content(content, path: nil)
|
|
29
|
+
new(**parse(content, path:))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
#: (String content, path: String?) -> { name: String, description: String, body: String }
|
|
35
|
+
def parse(content, path:)
|
|
36
|
+
lines = content.lines
|
|
37
|
+
|
|
38
|
+
unless lines.first&.strip == "---"
|
|
39
|
+
raise SkillError, "Missing YAML frontmatter delimiter#{location(path)}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
closing_index = (1...lines.length).find { |i| lines[i].strip == "---" }
|
|
43
|
+
unless closing_index
|
|
44
|
+
raise SkillError, "Unterminated YAML frontmatter#{location(path)}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
frontmatter = lines[1...closing_index].join
|
|
48
|
+
body = lines[(closing_index + 1)..].join.strip
|
|
49
|
+
|
|
50
|
+
yaml = parse_frontmatter(frontmatter, path)
|
|
51
|
+
unless yaml.is_a?(Hash)
|
|
52
|
+
raise SkillError, "Empty or malformed YAML frontmatter#{location(path)}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
name = yaml["name"]
|
|
56
|
+
unless name.is_a?(String)
|
|
57
|
+
raise SkillError, "Missing or non-string `name` in frontmatter#{location(path)}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
description = yaml["description"]
|
|
61
|
+
unless description.is_a?(String)
|
|
62
|
+
raise SkillError, "Missing or non-string `description` in frontmatter#{location(path)}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
{ name:, description:, body: }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
#: (String frontmatter, String? path) -> untyped
|
|
69
|
+
def parse_frontmatter(frontmatter, path)
|
|
70
|
+
YAML.safe_load(frontmatter)
|
|
71
|
+
rescue Psych::Exception => e
|
|
72
|
+
raise SkillError, "Malformed YAML frontmatter#{location(path)}: #{e.message}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
#: (String?) -> String
|
|
76
|
+
def location(path)
|
|
77
|
+
path ? " in #{path}" : ""
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
#: (name: String, description: String, body: String) -> void
|
|
82
|
+
def initialize(name:, description:, body:)
|
|
83
|
+
@name = name.freeze
|
|
84
|
+
@description = description.freeze
|
|
85
|
+
@body = body.freeze
|
|
86
|
+
freeze
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
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"
|
|
@@ -25,3 +31,5 @@ require "rubydex/graph"
|
|
|
25
31
|
require "rubydex/declaration"
|
|
26
32
|
require "rubydex/signature"
|
|
27
33
|
require "rubydex/reference"
|
|
34
|
+
|
|
35
|
+
Dir.glob("#{__dir__}/rubydex/rules/**/*.rb").each { |file| require file }
|
|
@@ -0,0 +1,140 @@
|
|
|
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/`, including
|
|
13
|
+
# `lib/rubydex_linter/rules/` directories in nested gems.
|
|
14
|
+
# This rule does not report files in those directories that define no rule subclass.
|
|
15
|
+
class RuleStructure < CustomRule
|
|
16
|
+
include Helpers::SourceAccessHelpers
|
|
17
|
+
|
|
18
|
+
BASE_RULE_NAME = "Rubydex::Linter::CustomRule" #: String
|
|
19
|
+
RULE_NAMESPACE = "Rubydex::Linter::Rules" #: String
|
|
20
|
+
RULE_FILE_PATTERNS = [
|
|
21
|
+
"rubydex_linter/rules/**/*.rb",
|
|
22
|
+
"**/lib/rubydex_linter/rules/**/*.rb",
|
|
23
|
+
].freeze #: Array[String]
|
|
24
|
+
TEST_FILE_PATTERNS = ["test/**/*", "**/test/**/*"].freeze #: Array[String]
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# @override
|
|
28
|
+
#: -> singleton(Severity::Base)
|
|
29
|
+
def default_severity
|
|
30
|
+
Severity::Error
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @override
|
|
35
|
+
#: -> void
|
|
36
|
+
def lint
|
|
37
|
+
rules = child_classes(BASE_RULE_NAME)
|
|
38
|
+
rule_definitions_by_file = {} #: Hash[String, Hash[Rubydex::Class, Definition]]
|
|
39
|
+
|
|
40
|
+
rules.each do |rule|
|
|
41
|
+
rule_docs = []
|
|
42
|
+
|
|
43
|
+
rule.definitions.each do |rule_definition|
|
|
44
|
+
uri = rule_definition.document.uri
|
|
45
|
+
path = path_for_uri(uri)
|
|
46
|
+
next unless path_in_workspace?(path)
|
|
47
|
+
|
|
48
|
+
if rule_file?(path)
|
|
49
|
+
rule_definitions = (rule_definitions_by_file[uri] ||= {}) #: Hash[Rubydex::Class, Definition]
|
|
50
|
+
rule_definitions[rule] ||= rule_definition
|
|
51
|
+
elsif !test_file?(path)
|
|
52
|
+
report_wrong_rule_directory(rule.name, rule_definition)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
rule_docs.concat(rule_definition.comments)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
rule_definition = rule.definitions.find do |definition|
|
|
59
|
+
path = path_for_uri(definition.document.uri)
|
|
60
|
+
path_in_workspace?(path) && (rule_file?(path) || !test_file?(path))
|
|
61
|
+
end
|
|
62
|
+
next unless rule_definition
|
|
63
|
+
|
|
64
|
+
rule_name = rule.name
|
|
65
|
+
|
|
66
|
+
if rule_docs.empty?
|
|
67
|
+
add_diagnostic(
|
|
68
|
+
"`#{rule_name}` is missing documentation.",
|
|
69
|
+
diagnostic_location(rule_definition),
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
next if rule_name.start_with?("#{RULE_NAMESPACE}::")
|
|
74
|
+
|
|
75
|
+
report_wrong_rule_namespace(rule_name, rule_definition)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
rule_definitions_by_file.each do |uri, rule_definitions|
|
|
79
|
+
next if rule_definitions.length <= 1
|
|
80
|
+
|
|
81
|
+
add_diagnostic(
|
|
82
|
+
"Each rule file must define only one linter rule; found #{rule_definitions.length}.",
|
|
83
|
+
file_location(uri),
|
|
84
|
+
related_information: rule_definitions.map do |rule, rule_definition|
|
|
85
|
+
RelatedInformation.new(
|
|
86
|
+
"`#{rule.name}` is defined here.",
|
|
87
|
+
diagnostic_location(rule_definition),
|
|
88
|
+
)
|
|
89
|
+
end,
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
#: (String, Definition) -> void
|
|
97
|
+
def report_wrong_rule_directory(rule_name, rule_definition)
|
|
98
|
+
add_diagnostic(
|
|
99
|
+
"`#{rule_name}` must be defined under `rubydex_linter/rules/` or `lib/rubydex_linter/rules/`.",
|
|
100
|
+
diagnostic_location(rule_definition),
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
#: (String, Definition) -> void
|
|
105
|
+
def report_wrong_rule_namespace(rule_name, rule_definition)
|
|
106
|
+
add_diagnostic(
|
|
107
|
+
"`#{rule_name}` must be defined under `#{RULE_NAMESPACE}`.",
|
|
108
|
+
diagnostic_location(rule_definition),
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
#: (String) -> bool
|
|
113
|
+
def path_in_workspace?(path)
|
|
114
|
+
workspace = graph.workspace_path
|
|
115
|
+
path == workspace || path.start_with?("#{workspace}/")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
#: (String) -> bool
|
|
119
|
+
def rule_file?(path)
|
|
120
|
+
path_matches_patterns?(path, RULE_FILE_PATTERNS)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
#: (String) -> bool
|
|
124
|
+
def test_file?(path)
|
|
125
|
+
path_matches_patterns?(path, TEST_FILE_PATTERNS)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
#: (String, Array[String]) -> bool
|
|
129
|
+
def path_matches_patterns?(path, patterns)
|
|
130
|
+
Helpers::PathHelpers.path_matches_patterns?(
|
|
131
|
+
path,
|
|
132
|
+
patterns,
|
|
133
|
+
workspace: graph.workspace_path,
|
|
134
|
+
flags: Helpers::PathHelpers::EXCLUDE_FNMATCH_FLAGS,
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|