rubyzen-lint 0.1.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 (61) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/README.md +110 -0
  4. data/lib/rubyzen/cache/parse_cache.rb +36 -0
  5. data/lib/rubyzen/collections/attributes_collection.rb +32 -0
  6. data/lib/rubyzen/collections/base_collection.rb +30 -0
  7. data/lib/rubyzen/collections/blocks_collection.rb +27 -0
  8. data/lib/rubyzen/collections/call_site_collection.rb +52 -0
  9. data/lib/rubyzen/collections/classes_collection.rb +77 -0
  10. data/lib/rubyzen/collections/constants_collection.rb +11 -0
  11. data/lib/rubyzen/collections/declaration_collection.rb +11 -0
  12. data/lib/rubyzen/collections/file_collection.rb +81 -0
  13. data/lib/rubyzen/collections/macros_collection.rb +11 -0
  14. data/lib/rubyzen/collections/methods_collection.rb +67 -0
  15. data/lib/rubyzen/collections/modules_collection.rb +36 -0
  16. data/lib/rubyzen/collections/parameters_collection.rb +11 -0
  17. data/lib/rubyzen/collections/raises_collection.rb +26 -0
  18. data/lib/rubyzen/collections/requires_collection.rb +32 -0
  19. data/lib/rubyzen/collections/rescues_collection.rb +19 -0
  20. data/lib/rubyzen/declarations/attribute_declaration.rb +62 -0
  21. data/lib/rubyzen/declarations/block_declaration.rb +49 -0
  22. data/lib/rubyzen/declarations/call_site_declaration.rb +98 -0
  23. data/lib/rubyzen/declarations/class_declaration.rb +168 -0
  24. data/lib/rubyzen/declarations/constant_declaration.rb +155 -0
  25. data/lib/rubyzen/declarations/file_declaration.rb +69 -0
  26. data/lib/rubyzen/declarations/if_statement_declaration.rb +44 -0
  27. data/lib/rubyzen/declarations/macro_declaration.rb +81 -0
  28. data/lib/rubyzen/declarations/method_declaration.rb +63 -0
  29. data/lib/rubyzen/declarations/module_declaration.rb +115 -0
  30. data/lib/rubyzen/declarations/parameter_declaration.rb +43 -0
  31. data/lib/rubyzen/declarations/raise_declaration.rb +87 -0
  32. data/lib/rubyzen/declarations/require_declaration.rb +61 -0
  33. data/lib/rubyzen/declarations/rescue_declaration.rb +54 -0
  34. data/lib/rubyzen/lint.rb +1 -0
  35. data/lib/rubyzen/matchers/matcher_helpers.rb +176 -0
  36. data/lib/rubyzen/matchers/zen_empty_matcher.rb +54 -0
  37. data/lib/rubyzen/matchers/zen_false_matcher.rb +63 -0
  38. data/lib/rubyzen/matchers/zen_true_matcher.rb +57 -0
  39. data/lib/rubyzen/parsers/a_s_t_parser.rb +33 -0
  40. data/lib/rubyzen/project.rb +69 -0
  41. data/lib/rubyzen/providers/attributes_provider.rb +19 -0
  42. data/lib/rubyzen/providers/blocks_provider.rb +15 -0
  43. data/lib/rubyzen/providers/call_site_provider.rb +15 -0
  44. data/lib/rubyzen/providers/class_name_provider.rb +36 -0
  45. data/lib/rubyzen/providers/collection_filter_provider.rb +64 -0
  46. data/lib/rubyzen/providers/constants_provider.rb +17 -0
  47. data/lib/rubyzen/providers/file_path_provider.rb +26 -0
  48. data/lib/rubyzen/providers/if_statements_provider.rb +11 -0
  49. data/lib/rubyzen/providers/line_number_provider.rb +11 -0
  50. data/lib/rubyzen/providers/lines_of_code_provider.rb +11 -0
  51. data/lib/rubyzen/providers/macros_provider.rb +17 -0
  52. data/lib/rubyzen/providers/raises_provider.rb +19 -0
  53. data/lib/rubyzen/providers/requires_provider.rb +19 -0
  54. data/lib/rubyzen/providers/rescues_provider.rb +17 -0
  55. data/lib/rubyzen/providers/source_code_provider.rb +11 -0
  56. data/lib/rubyzen/providers/visibility_provider.rb +49 -0
  57. data/lib/rubyzen/version.rb +3 -0
  58. data/lib/rubyzen-lint.rb +1 -0
  59. data/lib/rubyzen.rb +98 -0
  60. data/rubyzen-lint.gemspec +28 -0
  61. metadata +148 -0
