solid_score 0.2.0 → 0.3.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/lib/rubocop/cop/solid_score/helpers.rb +1 -1
- data/lib/solid_score/parser/class_body_scanner.rb +129 -0
- data/lib/solid_score/parser/constant_name.rb +23 -0
- data/lib/solid_score/parser/definition_collector.rb +56 -0
- data/lib/solid_score/parser/method_analyzer.rb +248 -0
- data/lib/solid_score/parser/ruby_parser.rb +15 -435
- data/lib/solid_score/runner.rb +1 -1
- data/lib/solid_score/version.rb +1 -1
- data/lib/solid_score.rb +4 -0
- metadata +9 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7ee2dc0b2b384805ab33e1feb4278ff65f196763453e98747536d3dad3cebbb4
|
|
4
|
+
data.tar.gz: 9dd4b65bad2542e7255f681c152eccf6e8005ed6af18b2a6e25884da2e1c3cce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 86f7674c77418367188bcf18b695c7c46116d4efe87db725e583b19b0df8626b7431dfe543b1060781953350b1ac9757165d291db298fcac7043fc48046a0db6
|
|
7
|
+
data.tar.gz: ec1b3636db05fa9f5f3c47d5a535ebc4fe18d6fdcf7f64ac545c921f773a6ccd01fef4a7fe46766c016d0dd07fc9cec9bde66847f6df1064c7913b29e11d97fc
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module SolidScore
|
|
6
|
+
module Parser
|
|
7
|
+
# Builds a Models::ClassInfo from a single Prism ClassNode/ModuleNode by
|
|
8
|
+
# scanning its direct body statements: method definitions, visibility
|
|
9
|
+
# modifiers, mixins, attr_* declarations and Rails DSL calls.
|
|
10
|
+
class ClassBodyScanner
|
|
11
|
+
# Rails/ActiveSupport DSL methods treated as declarations rather than
|
|
12
|
+
# ordinary method calls.
|
|
13
|
+
RAILS_DSL_METHODS = %i[
|
|
14
|
+
has_many has_one belongs_to has_and_belongs_to_many
|
|
15
|
+
validates validate validates_presence_of validates_uniqueness_of
|
|
16
|
+
validates_format_of validates_length_of validates_numericality_of
|
|
17
|
+
validates_inclusion_of validates_exclusion_of validates_associated
|
|
18
|
+
before_action after_action around_action skip_before_action
|
|
19
|
+
before_filter after_filter around_filter
|
|
20
|
+
before_validation after_validation
|
|
21
|
+
before_create after_create before_update after_update
|
|
22
|
+
before_save after_save before_destroy after_destroy
|
|
23
|
+
after_commit after_rollback
|
|
24
|
+
scope enum delegate
|
|
25
|
+
serialize store
|
|
26
|
+
accepts_nested_attributes_for
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
VISIBILITY_MODIFIERS = %i[private protected public].freeze
|
|
30
|
+
|
|
31
|
+
# Direct statements of a class/module body. The body is a
|
|
32
|
+
# StatementsNode normally, but a BeginNode when the class itself has
|
|
33
|
+
# a rescue/ensure clause, in which case the statements sit inside it.
|
|
34
|
+
def self.body_statements(node)
|
|
35
|
+
body = node.body
|
|
36
|
+
body = body.statements if body.is_a?(::Prism::BeginNode)
|
|
37
|
+
body&.body || []
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def initialize(node, file_path, namespace)
|
|
41
|
+
@node = node
|
|
42
|
+
@file_path = file_path
|
|
43
|
+
@namespace = namespace
|
|
44
|
+
@methods = []
|
|
45
|
+
@includes = []
|
|
46
|
+
@extends = []
|
|
47
|
+
@attr_readers = []
|
|
48
|
+
@attr_writers = []
|
|
49
|
+
@dsl_calls = []
|
|
50
|
+
@visibility = :public
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def class_info
|
|
54
|
+
statements.each { |statement| scan(statement) }
|
|
55
|
+
|
|
56
|
+
Models::ClassInfo.new(
|
|
57
|
+
name: qualified_name,
|
|
58
|
+
file_path: @file_path,
|
|
59
|
+
line_start: @node.location.start_line,
|
|
60
|
+
line_end: @node.location.end_line,
|
|
61
|
+
methods: @methods,
|
|
62
|
+
superclass: superclass_name,
|
|
63
|
+
includes: @includes,
|
|
64
|
+
extends: @extends,
|
|
65
|
+
instance_variables: @methods.flat_map(&:instance_variables).uniq,
|
|
66
|
+
attr_readers: @attr_readers,
|
|
67
|
+
attr_writers: @attr_writers,
|
|
68
|
+
kind: @node.is_a?(::Prism::ModuleNode) ? :module : :class,
|
|
69
|
+
dsl_calls: @dsl_calls
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def statements
|
|
76
|
+
self.class.body_statements(@node)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def qualified_name
|
|
80
|
+
raw = ConstantName.of(@node.constant_path)
|
|
81
|
+
@namespace ? "#{@namespace}::#{raw}" : raw
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def superclass_name
|
|
85
|
+
return nil unless @node.is_a?(::Prism::ClassNode)
|
|
86
|
+
|
|
87
|
+
ConstantName.of(@node.superclass)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def scan(statement)
|
|
91
|
+
case statement
|
|
92
|
+
when ::Prism::DefNode then scan_def(statement)
|
|
93
|
+
when ::Prism::CallNode then scan_call(statement)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def scan_def(node)
|
|
98
|
+
@methods << if node.receiver
|
|
99
|
+
MethodAnalyzer.new(node, visibility: :public, kind: :class).method_info
|
|
100
|
+
else
|
|
101
|
+
MethodAnalyzer.new(node, visibility: @visibility, kind: :instance).method_info
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def scan_call(node)
|
|
106
|
+
name = node.name
|
|
107
|
+
args = node.arguments&.arguments || []
|
|
108
|
+
|
|
109
|
+
case name
|
|
110
|
+
when *VISIBILITY_MODIFIERS then @visibility = name
|
|
111
|
+
when :include then @includes << ConstantName.of(args.first) if args.first
|
|
112
|
+
when :extend then @extends << ConstantName.of(args.first) if args.first
|
|
113
|
+
when :attr_reader then @attr_readers.concat(symbol_args(args))
|
|
114
|
+
when :attr_writer then @attr_writers.concat(symbol_args(args))
|
|
115
|
+
when :attr_accessor
|
|
116
|
+
names = symbol_args(args)
|
|
117
|
+
@attr_readers.concat(names)
|
|
118
|
+
@attr_writers.concat(names)
|
|
119
|
+
else
|
|
120
|
+
@dsl_calls << name if RAILS_DSL_METHODS.include?(name)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def symbol_args(args)
|
|
125
|
+
args.grep(::Prism::SymbolNode).map { |arg| arg.unescaped.to_sym }
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module SolidScore
|
|
6
|
+
module Parser
|
|
7
|
+
# Renders a Prism constant node as its qualified name ("Foo::Bar").
|
|
8
|
+
# Non-constant expressions (e.g. `Struct.new(:a)` as a superclass)
|
|
9
|
+
# fall back to their source text so callers still get a stable string.
|
|
10
|
+
module ConstantName
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def of(node)
|
|
14
|
+
case node
|
|
15
|
+
when nil then nil
|
|
16
|
+
when ::Prism::ConstantReadNode then node.name.to_s
|
|
17
|
+
when ::Prism::ConstantPathNode then "#{of(node.parent)}::#{node.name}"
|
|
18
|
+
else node.slice
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module SolidScore
|
|
6
|
+
module Parser
|
|
7
|
+
# Walks a Prism syntax tree and collects every class/module definition
|
|
8
|
+
# as Models::ClassInfo, qualifying nested names with their namespace.
|
|
9
|
+
#
|
|
10
|
+
# Top-level definitions are found anywhere in the tree (e.g. inside an
|
|
11
|
+
# `if defined?(...)` guard). Inside a class/module body only direct
|
|
12
|
+
# child definitions are collected, matching the previous parser.
|
|
13
|
+
class DefinitionCollector < ::Prism::Visitor
|
|
14
|
+
attr_reader :classes
|
|
15
|
+
|
|
16
|
+
def initialize(file_path)
|
|
17
|
+
super()
|
|
18
|
+
@file_path = file_path
|
|
19
|
+
@namespace = nil
|
|
20
|
+
@classes = []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def visit_class_node(node)
|
|
24
|
+
collect_definition(node)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def visit_module_node(node)
|
|
28
|
+
collect_definition(node)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def collect_definition(node)
|
|
34
|
+
info = ClassBodyScanner.new(node, @file_path, @namespace).class_info
|
|
35
|
+
@classes << info
|
|
36
|
+
collect_nested(node, info.name)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def collect_nested(node, namespace)
|
|
40
|
+
with_namespace(namespace) do
|
|
41
|
+
ClassBodyScanner.body_statements(node)
|
|
42
|
+
.select { |child| child.is_a?(::Prism::ClassNode) || child.is_a?(::Prism::ModuleNode) }
|
|
43
|
+
.each { |child| visit(child) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def with_namespace(namespace)
|
|
48
|
+
previous = @namespace
|
|
49
|
+
@namespace = namespace
|
|
50
|
+
yield
|
|
51
|
+
ensure
|
|
52
|
+
@namespace = previous
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module SolidScore
|
|
6
|
+
module Parser
|
|
7
|
+
# Builds a Models::MethodInfo from a Prism DefNode by walking the method
|
|
8
|
+
# body once and collecting every metric the analyzers need.
|
|
9
|
+
class MethodAnalyzer
|
|
10
|
+
# Factory-style method names that count as instantiation for the
|
|
11
|
+
# `@ivar ||= Const.new` memoisation pattern (Issue #12).
|
|
12
|
+
# Mirrors DipAnalyzer::FACTORY_METHODS.
|
|
13
|
+
FACTORY_METHODS = %i[new create build call open].freeze
|
|
14
|
+
|
|
15
|
+
# Instance variable nodes: reads, every assignment form, and
|
|
16
|
+
# multiple-assignment targets (`@a, @b = ...`).
|
|
17
|
+
INSTANCE_VARIABLE_NODES = [
|
|
18
|
+
::Prism::InstanceVariableReadNode,
|
|
19
|
+
::Prism::InstanceVariableWriteNode,
|
|
20
|
+
::Prism::InstanceVariableOrWriteNode,
|
|
21
|
+
::Prism::InstanceVariableAndWriteNode,
|
|
22
|
+
::Prism::InstanceVariableOperatorWriteNode,
|
|
23
|
+
::Prism::InstanceVariableTargetNode
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
# Statement-like nodes that count toward the effective body size
|
|
27
|
+
# (Issue #10). Calls, conditionals, assignments, loops, jumps and
|
|
28
|
+
# blocks each count once; `||=`-style operator assignments count as
|
|
29
|
+
# both the assignment and the operator (see OPERATOR_ASSIGNMENT_NODES).
|
|
30
|
+
#
|
|
31
|
+
# Deliberately not counted: `case/in` pattern matching, `begin`,
|
|
32
|
+
# `rescue`/`ensure` wrappers, `when` clauses (the enclosing `case`
|
|
33
|
+
# already counts) and variable reads.
|
|
34
|
+
EFFECTIVE_STATEMENT_NODES = [
|
|
35
|
+
::Prism::CallNode,
|
|
36
|
+
::Prism::IfNode, ::Prism::UnlessNode, ::Prism::CaseNode, ::Prism::ReturnNode,
|
|
37
|
+
::Prism::InstanceVariableWriteNode, ::Prism::LocalVariableWriteNode,
|
|
38
|
+
::Prism::GlobalVariableWriteNode, ::Prism::ClassVariableWriteNode,
|
|
39
|
+
::Prism::MultiWriteNode,
|
|
40
|
+
::Prism::InstanceVariableTargetNode, ::Prism::LocalVariableTargetNode,
|
|
41
|
+
::Prism::GlobalVariableTargetNode, ::Prism::ClassVariableTargetNode,
|
|
42
|
+
::Prism::CallTargetNode, ::Prism::IndexTargetNode,
|
|
43
|
+
::Prism::ConstantOrWriteNode, ::Prism::ConstantAndWriteNode, ::Prism::ConstantOperatorWriteNode,
|
|
44
|
+
::Prism::ConstantPathOrWriteNode, ::Prism::ConstantPathAndWriteNode, ::Prism::ConstantPathOperatorWriteNode,
|
|
45
|
+
::Prism::YieldNode, ::Prism::BlockNode, ::Prism::LambdaNode,
|
|
46
|
+
::Prism::WhileNode, ::Prism::UntilNode, ::Prism::ForNode,
|
|
47
|
+
::Prism::NextNode, ::Prism::BreakNode, ::Prism::RetryNode, ::Prism::RedoNode,
|
|
48
|
+
::Prism::SuperNode, ::Prism::ForwardingSuperNode
|
|
49
|
+
].freeze
|
|
50
|
+
|
|
51
|
+
# `x ||= v`, `x &&= v`, `x += v` on variables and attributes. Counted
|
|
52
|
+
# twice so the effective statement count matches the previous parser,
|
|
53
|
+
# which saw these as an assignment node wrapping an operator node.
|
|
54
|
+
# The attribute and index forms also read the target (`self.a ||= v`
|
|
55
|
+
# calls `a`), so they are recorded as method calls too.
|
|
56
|
+
OPERATOR_ASSIGNMENT_NODES = [
|
|
57
|
+
::Prism::LocalVariableOrWriteNode, ::Prism::LocalVariableAndWriteNode,
|
|
58
|
+
::Prism::LocalVariableOperatorWriteNode,
|
|
59
|
+
::Prism::InstanceVariableOrWriteNode, ::Prism::InstanceVariableAndWriteNode,
|
|
60
|
+
::Prism::InstanceVariableOperatorWriteNode,
|
|
61
|
+
::Prism::GlobalVariableOrWriteNode, ::Prism::GlobalVariableAndWriteNode,
|
|
62
|
+
::Prism::GlobalVariableOperatorWriteNode,
|
|
63
|
+
::Prism::ClassVariableOrWriteNode, ::Prism::ClassVariableAndWriteNode,
|
|
64
|
+
::Prism::ClassVariableOperatorWriteNode,
|
|
65
|
+
::Prism::CallOrWriteNode, ::Prism::CallAndWriteNode, ::Prism::CallOperatorWriteNode,
|
|
66
|
+
::Prism::IndexOrWriteNode, ::Prism::IndexAndWriteNode, ::Prism::IndexOperatorWriteNode
|
|
67
|
+
].freeze
|
|
68
|
+
|
|
69
|
+
ATTRIBUTE_ASSIGNMENT_NODES = [
|
|
70
|
+
::Prism::CallOrWriteNode, ::Prism::CallAndWriteNode, ::Prism::CallOperatorWriteNode
|
|
71
|
+
].freeze
|
|
72
|
+
|
|
73
|
+
INDEX_ASSIGNMENT_NODES = [
|
|
74
|
+
::Prism::IndexOrWriteNode, ::Prism::IndexAndWriteNode, ::Prism::IndexOperatorWriteNode
|
|
75
|
+
].freeze
|
|
76
|
+
|
|
77
|
+
# Branching nodes that add one to cyclomatic complexity. Rescue is
|
|
78
|
+
# handled separately in visit_begin_node so that a multi-clause rescue
|
|
79
|
+
# counts once, as the previous parser did.
|
|
80
|
+
BRANCH_NODES = [
|
|
81
|
+
::Prism::IfNode, ::Prism::UnlessNode, ::Prism::WhileNode, ::Prism::UntilNode,
|
|
82
|
+
::Prism::ForNode, ::Prism::WhenNode, ::Prism::AndNode, ::Prism::OrNode,
|
|
83
|
+
::Prism::RescueModifierNode
|
|
84
|
+
].freeze
|
|
85
|
+
|
|
86
|
+
def initialize(node, visibility:, kind:)
|
|
87
|
+
@node = node
|
|
88
|
+
@visibility = visibility
|
|
89
|
+
@kind = kind
|
|
90
|
+
@instance_variables = []
|
|
91
|
+
@called_methods = []
|
|
92
|
+
@method_calls = []
|
|
93
|
+
@raises = []
|
|
94
|
+
@calls_super = false
|
|
95
|
+
@case_when_count = 0
|
|
96
|
+
@cyclomatic_complexity = 1
|
|
97
|
+
@effective_statement_count = 0
|
|
98
|
+
@memoized_factory_receiver = nil
|
|
99
|
+
@nested_definition_depth = 0
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def method_info
|
|
103
|
+
walk(@node.body)
|
|
104
|
+
|
|
105
|
+
Models::MethodInfo.new(
|
|
106
|
+
name: @node.name,
|
|
107
|
+
visibility: @visibility,
|
|
108
|
+
line_start: @node.location.start_line,
|
|
109
|
+
line_end: @node.location.end_line,
|
|
110
|
+
instance_variables: @instance_variables.uniq,
|
|
111
|
+
called_methods: @called_methods.uniq,
|
|
112
|
+
parameters: parameters,
|
|
113
|
+
cyclomatic_complexity: @cyclomatic_complexity,
|
|
114
|
+
raises: @raises,
|
|
115
|
+
calls_super: @calls_super,
|
|
116
|
+
method_calls: @method_calls,
|
|
117
|
+
case_when_count: @case_when_count,
|
|
118
|
+
kind: @kind,
|
|
119
|
+
effective_statement_count: @effective_statement_count,
|
|
120
|
+
memoized_factory_receiver: @memoized_factory_receiver
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
# Pre-order walk. Generic bookkeeping first, then node-specific hooks,
|
|
127
|
+
# then recurse into children. A plain recursion is used instead of
|
|
128
|
+
# Prism::Visitor because every visit_* there iterates children itself,
|
|
129
|
+
# leaving no single place to hook all nodes.
|
|
130
|
+
def walk(node)
|
|
131
|
+
return unless node
|
|
132
|
+
|
|
133
|
+
record_instance_variable(node)
|
|
134
|
+
record_effective_statement(node)
|
|
135
|
+
@cyclomatic_complexity += 1 if BRANCH_NODES.any? { |type| node.is_a?(type) }
|
|
136
|
+
inspect_node(node)
|
|
137
|
+
|
|
138
|
+
nested = node.is_a?(::Prism::DefNode)
|
|
139
|
+
@nested_definition_depth += 1 if nested
|
|
140
|
+
node.compact_child_nodes.each { |child| walk(child) }
|
|
141
|
+
@nested_definition_depth -= 1 if nested
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def inspect_node(node)
|
|
145
|
+
case node
|
|
146
|
+
when ::Prism::CallNode then inspect_call(node)
|
|
147
|
+
when *ATTRIBUTE_ASSIGNMENT_NODES then record_call(node.read_name, node.receiver)
|
|
148
|
+
when *INDEX_ASSIGNMENT_NODES then record_call(:[], node.receiver)
|
|
149
|
+
when ::Prism::CallTargetNode then record_call(node.name, node.receiver)
|
|
150
|
+
when ::Prism::IndexTargetNode then record_call(:[]=, node.receiver)
|
|
151
|
+
when ::Prism::InstanceVariableOrWriteNode
|
|
152
|
+
@memoized_factory_receiver ||= memoized_factory_receiver_for(node)
|
|
153
|
+
when ::Prism::SuperNode, ::Prism::ForwardingSuperNode then @calls_super = true
|
|
154
|
+
when ::Prism::CaseNode
|
|
155
|
+
@case_when_count += node.conditions.count { |condition| condition.is_a?(::Prism::WhenNode) }
|
|
156
|
+
when ::Prism::BeginNode then @cyclomatic_complexity += 1 if node.rescue_clause
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def inspect_call(node)
|
|
161
|
+
record_call(node.name, node.receiver)
|
|
162
|
+
record_raise(node)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def record_call(name, receiver_node)
|
|
166
|
+
receiver, receiver_type = receiver_info(receiver_node)
|
|
167
|
+
@called_methods << name
|
|
168
|
+
@method_calls << Models::MethodCallInfo.new(method_name: name, receiver: receiver, receiver_type: receiver_type)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def record_instance_variable(node)
|
|
172
|
+
@instance_variables << node.name if INSTANCE_VARIABLE_NODES.any? { |type| node.is_a?(type) }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def record_effective_statement(node)
|
|
176
|
+
return if @nested_definition_depth.positive?
|
|
177
|
+
|
|
178
|
+
@effective_statement_count += 1 if EFFECTIVE_STATEMENT_NODES.any? { |type| node.is_a?(type) }
|
|
179
|
+
@effective_statement_count += 2 if OPERATOR_ASSIGNMENT_NODES.any? { |type| node.is_a?(type) }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def record_raise(node)
|
|
183
|
+
return unless %i[raise fail].include?(node.name)
|
|
184
|
+
|
|
185
|
+
raised = node.arguments&.arguments&.first
|
|
186
|
+
return unless raised.is_a?(::Prism::ConstantReadNode) || raised.is_a?(::Prism::ConstantPathNode)
|
|
187
|
+
|
|
188
|
+
@raises << ConstantName.of(raised)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def receiver_info(node)
|
|
192
|
+
case node
|
|
193
|
+
when nil, ::Prism::SelfNode then [nil, :self]
|
|
194
|
+
when ::Prism::ConstantReadNode, ::Prism::ConstantPathNode then [ConstantName.of(node), :const]
|
|
195
|
+
when ::Prism::InstanceVariableReadNode then [node.name.to_s, :ivar]
|
|
196
|
+
when ::Prism::LocalVariableReadNode then [node.name.to_s, :lvar]
|
|
197
|
+
when ::Prism::CallNode then [nil, :send]
|
|
198
|
+
else [nil, :unknown]
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def memoized_factory_receiver_for(node)
|
|
203
|
+
call = node.value
|
|
204
|
+
return nil unless call.is_a?(::Prism::CallNode) && FACTORY_METHODS.include?(call.name)
|
|
205
|
+
|
|
206
|
+
receiver = call.receiver
|
|
207
|
+
return nil unless receiver.is_a?(::Prism::ConstantReadNode) || receiver.is_a?(::Prism::ConstantPathNode)
|
|
208
|
+
|
|
209
|
+
ConstantName.of(receiver)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def parameters
|
|
213
|
+
params = @node.parameters
|
|
214
|
+
return [] unless params
|
|
215
|
+
|
|
216
|
+
[
|
|
217
|
+
*params.requireds.map { |p| positional(p) },
|
|
218
|
+
*params.optionals.map { |p| [:optarg, p.name] },
|
|
219
|
+
*Array(params.rest).map { |p| rest(p) },
|
|
220
|
+
*params.posts.map { |p| positional(p) },
|
|
221
|
+
*params.keywords.map { |p| keyword(p) },
|
|
222
|
+
*Array(params.keyword_rest).map { |p| keyword_rest(p) },
|
|
223
|
+
*Array(params.block).map { |p| [:blockarg, p.name] }
|
|
224
|
+
]
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def positional(node)
|
|
228
|
+
node.is_a?(::Prism::MultiTargetNode) ? [:mlhs, nil] : [:arg, node.name]
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def rest(node)
|
|
232
|
+
node.is_a?(::Prism::ImplicitRestNode) ? [:restarg, nil] : [:restarg, node.name]
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def keyword(node)
|
|
236
|
+
node.is_a?(::Prism::RequiredKeywordParameterNode) ? [:kwarg, node.name] : [:kwoptarg, node.name]
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def keyword_rest(node)
|
|
240
|
+
case node
|
|
241
|
+
when ::Prism::ForwardingParameterNode then [:forward_arg, nil]
|
|
242
|
+
when ::Prism::NoKeywordsParameterNode then [:kwnilarg, nil]
|
|
243
|
+
else [:kwrestarg, node.name]
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
@@ -1,450 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "prism"
|
|
4
4
|
|
|
5
5
|
module SolidScore
|
|
6
6
|
module Parser
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
# - case/when 分岐数のカウント
|
|
12
|
-
#
|
|
13
|
-
# Phase 2a 改善:
|
|
14
|
-
# - クラスメソッド (def self.xxx) の解析対応
|
|
15
|
-
# - モジュール (module) の解析対応
|
|
16
|
-
# - ネストしたクラス/モジュール対応
|
|
17
|
-
# - Rails DSL認識
|
|
18
|
-
class RubyParser
|
|
19
|
-
# Rails/ActiveSupport DSLメソッド
|
|
20
|
-
# これらはメソッド定義ではなくDSL宣言として扱う
|
|
21
|
-
RAILS_DSL_METHODS = %i[
|
|
22
|
-
has_many has_one belongs_to has_and_belongs_to_many
|
|
23
|
-
validates validate validates_presence_of validates_uniqueness_of
|
|
24
|
-
validates_format_of validates_length_of validates_numericality_of
|
|
25
|
-
validates_inclusion_of validates_exclusion_of validates_associated
|
|
26
|
-
before_action after_action around_action skip_before_action
|
|
27
|
-
before_filter after_filter around_filter
|
|
28
|
-
before_validation after_validation
|
|
29
|
-
before_create after_create before_update after_update
|
|
30
|
-
before_save after_save before_destroy after_destroy
|
|
31
|
-
after_commit after_rollback
|
|
32
|
-
scope enum delegate
|
|
33
|
-
serialize store
|
|
34
|
-
accepts_nested_attributes_for
|
|
35
|
-
].freeze
|
|
7
|
+
# Raised when a source file cannot be parsed. Prism itself never raises
|
|
8
|
+
# on syntax errors (it returns a partial tree plus diagnostics), so the
|
|
9
|
+
# first error is surfaced here for callers that want to skip the file.
|
|
10
|
+
class SyntaxError < StandardError; end
|
|
36
11
|
|
|
12
|
+
# Parses Ruby source with Prism's native node API and extracts
|
|
13
|
+
# class/module definitions as Models::ClassInfo.
|
|
14
|
+
class RubyParser
|
|
37
15
|
def parse_file(file_path)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return [] unless ast
|
|
16
|
+
result = ::Prism.parse_file(file_path)
|
|
17
|
+
raise SyntaxError, format_error(file_path, result.errors.first) if result.failure?
|
|
41
18
|
|
|
42
|
-
|
|
19
|
+
collector = DefinitionCollector.new(file_path)
|
|
20
|
+
result.value.accept(collector)
|
|
21
|
+
collector.classes
|
|
43
22
|
end
|
|
44
23
|
|
|
45
24
|
private
|
|
46
25
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return classes unless node.is_a?(::AST::Node)
|
|
50
|
-
|
|
51
|
-
case node.type
|
|
52
|
-
when :class
|
|
53
|
-
class_info = build_class_info(node, file_path, namespace)
|
|
54
|
-
classes << class_info
|
|
55
|
-
# ネストしたクラス/モジュールを探索
|
|
56
|
-
extract_nested_definitions(node.children[2], file_path, classes, class_info.name)
|
|
57
|
-
when :module
|
|
58
|
-
module_info = build_module_info(node, file_path, namespace)
|
|
59
|
-
classes << module_info
|
|
60
|
-
# モジュール内のネストしたクラス/モジュールを探索
|
|
61
|
-
extract_nested_definitions(node.children[1], file_path, classes, module_info.name)
|
|
62
|
-
else
|
|
63
|
-
node.children.each { |child| extract_definitions(child, file_path, classes, namespace) }
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
classes
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# ネストしたクラス/モジュールを探索(親のbodyから)
|
|
70
|
-
def extract_nested_definitions(body, file_path, classes, parent_name)
|
|
71
|
-
return unless body.is_a?(::AST::Node)
|
|
72
|
-
|
|
73
|
-
nodes = body.type == :begin ? body.children : [body]
|
|
74
|
-
nodes.each do |child|
|
|
75
|
-
next unless child.is_a?(::AST::Node)
|
|
76
|
-
|
|
77
|
-
case child.type
|
|
78
|
-
when :class, :module
|
|
79
|
-
extract_definitions(child, file_path, classes, parent_name)
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def build_class_info(node, file_path, namespace = nil)
|
|
85
|
-
raw_name = extract_class_name(node.children[0])
|
|
86
|
-
name = namespace ? "#{namespace}::#{raw_name}" : raw_name
|
|
87
|
-
superclass = node.children[1] ? extract_class_name(node.children[1]) : nil
|
|
88
|
-
body = node.children[2]
|
|
89
|
-
|
|
90
|
-
build_definition_info(name, file_path, node, body, superclass: superclass, kind: :class)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# Phase 2a: モジュール情報を構築
|
|
94
|
-
def build_module_info(node, file_path, namespace = nil)
|
|
95
|
-
raw_name = extract_class_name(node.children[0])
|
|
96
|
-
name = namespace ? "#{namespace}::#{raw_name}" : raw_name
|
|
97
|
-
body = node.children[1]
|
|
98
|
-
|
|
99
|
-
build_definition_info(name, file_path, node, body, kind: :module)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# クラス/モジュール共通の情報構築
|
|
103
|
-
def build_definition_info(name, file_path, node, body, superclass: nil, kind: :class)
|
|
104
|
-
methods = []
|
|
105
|
-
includes = []
|
|
106
|
-
extends = []
|
|
107
|
-
attr_readers = []
|
|
108
|
-
attr_writers = []
|
|
109
|
-
dsl_calls = []
|
|
110
|
-
current_visibility = :public
|
|
111
|
-
|
|
112
|
-
if body
|
|
113
|
-
traverse_body(body, methods, includes, extends, attr_readers, attr_writers,
|
|
114
|
-
dsl_calls, current_visibility)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
instance_variables = methods.flat_map(&:instance_variables).uniq
|
|
118
|
-
|
|
119
|
-
Models::ClassInfo.new(
|
|
120
|
-
name: name,
|
|
121
|
-
file_path: file_path,
|
|
122
|
-
line_start: node.loc.line,
|
|
123
|
-
line_end: node.loc.last_line,
|
|
124
|
-
methods: methods,
|
|
125
|
-
superclass: superclass,
|
|
126
|
-
includes: includes,
|
|
127
|
-
extends: extends,
|
|
128
|
-
instance_variables: instance_variables,
|
|
129
|
-
attr_readers: attr_readers,
|
|
130
|
-
attr_writers: attr_writers,
|
|
131
|
-
kind: kind,
|
|
132
|
-
dsl_calls: dsl_calls
|
|
133
|
-
)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def traverse_body(node, methods, includes, extends, attr_readers, attr_writers,
|
|
137
|
-
dsl_calls, current_visibility)
|
|
138
|
-
return unless node.is_a?(::AST::Node)
|
|
139
|
-
|
|
140
|
-
case node.type
|
|
141
|
-
when :begin
|
|
142
|
-
node.children.each do |child|
|
|
143
|
-
current_visibility = traverse_body(child, methods, includes, extends,
|
|
144
|
-
attr_readers, attr_writers,
|
|
145
|
-
dsl_calls, current_visibility)
|
|
146
|
-
end
|
|
147
|
-
when :def
|
|
148
|
-
methods << build_method_info(node, current_visibility, kind: :instance)
|
|
149
|
-
when :defs
|
|
150
|
-
# Phase 2a: クラスメソッド (def self.xxx)
|
|
151
|
-
# :defs ノード構造: [receiver, name, args, body]
|
|
152
|
-
methods << build_method_info(node, :public, kind: :class)
|
|
153
|
-
when :send
|
|
154
|
-
current_visibility = handle_send_node(node, current_visibility, includes, extends,
|
|
155
|
-
attr_readers, attr_writers, dsl_calls)
|
|
156
|
-
when :class, :module
|
|
157
|
-
# ネストしたクラス/モジュールはtraverse_bodyではスキップ
|
|
158
|
-
# extract_nested_definitionsで別途処理する
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
current_visibility
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def handle_send_node(node, current_visibility, includes, extends,
|
|
165
|
-
attr_readers, attr_writers, dsl_calls)
|
|
166
|
-
method_name = node.children[1]
|
|
167
|
-
|
|
168
|
-
case method_name
|
|
169
|
-
when :private, :protected, :public
|
|
170
|
-
method_name
|
|
171
|
-
when :include
|
|
172
|
-
includes << extract_class_name(node.children[2]) if node.children[2]
|
|
173
|
-
current_visibility
|
|
174
|
-
when :extend
|
|
175
|
-
extends << extract_class_name(node.children[2]) if node.children[2]
|
|
176
|
-
current_visibility
|
|
177
|
-
when :attr_reader
|
|
178
|
-
node.children[2..].each { |arg| attr_readers << arg.children[0] if arg.type == :sym }
|
|
179
|
-
current_visibility
|
|
180
|
-
when :attr_accessor
|
|
181
|
-
node.children[2..].each do |arg|
|
|
182
|
-
next unless arg.type == :sym
|
|
183
|
-
|
|
184
|
-
attr_readers << arg.children[0]
|
|
185
|
-
attr_writers << arg.children[0]
|
|
186
|
-
end
|
|
187
|
-
current_visibility
|
|
188
|
-
when :attr_writer
|
|
189
|
-
node.children[2..].each { |arg| attr_writers << arg.children[0] if arg.type == :sym }
|
|
190
|
-
current_visibility
|
|
191
|
-
else
|
|
192
|
-
# Phase 2a: Rails DSL認識
|
|
193
|
-
if RAILS_DSL_METHODS.include?(method_name)
|
|
194
|
-
dsl_calls << method_name
|
|
195
|
-
end
|
|
196
|
-
current_visibility
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
# Issue #12: factory-style method names that count as instantiation.
|
|
201
|
-
# Mirrors DipAnalyzer::FACTORY_METHODS so the bonus tracks the same set
|
|
202
|
-
# of patterns that are penalised as concrete dependencies.
|
|
203
|
-
MEMOIZED_FACTORY_METHODS = %i[new create build call open].freeze
|
|
204
|
-
|
|
205
|
-
# Issue #10: Statement-like AST nodes that count toward the effective body size.
|
|
206
|
-
#
|
|
207
|
-
# Notes on what we *don't* count and why:
|
|
208
|
-
# - :begin / :rescue / :ensure are sequence wrappers (their handler bodies
|
|
209
|
-
# are counted via their children).
|
|
210
|
-
# - :when is a child of :case and would double-count the dispatch.
|
|
211
|
-
# - :ivar / :lvar are lookups, not statements.
|
|
212
|
-
#
|
|
213
|
-
# Notes on what we *do* count:
|
|
214
|
-
# - :send and :csend (safe-navigation) treat method calls as a statement;
|
|
215
|
-
# chained calls intentionally accumulate because each step is observable
|
|
216
|
-
# behaviour.
|
|
217
|
-
# - :masgn (multiple assignment) joins the explicit assignment forms.
|
|
218
|
-
EFFECTIVE_STATEMENT_TYPES = %i[
|
|
219
|
-
send csend
|
|
220
|
-
if case return
|
|
221
|
-
ivasgn lvasgn gvasgn cvasgn masgn
|
|
222
|
-
and_asgn or_asgn op_asgn
|
|
223
|
-
yield block
|
|
224
|
-
while until for
|
|
225
|
-
next break retry redo super zsuper
|
|
226
|
-
].freeze
|
|
227
|
-
|
|
228
|
-
# Nested method/lambda boundaries — count_effective_statements stops at
|
|
229
|
-
# these so that statements inside an inner lambda don't leak into the
|
|
230
|
-
# outer method's count.
|
|
231
|
-
NESTED_DEFINITION_TYPES = %i[def defs].freeze
|
|
232
|
-
|
|
233
|
-
# :def ノード構造: [name, args, body]
|
|
234
|
-
# :defs ノード構造: [receiver, name, args, body]
|
|
235
|
-
def build_method_info(node, visibility, kind: :instance)
|
|
236
|
-
name, args, body = extract_method_parts(node, kind)
|
|
237
|
-
|
|
238
|
-
instance_vars = []
|
|
239
|
-
called_methods = []
|
|
240
|
-
raises = []
|
|
241
|
-
method_calls = []
|
|
242
|
-
calls_super = false
|
|
243
|
-
case_when_count = 0
|
|
244
|
-
memoized_receiver = nil
|
|
245
|
-
effective_count = 0
|
|
246
|
-
|
|
247
|
-
if body
|
|
248
|
-
collect_method_details(body, instance_vars, called_methods, raises, method_calls)
|
|
249
|
-
calls_super = contains_super?(body)
|
|
250
|
-
case_when_count = count_case_when_branches(body)
|
|
251
|
-
memoized_receiver = detect_memoized_factory_receiver(body)
|
|
252
|
-
effective_count = count_effective_statements(body)
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
parameters = extract_parameters(args)
|
|
256
|
-
complexity = calculate_cyclomatic_complexity(body)
|
|
257
|
-
|
|
258
|
-
Models::MethodInfo.new(
|
|
259
|
-
name: name,
|
|
260
|
-
visibility: visibility,
|
|
261
|
-
line_start: node.loc.line,
|
|
262
|
-
line_end: node.loc.last_line,
|
|
263
|
-
instance_variables: instance_vars.uniq,
|
|
264
|
-
called_methods: called_methods.uniq,
|
|
265
|
-
parameters: parameters,
|
|
266
|
-
cyclomatic_complexity: complexity,
|
|
267
|
-
raises: raises,
|
|
268
|
-
calls_super: calls_super,
|
|
269
|
-
method_calls: method_calls,
|
|
270
|
-
case_when_count: case_when_count,
|
|
271
|
-
kind: kind,
|
|
272
|
-
effective_statement_count: effective_count,
|
|
273
|
-
memoized_factory_receiver: memoized_receiver
|
|
274
|
-
)
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
# Issue #12: Detects the `@ivar ||= ConstantClass.new(...)` memoised
|
|
278
|
-
# factory pattern (and the broader factory-method variants such as
|
|
279
|
-
# `.create`, `.build`, `.call`, `.open`). Returns the constant's
|
|
280
|
-
# qualified name (e.g. "ProvisioningService", "Foo::Bar") so the
|
|
281
|
-
# analyzer can apply the standard-library whitelist consistently
|
|
282
|
-
# with the concrete-dependency penalty path.
|
|
283
|
-
def detect_memoized_factory_receiver(node)
|
|
284
|
-
return nil unless node.is_a?(::AST::Node)
|
|
285
|
-
|
|
286
|
-
receiver = memoized_factory_receiver_for(node)
|
|
287
|
-
return receiver if receiver
|
|
288
|
-
|
|
289
|
-
node.children.each do |child|
|
|
290
|
-
found = detect_memoized_factory_receiver(child)
|
|
291
|
-
return found if found
|
|
292
|
-
end
|
|
293
|
-
nil
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
def memoized_factory_receiver_for(node)
|
|
297
|
-
return nil unless node.type == :or_asgn
|
|
298
|
-
|
|
299
|
-
lhs, rhs = node.children
|
|
300
|
-
return nil unless lhs.is_a?(::AST::Node) && lhs.type == :ivasgn
|
|
301
|
-
return nil unless rhs.is_a?(::AST::Node) && rhs.type == :send
|
|
302
|
-
|
|
303
|
-
receiver, method_name = rhs.children[0], rhs.children[1]
|
|
304
|
-
return nil unless receiver.is_a?(::AST::Node) && receiver.type == :const
|
|
305
|
-
return nil unless MEMOIZED_FACTORY_METHODS.include?(method_name)
|
|
306
|
-
|
|
307
|
-
extract_class_name(receiver)
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
def count_effective_statements(node, count = 0, root: true)
|
|
311
|
-
return count unless node.is_a?(::AST::Node)
|
|
312
|
-
# Don't recurse into nested method/lambda definitions when counting
|
|
313
|
-
# the parent body. The nested definition's own effective count is
|
|
314
|
-
# tracked separately by build_method_info.
|
|
315
|
-
return count if !root && NESTED_DEFINITION_TYPES.include?(node.type)
|
|
316
|
-
|
|
317
|
-
count += 1 if EFFECTIVE_STATEMENT_TYPES.include?(node.type)
|
|
318
|
-
node.children.each { |child| count = count_effective_statements(child, count, root: false) }
|
|
319
|
-
count
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
def extract_method_parts(node, kind)
|
|
323
|
-
case kind
|
|
324
|
-
when :class
|
|
325
|
-
[node.children[1], node.children[2], node.children[3]]
|
|
326
|
-
else
|
|
327
|
-
[node.children[0], node.children[1], node.children[2]]
|
|
328
|
-
end
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
# Phase 1 改善: レシーバ情報を含むメソッド呼び出し情報を収集
|
|
332
|
-
def collect_method_details(node, instance_vars, called_methods, raises, method_calls)
|
|
333
|
-
return unless node.is_a?(::AST::Node)
|
|
334
|
-
|
|
335
|
-
case node.type
|
|
336
|
-
when :ivar, :ivasgn
|
|
337
|
-
instance_vars << node.children[0]
|
|
338
|
-
when :send
|
|
339
|
-
receiver_node = node.children[0]
|
|
340
|
-
method_name = node.children[1]
|
|
341
|
-
|
|
342
|
-
called_methods << method_name
|
|
343
|
-
|
|
344
|
-
# Collect detailed method call info with receiver
|
|
345
|
-
method_calls << build_method_call_info(receiver_node, method_name)
|
|
346
|
-
|
|
347
|
-
if %i[raise fail].include?(method_name)
|
|
348
|
-
raise_class = node.children[2]
|
|
349
|
-
raises << extract_class_name(raise_class) if raise_class.is_a?(::AST::Node) && raise_class.type == :const
|
|
350
|
-
end
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
node.children.each do |child|
|
|
354
|
-
collect_method_details(child, instance_vars, called_methods, raises, method_calls)
|
|
355
|
-
end
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
# Phase 1 改善: メソッド呼び出し情報を構築
|
|
359
|
-
def build_method_call_info(receiver_node, method_name)
|
|
360
|
-
receiver, receiver_type = extract_receiver_info(receiver_node)
|
|
361
|
-
|
|
362
|
-
Models::MethodCallInfo.new(
|
|
363
|
-
method_name: method_name,
|
|
364
|
-
receiver: receiver,
|
|
365
|
-
receiver_type: receiver_type
|
|
366
|
-
)
|
|
367
|
-
end
|
|
368
|
-
|
|
369
|
-
# Phase 1 改善: レシーバ情報を抽出
|
|
370
|
-
def extract_receiver_info(node)
|
|
371
|
-
return [nil, :self] if node.nil?
|
|
372
|
-
return [nil, :unknown] unless node.is_a?(::AST::Node)
|
|
373
|
-
|
|
374
|
-
case node.type
|
|
375
|
-
when :const
|
|
376
|
-
[extract_class_name(node), :const]
|
|
377
|
-
when :ivar
|
|
378
|
-
[node.children[0].to_s, :ivar]
|
|
379
|
-
when :lvar
|
|
380
|
-
[node.children[0].to_s, :lvar]
|
|
381
|
-
when :self
|
|
382
|
-
[nil, :self]
|
|
383
|
-
when :send
|
|
384
|
-
# Chained method call (e.g., foo.bar.baz)
|
|
385
|
-
[nil, :send]
|
|
386
|
-
else
|
|
387
|
-
[nil, :unknown]
|
|
388
|
-
end
|
|
389
|
-
end
|
|
390
|
-
|
|
391
|
-
# Phase 1 改善: case/when 分岐数をカウント
|
|
392
|
-
def count_case_when_branches(node, count = 0)
|
|
393
|
-
return count unless node.is_a?(::AST::Node)
|
|
394
|
-
|
|
395
|
-
if node.type == :case
|
|
396
|
-
when_count = node.children.count { |child| child.is_a?(::AST::Node) && child.type == :when }
|
|
397
|
-
count += when_count
|
|
398
|
-
end
|
|
399
|
-
|
|
400
|
-
node.children.each do |child|
|
|
401
|
-
count = count_case_when_branches(child, count)
|
|
402
|
-
end
|
|
403
|
-
|
|
404
|
-
count
|
|
405
|
-
end
|
|
406
|
-
|
|
407
|
-
def contains_super?(node)
|
|
408
|
-
return false unless node.is_a?(::AST::Node)
|
|
409
|
-
return true if %i[super zsuper].include?(node.type)
|
|
410
|
-
|
|
411
|
-
node.children.any? { |child| contains_super?(child) }
|
|
412
|
-
end
|
|
413
|
-
|
|
414
|
-
def extract_parameters(args_node)
|
|
415
|
-
return [] unless args_node
|
|
416
|
-
|
|
417
|
-
args_node.children.map do |arg|
|
|
418
|
-
[arg.type, arg.children[0]]
|
|
419
|
-
end
|
|
420
|
-
end
|
|
421
|
-
|
|
422
|
-
def calculate_cyclomatic_complexity(node, complexity = 1)
|
|
423
|
-
return complexity unless node.is_a?(::AST::Node)
|
|
424
|
-
|
|
425
|
-
case node.type
|
|
426
|
-
when :if, :while, :until, :for, :when, :and, :or, :rescue
|
|
427
|
-
complexity += 1
|
|
428
|
-
end
|
|
429
|
-
|
|
430
|
-
node.children.each do |child|
|
|
431
|
-
complexity = calculate_cyclomatic_complexity(child, complexity)
|
|
432
|
-
end
|
|
433
|
-
|
|
434
|
-
complexity
|
|
435
|
-
end
|
|
436
|
-
|
|
437
|
-
def extract_class_name(node)
|
|
438
|
-
return nil unless node.is_a?(::AST::Node)
|
|
439
|
-
|
|
440
|
-
case node.type
|
|
441
|
-
when :const
|
|
442
|
-
parent = node.children[0]
|
|
443
|
-
name = node.children[1].to_s
|
|
444
|
-
parent ? "#{extract_class_name(parent)}::#{name}" : name
|
|
445
|
-
else
|
|
446
|
-
node.to_s
|
|
447
|
-
end
|
|
26
|
+
def format_error(file_path, error)
|
|
27
|
+
"#{file_path}:#{error.location.start_line}:#{error.location.start_column}: #{error.message}"
|
|
448
28
|
end
|
|
449
29
|
end
|
|
450
30
|
end
|
data/lib/solid_score/runner.rb
CHANGED
data/lib/solid_score/version.rb
CHANGED
data/lib/solid_score.rb
CHANGED
|
@@ -4,6 +4,10 @@ require_relative "solid_score/version"
|
|
|
4
4
|
require_relative "solid_score/models/method_info"
|
|
5
5
|
require_relative "solid_score/models/class_info"
|
|
6
6
|
require_relative "solid_score/models/score_result"
|
|
7
|
+
require_relative "solid_score/parser/constant_name"
|
|
8
|
+
require_relative "solid_score/parser/method_analyzer"
|
|
9
|
+
require_relative "solid_score/parser/class_body_scanner"
|
|
10
|
+
require_relative "solid_score/parser/definition_collector"
|
|
7
11
|
require_relative "solid_score/parser/ruby_parser"
|
|
8
12
|
require_relative "solid_score/analyzers/base_analyzer"
|
|
9
13
|
require_relative "solid_score/analyzers/mitigations"
|
metadata
CHANGED
|
@@ -1,43 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid_score
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- harachan
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
14
|
+
name: prism
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
19
|
+
version: '1.0'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
27
|
-
- !ruby/object:Gem::Dependency
|
|
28
|
-
name: parser
|
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
|
30
|
-
requirements:
|
|
31
|
-
- - "~>"
|
|
32
|
-
- !ruby/object:Gem::Version
|
|
33
|
-
version: '3.3'
|
|
34
|
-
type: :runtime
|
|
35
|
-
prerelease: false
|
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
-
requirements:
|
|
38
|
-
- - "~>"
|
|
39
|
-
- !ruby/object:Gem::Version
|
|
40
|
-
version: '3.3'
|
|
26
|
+
version: '1.0'
|
|
41
27
|
description: Static analysis tool that scores Ruby classes/modules against SOLID principles
|
|
42
28
|
using AST analysis
|
|
43
29
|
email:
|
|
@@ -80,6 +66,10 @@ files:
|
|
|
80
66
|
- lib/solid_score/models/class_info.rb
|
|
81
67
|
- lib/solid_score/models/method_info.rb
|
|
82
68
|
- lib/solid_score/models/score_result.rb
|
|
69
|
+
- lib/solid_score/parser/class_body_scanner.rb
|
|
70
|
+
- lib/solid_score/parser/constant_name.rb
|
|
71
|
+
- lib/solid_score/parser/definition_collector.rb
|
|
72
|
+
- lib/solid_score/parser/method_analyzer.rb
|
|
83
73
|
- lib/solid_score/parser/ruby_parser.rb
|
|
84
74
|
- lib/solid_score/presets.rb
|
|
85
75
|
- lib/solid_score/runner.rb
|