@@ -0,0 +1,81 @@
1
+
2
+ module Rubyzen
3
+ module Collections
4
+ # Collection of parsed file declarations. Serves as the top-level entry point
5
+ # for navigating into classes, modules, constants, and other code elements.
6
+ #
7
+ # @example Getting all controller classes
8
+ # project.files.with_paths('src/controllers/').classes
9
+ class FileCollection < BaseCollection
10
+ include Rubyzen::Providers::CollectionFilterProvider
11
+
12
+ # Filters files whose path includes any of the given substrings.
13
+ #
14
+ # @param paths [Array<String>] path substrings to match
15
+ # @return [FileCollection]
16
+ def with_paths(*paths)
17
+ filter do |file_declaration|
18
+ paths.any? { |p| file_declaration.path.include?(p) }
19
+ end
20
+ end
21
+
22
+ # Excludes files whose path includes any of the given substrings.
23
+ #
24
+ # @param paths [Array<String>] path substrings to exclude
25
+ # @return [FileCollection]
26
+ def without_paths(*paths)
27
+ filter do |file_declaration|
28
+ !paths.any? { |p| file_declaration.path.include?(p) }
29
+ end
30
+ end
31
+
32
+ # Returns all class declarations across every file.
33
+ #
34
+ # @return [ClassesCollection]
35
+ def classes
36
+ all_classes = flat_map(&:classes)
37
+ ClassesCollection.new(all_classes)
38
+ end
39
+
40
+ # Returns all module declarations across every file.
41
+ #
42
+ # @return [ModulesCollection]
43
+ def modules
44
+ all_modules = flat_map(&:modules)
45
+ ModulesCollection.new(all_modules)
46
+ end
47
+
48
+ # Returns all constant declarations across every file.
49
+ #
50
+ # @return [ConstantsCollection]
51
+ def constants
52
+ all_constants = flat_map(&:constants)
53
+ ConstantsCollection.new(all_constants)
54
+ end
55
+
56
+ # Returns all require/require_relative/load statements across every file.
57
+ #
58
+ # @return [RequiresCollection]
59
+ def requires
60
+ all_requires = flat_map(&:requires)
61
+ RequiresCollection.new(all_requires)
62
+ end
63
+
64
+ # Returns all call sites across every file.
65
+ #
66
+ # @return [CallSiteCollection]
67
+ def call_sites
68
+ all_call_sites = flat_map(&:call_sites)
69
+ CallSiteCollection.new(all_call_sites)
70
+ end
71
+
72
+ # Returns all block declarations across every file.
73
+ #
74
+ # @return [BlocksCollection]
75
+ def blocks
76
+ all_blocks = flat_map(&:blocks)
77
+ BlocksCollection.new(all_blocks)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,11 @@
1
+ module Rubyzen
2
+ module Collections
3
+ # Collection of class-level macro invocations (e.g., +validates+, +has_many+, +before_action+).
4
+ #
5
+ # @example Filtering macros by name
6
+ # controllers.macros.with_name('before_action')
7
+ class MacrosCollection < BaseCollection
8
+ include Rubyzen::Providers::CollectionFilterProvider
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,67 @@
1
+ module Rubyzen
2
+ module Collections
3
+ # Collection of method declarations with access to parameters,
4
+ # call sites, if statements, rescues, and raises within each method.
5
+ #
6
+ # @example Ensuring no method has more than 5 parameters
7
+ # controllers.all_methods.each { |m| expect(m.parameters.size).to be <= 5 }
8
+ class MethodsCollection < BaseCollection
9
+ include Rubyzen::Providers::CollectionFilterProvider
10
+
11
+ # Returns all parameters across every method.
12
+ #
13
+ # @return [ParametersCollection]
14
+ def parameters
15
+ ParametersCollection.new(
16
+ flat_map do |method|
17
+ method.parameters
18
+ end
19
+ )
20
+ end
21
+
22
+ # Returns all if-statement declarations across every method.
23
+ #
24
+ # @return [DeclarationCollection]
25
+ def if_statements
26
+ DeclarationCollection.new(
27
+ flat_map do |method|
28
+ method.if_statements
29
+ end
30
+ )
31
+ end
32
+
33
+ # Returns all call sites across every method.
34
+ #
35
+ # @return [CallSiteCollection]
36
+ def call_sites
37
+ CallSiteCollection.new(
38
+ flat_map do |method|
39
+ method.call_sites
40
+ end
41
+ )
42
+ end
43
+
44
+ # Returns all rescue declarations across every method.
45
+ #
46
+ # @return [RescuesCollection]
47
+ def rescues
48
+ RescuesCollection.new(
49
+ flat_map do |method|
50
+ method.rescues
51
+ end
52
+ )
53
+ end
54
+
55
+ # Returns all raise declarations across every method.
56
+ #
57
+ # @return [RaisesCollection]
58
+ def raises
59
+ RaisesCollection.new(
60
+ flat_map do |method|
61
+ method.raises
62
+ end
63
+ )
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,36 @@
1
+ module Rubyzen
2
+ module Collections
3
+ # Collection of module declarations with methods for navigating into
4
+ # child elements (methods, classes, constants).
5
+ #
6
+ # @example Getting all methods defined in modules
7
+ # project.files.modules.all_methods
8
+ class ModulesCollection < BaseCollection
9
+ include Rubyzen::Providers::CollectionFilterProvider
10
+
11
+ # Returns all methods defined across every module.
12
+ #
13
+ # @return [MethodsCollection]
14
+ def all_methods
15
+ all_methods = flat_map(&:all_methods)
16
+ MethodsCollection.new(all_methods)
17
+ end
18
+
19
+ # Returns all class declarations nested inside every module.
20
+ #
21
+ # @return [ClassesCollection]
22
+ def classes
23
+ all_classes = flat_map(&:classes)
24
+ ClassesCollection.new(all_classes)
25
+ end
26
+
27
+ # Returns all constant declarations across every module.
28
+ #
29
+ # @return [ConstantsCollection]
30
+ def constants
31
+ all_constants = flat_map(&:constants)
32
+ ConstantsCollection.new(all_constants)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,11 @@
1
+ module Rubyzen
2
+ module Collections
3
+ # Collection of method parameter declarations.
4
+ #
5
+ # @example Filtering parameters by name
6
+ # controllers.all_methods.parameters.with_name('id')
7
+ class ParametersCollection < BaseCollection
8
+ include Rubyzen::Providers::CollectionFilterProvider
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,26 @@
1
+ module Rubyzen
2
+ module Collections
3
+ # Collection of raise declarations found in methods or classes.
4
+ #
5
+ # @example Ensuring no plain-string raises in controllers
6
+ # expect(controllers.raises.with_string).to zen_empty
7
+ class RaisesCollection < BaseCollection
8
+ # Filters raises that use a plain string message (not an exception class).
9
+ #
10
+ # @return [RaisesCollection]
11
+ def with_string
12
+ filter(&:with_string?)
13
+ end
14
+
15
+ # Filters raises that include the given exception class.
16
+ #
17
+ # @param exception_class [String] the exception class name to match
18
+ # @return [RaisesCollection]
19
+ def with_exception_type(exception_class)
20
+ filter do |raise_declaration|
21
+ raise_declaration.exception_types.include?(exception_class)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,32 @@
1
+ module Rubyzen
2
+ module Collections
3
+ # Collection of require/require_relative/load statements found in files.
4
+ #
5
+ # @example Ensuring controllers do not use require_relative
6
+ # expect(controller_files.requires.require_relative_calls).to zen_empty
7
+ class RequiresCollection < BaseCollection
8
+ include Rubyzen::Providers::CollectionFilterProvider
9
+
10
+ # Returns only +require+ calls.
11
+ #
12
+ # @return [RequiresCollection]
13
+ def require_calls
14
+ filter(&:require?)
15
+ end
16
+
17
+ # Returns only +require_relative+ calls.
18
+ #
19
+ # @return [RequiresCollection]
20
+ def require_relative_calls
21
+ filter(&:require_relative?)
22
+ end
23
+
24
+ # Returns only +load+ calls.
25
+ #
26
+ # @return [RequiresCollection]
27
+ def load_calls
28
+ filter(&:load?)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,19 @@
1
+ module Rubyzen
2
+ module Collections
3
+ # Collection of rescue declarations found in methods or classes.
4
+ #
5
+ # @example Checking for StandardError rescues
6
+ # project.files.classes.all_methods.rescues.with_exception_type('StandardError')
7
+ class RescuesCollection < BaseCollection
8
+ # Filters rescues that handle the given exception class.
9
+ #
10
+ # @param exception_class [String] the exception class name to match
11
+ # @return [RescuesCollection]
12
+ def with_exception_type(exception_class)
13
+ filter do |rescue_declaration|
14
+ rescue_declaration.exception_types.include?(exception_class)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,62 @@
1
+ module Rubyzen
2
+ module Declarations
3
+ # Represents an +attr_reader+, +attr_writer+, or +attr_accessor+ declaration.
4
+ #
5
+ # @example
6
+ # attr = klass.attributes.first
7
+ # attr.name #=> "attr_reader"
8
+ # attr.symbols #=> ["name", "email"]
9
+ # attr.reader? #=> true
10
+ # attr.private? #=> false
11
+ #
12
+ class AttributeDeclaration
13
+ include Rubyzen::Providers::FilePathProvider
14
+ include Rubyzen::Providers::ClassNameProvider
15
+ include Rubyzen::Providers::LineNumberProvider
16
+ include Rubyzen::Providers::VisibilityProvider
17
+
18
+ # @return [RuboCop::AST::Node]
19
+ attr_reader :node
20
+
21
+ # @return [ClassDeclaration, ModuleDeclaration]
22
+ attr_reader :parent_class
23
+ alias :parent :parent_class
24
+
25
+ # @param node [RuboCop::AST::Node] the AST node
26
+ # @param parent_class [ClassDeclaration, ModuleDeclaration] the parent declaration
27
+ def initialize(node, parent_class)
28
+ @node = node
29
+ @parent_class = parent_class
30
+ end
31
+
32
+ # Returns the attribute type name.
33
+ #
34
+ # @return [String] one of +"attr_reader"+, +"attr_writer"+, +"attr_accessor"+
35
+ def name
36
+ node.method_name.to_s
37
+ end
38
+
39
+ # Returns the declared symbol names.
40
+ #
41
+ # @return [Array<String>] e.g. +["name", "email"]+
42
+ def symbols
43
+ node.arguments.map { |arg| arg.value.to_s if arg.type == :sym }.compact
44
+ end
45
+
46
+ # @return [Boolean] true for +attr_reader+ and +attr_accessor+
47
+ def reader?
48
+ %w[attr_reader attr_accessor].include?(name)
49
+ end
50
+
51
+ # @return [Boolean] true for +attr_writer+ and +attr_accessor+
52
+ def writer?
53
+ %w[attr_writer attr_accessor].include?(name)
54
+ end
55
+
56
+ # @return [Boolean] true only for +attr_accessor+
57
+ def accessor?
58
+ name == 'attr_accessor'
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,49 @@
1
+ module Rubyzen
2
+ module Declarations
3
+ # Represents a Ruby block (+do...end+ or +{ }+).
4
+ #
5
+ # @example
6
+ # block = method.blocks.first
7
+ # block.method_name #=> "each"
8
+ # block.call_sites #=> CallSiteCollection
9
+ # block.lines_of_code #=> 5
10
+ #
11
+ class BlockDeclaration
12
+ include Rubyzen::Providers::FilePathProvider
13
+ include Rubyzen::Providers::LineNumberProvider
14
+ include Rubyzen::Providers::ClassNameProvider
15
+ include Rubyzen::Providers::LinesOfCodeProvider
16
+ include Rubyzen::Providers::RescuesProvider
17
+ include Rubyzen::Providers::RaisesProvider
18
+ include Rubyzen::Providers::SourceCodeProvider
19
+ include Rubyzen::Providers::CallSiteProvider
20
+
21
+ # @return [RuboCop::AST::Node]
22
+ attr_reader :node
23
+
24
+ # @return [MethodDeclaration, FileDeclaration]
25
+ attr_reader :parent
26
+
27
+ # @param node [RuboCop::AST::Node] the AST node
28
+ # @param parent [MethodDeclaration, FileDeclaration] the parent declaration
29
+ def initialize(node, parent)
30
+ @node = node
31
+ @parent = parent
32
+ end
33
+
34
+ # Returns the method name the block is passed to. Alias for {#method_name}.
35
+ #
36
+ # @return [String]
37
+ def name
38
+ method_name
39
+ end
40
+
41
+ # Returns the method name the block is passed to.
42
+ #
43
+ # @return [String] e.g. +"each"+, +"map"+, +"let"+
44
+ def method_name
45
+ node.method_name.to_s
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,98 @@
1
+ module Rubyzen
2
+ module Declarations
3
+ # Represents a method call site (a +send+ node in the AST).
4
+ #
5
+ # @example
6
+ # call_site = method.call_sites.first
7
+ # call_site.method_name #=> "find"
8
+ # call_site.receiver #=> "User"
9
+ # call_site.keyword_args #=> [:id, :name]
10
+ #
11
+ class CallSiteDeclaration
12
+ include Rubyzen::Providers::FilePathProvider
13
+ include Rubyzen::Providers::LineNumberProvider
14
+ include Rubyzen::Providers::ClassNameProvider
15
+ include Rubyzen::Providers::SourceCodeProvider
16
+
17
+ # @return [RuboCop::AST::Node]
18
+ attr_reader :node
19
+
20
+ # @return [MethodDeclaration, BlockDeclaration, FileDeclaration]
21
+ attr_reader :parent
22
+
23
+ # @param node [RuboCop::AST::Node] the AST node
24
+ # @param parent [MethodDeclaration, BlockDeclaration, FileDeclaration] the parent declaration
25
+ def initialize(node, parent)
26
+ @node = node
27
+ @parent = parent
28
+ end
29
+
30
+ # Returns the called method name. Alias for {#method_name}.
31
+ #
32
+ # @return [String]
33
+ def name
34
+ method_name
35
+ end
36
+
37
+ # Returns the constant name of the receiver, if any.
38
+ #
39
+ # @return [String, nil] e.g. +"User"+ for +User.find(1)+, +nil+ for +save+
40
+ def receiver
41
+ node.receiver&.type == :const ? node.receiver.const_name : nil
42
+ end
43
+
44
+ # Returns the called method name.
45
+ #
46
+ # @return [String]
47
+ def method_name
48
+ node.method_name.to_s
49
+ end
50
+
51
+ # Returns the keyword argument keys passed in the call.
52
+ #
53
+ # @return [Array<Symbol>] e.g. +[:level, :details]+
54
+ def keyword_args
55
+ node.arguments.flat_map do |arg|
56
+ next [] unless arg.hash_type?
57
+
58
+ arg.pairs.filter_map do |pair|
59
+ pair.key.value if pair.key.type == :sym
60
+ end
61
+ end.uniq
62
+ end
63
+
64
+ # Returns a hash mapping keyword argument keys to their literal values.
65
+ #
66
+ # @return [Hash{Symbol => Object}] values are +nil+ for non-literal expressions
67
+ def keyword_arg_value_pairs
68
+ result = {}
69
+ node.arguments.each do |arg|
70
+ next unless arg.hash_type?
71
+
72
+ arg.pairs.each do |pair|
73
+ next unless pair.key.type == :sym
74
+
75
+ value_node = pair.value
76
+ result[pair.key.value] = value_node.respond_to?(:value) ? value_node.value : nil
77
+ end
78
+ end
79
+ result
80
+ end
81
+
82
+ # Returns positional symbol arguments.
83
+ #
84
+ # @return [Array<Symbol>] e.g. +[:name, :email]+
85
+ def symbols
86
+ node.arguments.select { |arg| arg.type == :sym }.map(&:value)
87
+ end
88
+
89
+ # Returns positional string arguments.
90
+ #
91
+ # @return [Array<String>]
92
+ def strings
93
+ node.arguments.select { |arg| arg.type == :str }.map(&:value)
94
+ end
95
+
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,168 @@
1
+ module Rubyzen
2
+ module Declarations
3
+ # Represents a Ruby class definition. Provides access to methods, attributes,
4
+ # macros, and other class-level constructs.
5
+ #
6
+ # @example
7
+ # klass = file.classes.first
8
+ # klass.name #=> "Admin::UsersController"
9
+ # klass.superclass_name #=> "ApplicationController"
10
+ # klass.instance_methods #=> MethodsCollection
11
+ #
12
+ class ClassDeclaration
13
+ include Rubyzen::Providers::IfStatementsProvider
14
+ include Rubyzen::Providers::BlocksProvider
15
+ include Rubyzen::Providers::FilePathProvider
16
+ include Rubyzen::Providers::LineNumberProvider
17
+ include Rubyzen::Providers::LinesOfCodeProvider
18
+ include Rubyzen::Providers::ClassNameProvider
19
+ include Rubyzen::Providers::ConstantsProvider
20
+ include Rubyzen::Providers::AttributesProvider
21
+ include Rubyzen::Providers::MacrosProvider
22
+ include Rubyzen::Providers::RescuesProvider
23
+ include Rubyzen::Providers::RaisesProvider
24
+
25
+ # @return [RuboCop::AST::Node] the class AST node
26
+ attr_reader :node
27
+
28
+ # @return [FileDeclaration] the file this class belongs to
29
+ attr_reader :file_declaration
30
+
31
+ # @param node [RuboCop::AST::Node]
32
+ # @param file_declaration [FileDeclaration]
33
+ def initialize(node, file_declaration)
34
+ @node = node
35
+ @file_declaration = file_declaration
36
+ end
37
+
38
+ # Returns the fully-qualified class name including parent modules.
39
+ #
40
+ # @return [String] e.g. +"Admin::UsersController"+
41
+ def name
42
+ parent_module_names = []
43
+ current_node = node.parent
44
+
45
+ while current_node
46
+ if current_node.type == :module
47
+ parent_module_names.unshift(current_node.identifier&.const_name)
48
+ end
49
+ current_node = current_node.parent
50
+ end
51
+
52
+ [parent_module_names, name_without_modules].flatten.compact.join('::')
53
+ end
54
+
55
+ # Returns the class name without module prefixes.
56
+ #
57
+ # @return [String] e.g. +"UsersController"+
58
+ def name_without_modules
59
+ node.identifier&.const_name
60
+ end
61
+
62
+ # Returns the superclass name, if any.
63
+ #
64
+ # @return [String, nil] e.g. +"ApplicationController"+
65
+ def superclass_name
66
+ super_node = node.children[1]
67
+ return nil unless super_node&.type == :const
68
+
69
+ super_node.const_name
70
+ end
71
+
72
+ # Checks whether the superclass name starts with the given prefix.
73
+ #
74
+ # @param prefix [String]
75
+ # @return [Boolean]
76
+ def superclass_prefix?(prefix)
77
+ superclass_name&.start_with?(prefix)
78
+ end
79
+
80
+ # Returns instance methods defined directly in this class.
81
+ #
82
+ # @return [Collections::MethodsCollection]
83
+ def instance_methods
84
+ Collections::MethodsCollection.new(
85
+ instance_method_nodes.map do |def_node|
86
+ MethodDeclaration.new(def_node, self)
87
+ end
88
+ )
89
+ end
90
+
91
+ # Returns class methods (both +self.method+ and +class << self+ styles).
92
+ #
93
+ # @return [Collections::MethodsCollection]
94
+ def class_methods
95
+ Collections::MethodsCollection.new(
96
+ class_method_nodes.map do |method_node|
97
+ MethodDeclaration.new(method_node, self)
98
+ end
99
+ )
100
+ end
101
+
102
+ # Returns unique method names called anywhere in this class.
103
+ #
104
+ # @return [Array<String>]
105
+ def called_method_names
106
+ node.each_descendant(:send).map { |send_node| send_node.method_name.to_s }.uniq
107
+ end
108
+
109
+ # Returns the top-level module name from the enclosing file.
110
+ #
111
+ # @return [String, nil]
112
+ def top_level_module
113
+ file_declaration.top_level_module_name
114
+ end
115
+
116
+ private
117
+
118
+ def class_body_node
119
+ node.children[2]
120
+ end
121
+
122
+ def class_body_children
123
+ body = class_body_node
124
+ return [] unless body
125
+
126
+ body.type == :begin ? body.child_nodes : [body]
127
+ end
128
+
129
+ def instance_method_nodes
130
+ class_body_children.select { |child| child.type == :def }
131
+ end
132
+
133
+ def class_defs_nodes
134
+ class_body_children.select do |child|
135
+ child.type == :defs && child.children[0]&.type == :self
136
+ end
137
+ end
138
+
139
+ def class_method_nodes
140
+ class_defs_nodes + class_sclass_def_nodes
141
+ end
142
+
143
+ def class_sclass_def_nodes
144
+ class_body_children
145
+ .select { |child| singleton_class_node?(child) }
146
+ .flat_map do |child|
147
+ body_children(child.children[1]).select do |body_child|
148
+ method_node?(body_child)
149
+ end
150
+ end
151
+ end
152
+
153
+ def singleton_class_node?(child)
154
+ child.type == :sclass && child.children[0]&.type == :self
155
+ end
156
+
157
+ def body_children(body)
158
+ return [] unless body
159
+
160
+ body.type == :begin ? body.child_nodes : [body]
161
+ end
162
+
163
+ def method_node?(child)
164
+ %i[def defs].include?(child.type)
165
+ end
166
+ end
167
+ end
168
+ end