hashira 0.3.0 → 0.5.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/CHANGELOG.md +155 -0
- data/README.md +127 -25
- data/lib/hashira/analysis/finding.rb +7 -6
- data/lib/hashira/analysis/node_walk.rb +3 -3
- data/lib/hashira/analysis/syntax.rb +21 -6
- data/lib/hashira/analysis/type_walk.rb +7 -9
- data/lib/hashira/churn.rb +11 -13
- data/lib/hashira/ci/accepted.rb +27 -31
- data/lib/hashira/ci/baseline.rb +22 -24
- data/lib/hashira/ci/diff.rb +8 -7
- data/lib/hashira/ci/edge_diff_report.rb +13 -17
- data/lib/hashira/ci/finding_diff_report.rb +14 -18
- data/lib/hashira/ci/gate.rb +19 -23
- data/lib/hashira/ci/improvement.rb +9 -14
- data/lib/hashira/ci/ratchet.rb +32 -27
- data/lib/hashira/ci/ratchet_report.rb +26 -32
- data/lib/hashira/cli/arguments.rb +23 -0
- data/lib/hashira/cli/command_line.rb +55 -71
- data/lib/hashira/cli/fail_on.rb +24 -19
- data/lib/hashira/cli/options.rb +9 -4
- data/lib/hashira/cli/package_by.rb +14 -0
- data/lib/hashira/cli/run.rb +33 -27
- data/lib/hashira/cli/skip.rb +14 -20
- data/lib/hashira/cli/usage.rb +44 -41
- data/lib/hashira/cli.rb +17 -19
- data/lib/hashira/complexity/boolean_run.rb +13 -15
- data/lib/hashira/complexity/cognitive_score.rb +68 -66
- data/lib/hashira/complexity/if_chain.rb +33 -38
- data/lib/hashira/complexity/method_finding.rb +20 -42
- data/lib/hashira/complexity/method_score.rb +9 -7
- data/lib/hashira/complexity/rescue_scan.rb +16 -21
- data/lib/hashira/complexity/rollup.rb +13 -15
- data/lib/hashira/complexity/scores.rb +44 -0
- data/lib/hashira/coupling/audiences.rb +49 -0
- data/lib/hashira/coupling/catalog.rb +27 -0
- data/lib/hashira/coupling/census.rb +55 -0
- data/lib/hashira/coupling/constant_registry.rb +47 -0
- data/lib/hashira/coupling/cycle_findings.rb +31 -0
- data/lib/hashira/coupling/cycle_search.rb +37 -0
- data/lib/hashira/coupling/cycles.rb +14 -0
- data/lib/hashira/coupling/definition.rb +22 -0
- data/lib/hashira/coupling/definitions.rb +31 -0
- data/lib/hashira/coupling/edge.rb +10 -0
- data/lib/hashira/coupling/edge_map.rb +33 -0
- data/lib/hashira/coupling/folder_placement.rb +17 -0
- data/lib/hashira/coupling/folding.rb +56 -0
- data/lib/hashira/coupling/graph.rb +63 -0
- data/lib/hashira/coupling/metric.rb +15 -0
- data/lib/hashira/coupling/mixed_audience_findings.rb +32 -0
- data/lib/hashira/coupling/namespace_placement.rb +28 -0
- data/lib/hashira/{analysis → coupling}/namespace_prefix.rb +11 -11
- data/lib/hashira/coupling/naming.rb +14 -0
- data/lib/hashira/coupling/no_folding.rb +13 -0
- data/lib/hashira/coupling/placement.rb +19 -0
- data/lib/hashira/coupling/references.rb +55 -0
- data/lib/hashira/coupling/report.rb +28 -0
- data/lib/hashira/coupling/roll_call.rb +45 -0
- data/lib/hashira/coupling/roll_call_findings.rb +30 -0
- data/lib/hashira/coupling/roster.rb +25 -0
- data/lib/hashira/coupling/rule.rb +18 -0
- data/lib/hashira/coupling/scope.rb +28 -0
- data/lib/hashira/coupling/sdp_check.rb +14 -0
- data/lib/hashira/coupling/sdp_violation_findings.rb +25 -0
- data/lib/hashira/coupling/wide_edge_findings.rb +25 -0
- data/lib/hashira/coupling/words.rb +29 -0
- data/lib/hashira/diagram/dot.rb +8 -11
- data/lib/hashira/diagram/mermaid.rb +11 -16
- data/lib/hashira/diagram/source.rb +15 -0
- data/lib/hashira/duplication/clones.rb +17 -0
- data/lib/hashira/duplication/cluster.rb +14 -13
- data/lib/hashira/duplication/clusters.rb +46 -0
- data/lib/hashira/duplication/delta.rb +21 -30
- data/lib/hashira/duplication/duplication_finding.rb +17 -23
- data/lib/hashira/duplication/fragment.rb +19 -23
- data/lib/hashira/duplication/grouping.rb +12 -16
- data/lib/hashira/duplication/harvest.rb +27 -0
- data/lib/hashira/duplication/index.rb +20 -24
- data/lib/hashira/duplication/maximal.rb +11 -15
- data/lib/hashira/duplication/near_miss.rb +19 -22
- data/lib/hashira/duplication/sequence.rb +20 -22
- data/lib/hashira/duplication/similarity.rb +31 -34
- data/lib/hashira/duplication/union_find.rb +10 -14
- data/lib/hashira/duplication/variance.rb +35 -43
- data/lib/hashira/error.rb +4 -2
- data/lib/hashira/hotspots/file_cost.rb +8 -7
- data/lib/hashira/hotspots/rollup.rb +19 -21
- data/lib/hashira/pipeline.rb +41 -31
- data/lib/hashira/project.rb +50 -49
- data/lib/hashira/report/complexity_table.rb +27 -31
- data/lib/hashira/report/dependency_map.rb +17 -22
- data/lib/hashira/report/finding_lines.rb +15 -19
- data/lib/hashira/report/graph_payload.rb +14 -20
- data/lib/hashira/report/hotspot_table.rb +32 -37
- data/lib/hashira/report/json.rb +32 -32
- data/lib/hashira/report/metrics_table.rb +46 -33
- data/lib/hashira/report/phrases.rb +98 -0
- data/lib/hashira/report/smell_phrases.rb +68 -0
- data/lib/hashira/report/text.rb +59 -52
- data/lib/hashira/smells/census.rb +32 -0
- data/lib/hashira/smells/check.rb +30 -0
- data/lib/hashira/smells/conditions.rb +44 -0
- data/lib/hashira/smells/contexts.rb +64 -0
- data/lib/hashira/smells/control_parameter.rb +22 -0
- data/lib/hashira/smells/data_clump.rb +30 -0
- data/lib/hashira/smells/duplicate_method_call.rb +54 -0
- data/lib/hashira/smells/feature_envy.rb +17 -0
- data/lib/hashira/smells/instance_variable_assumption.rb +44 -0
- data/lib/hashira/smells/manual_dispatch.rb +16 -0
- data/lib/hashira/smells/module_initialize.rb +9 -0
- data/lib/hashira/smells/nil_check.rb +32 -0
- data/lib/hashira/smells/param_check.rb +51 -0
- data/lib/hashira/smells/refs.rb +53 -0
- data/lib/hashira/smells/repeated_conditional.rb +29 -0
- data/lib/hashira/smells/report.rb +31 -0
- data/lib/hashira/smells/scope.rb +29 -0
- data/lib/hashira/smells/too_many_instance_variables.rb +24 -0
- data/lib/hashira/smells/utility_function.rb +13 -0
- data/lib/hashira/smells/visibility.rb +64 -0
- data/lib/hashira/version.rb +1 -1
- data/lib/hashira.rb +131 -60
- metadata +62 -20
- data/lib/hashira/analysis/census.rb +0 -39
- data/lib/hashira/analysis/constant_registry.rb +0 -40
- data/lib/hashira/analysis/cycle_findings.rb +0 -33
- data/lib/hashira/analysis/cycle_search.rb +0 -44
- data/lib/hashira/analysis/definitions.rb +0 -27
- data/lib/hashira/analysis/edge.rb +0 -9
- data/lib/hashira/analysis/edge_map.rb +0 -36
- data/lib/hashira/analysis/graph.rb +0 -56
- data/lib/hashira/analysis/metric.rb +0 -14
- data/lib/hashira/analysis/references.rb +0 -39
- data/lib/hashira/analysis/rule.rb +0 -22
- data/lib/hashira/analysis/sdp_check.rb +0 -18
- data/lib/hashira/analysis/sdp_violation_findings.rb +0 -27
- data/lib/hashira/complexity/analyzer.rb +0 -45
- data/lib/hashira/diagram/renderer.rb +0 -20
- data/lib/hashira/duplication/analyzer.rb +0 -21
- data/lib/hashira/duplication/clusterer.rb +0 -50
- data/lib/hashira/duplication/extractor.rb +0 -31
data/lib/hashira/cli/run.rb
CHANGED
|
@@ -1,44 +1,50 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class Run
|
|
6
|
-
MODES = { update_baseline: :update_baseline, ratchet: :check_ratchet,
|
|
7
|
-
fail_on: :check_gate, json: :print_json,
|
|
8
|
-
dot: :print_diagram, mermaid: :print_diagram }.freeze
|
|
3
|
+
class Hashira::CLI::Run
|
|
4
|
+
MODES = { update: :update, ratchet: :check, fail_on: :guard, json: :json, dot: :diagram, mermaid: :diagram }.freeze
|
|
9
5
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
def initialize(pipeline, options)
|
|
7
|
+
@pipeline = pipeline
|
|
8
|
+
@options = options
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def status = __send__(MODES.fetch(@options.mode, :text))
|
|
12
|
+
|
|
13
|
+
private
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
def graph = @pipeline.graph
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
def findings = @findings ||= Hashira::CI::Accepted.load(@options.baseline).screen(@pipeline.findings)
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
def update = ratchet.update
|
|
20
|
+
|
|
21
|
+
def check
|
|
22
|
+
stop = ratchet.blocker
|
|
23
|
+
raise(Hashira::Error, stop) if stop
|
|
24
|
+
ratchet.check
|
|
25
|
+
end
|
|
20
26
|
|
|
21
|
-
|
|
27
|
+
def guard = gate.check
|
|
22
28
|
|
|
23
|
-
|
|
29
|
+
def diagram = source.print
|
|
24
30
|
|
|
25
|
-
|
|
31
|
+
def json = report(Hashira::Report::Json)
|
|
26
32
|
|
|
27
|
-
|
|
33
|
+
def text = report(Hashira::Report::Text)
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
def report(kind) = kind.new(view).print
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
def ratchet = @ratchet ||= Hashira::CI::Ratchet.new(graph, findings.all, @options.baseline)
|
|
32
38
|
|
|
33
|
-
|
|
39
|
+
def gate = Hashira::CI::Gate.new(findings, @options.fail_on)
|
|
34
40
|
|
|
35
|
-
|
|
41
|
+
def source = Hashira::Diagram::Source.new(graph, @options.mode)
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
def view
|
|
44
|
+
Hashira::Report::View.new(
|
|
45
|
+
project: @pipeline.project, graph: (graph if @pipeline.enabled?(:coupling)),
|
|
46
|
+
complexity: @pipeline.complexity, duplication: @pipeline.duplication,
|
|
47
|
+
hotspots: @pipeline.hotspots, findings:
|
|
48
|
+
)
|
|
43
49
|
end
|
|
44
50
|
end
|
data/lib/hashira/cli/skip.rb
CHANGED
|
@@ -1,27 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module Hashira
|
|
4
|
-
|
|
5
|
-
module Skip
|
|
6
|
-
module_function
|
|
3
|
+
module Hashira::CLI::Skip
|
|
4
|
+
module_function
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
keys
|
|
15
|
-
end
|
|
6
|
+
def parse(list)
|
|
7
|
+
return [] unless list
|
|
8
|
+
keys = list.split(",").map { key(it.strip) }
|
|
9
|
+
raise(Hashira::Error, "cannot skip every analyzer") if (Hashira::Pipeline::ANALYZERS - keys).empty?
|
|
10
|
+
keys
|
|
11
|
+
end
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
def key(name)
|
|
14
|
+
symbol = name.to_sym
|
|
15
|
+
Hashira::Pipeline::ANALYZERS.include?(symbol) ? symbol : unknown(name)
|
|
16
|
+
end
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
end
|
|
25
|
-
end
|
|
18
|
+
def unknown(name)
|
|
19
|
+
raise(Hashira::Error, "unknown --skip #{name.inspect} (use: #{Hashira::Pipeline::ANALYZERS.join(", ")})")
|
|
26
20
|
end
|
|
27
21
|
end
|
data/lib/hashira/cli/usage.rb
CHANGED
|
@@ -1,45 +1,48 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module Hashira
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
3
|
+
module Hashira::CLI::Usage
|
|
4
|
+
TEXT = <<~HELP
|
|
5
|
+
Usage: hashira [DIRECTORY ...] [options]
|
|
6
|
+
|
|
7
|
+
Coupling, cognitive-complexity, duplication, and code-smell metrics
|
|
8
|
+
for Ruby, via Prism, rolled up per file as a ranked list of hotspots.
|
|
9
|
+
With no directory, auto-detects lib/<gem>.
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
--format FORMAT text (default), json, dot, or mermaid
|
|
13
|
+
--json shorthand for --format json
|
|
14
|
+
--fail-on KINDS exit 1 if findings exist; comma-separated
|
|
15
|
+
kinds: cycles, sdp, mixed_audience, wide_edge,
|
|
16
|
+
roll_call, complexity, duplication, smells (all
|
|
17
|
+
of them), or one smell kind such as feature_envy
|
|
18
|
+
--skip ANALYZERS drop an analyzer; comma-separated: coupling,
|
|
19
|
+
complexity, duplication, smells
|
|
20
|
+
--package-by WHAT group coupling by: auto, folder, or namespace
|
|
21
|
+
(top-level constant). Default auto: namespace for
|
|
22
|
+
Rails apps (config/application.rb in or beside the
|
|
23
|
+
analyzed directory), folder otherwise
|
|
24
|
+
--ratchet fail when edges or findings appear that the
|
|
25
|
+
baseline lacks
|
|
26
|
+
--update-baseline record the current edges and findings
|
|
27
|
+
--baseline PATH baseline file (default: hashira_baseline.json)
|
|
28
|
+
-h, --help print this help
|
|
29
|
+
--version print the version
|
|
30
|
+
|
|
31
|
+
Findings accepted by design can be recorded in the baseline as
|
|
32
|
+
"accepted": [{"kind": "...", "package": "...", "reason": "..."}]
|
|
33
|
+
— they leave reports and gates, keeping a one-line reminder each.
|
|
34
|
+
Clones are named by "digest" instead of "package", so an acceptance
|
|
35
|
+
survives the lines above it moving. Read digests from --json.
|
|
36
|
+
HELP
|
|
37
|
+
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
def help = emit(TEXT)
|
|
41
|
+
|
|
42
|
+
def version = emit("hashira #{Hashira::VERSION}")
|
|
43
|
+
|
|
44
|
+
def emit(output)
|
|
45
|
+
puts(output)
|
|
46
|
+
0
|
|
44
47
|
end
|
|
45
48
|
end
|
data/lib/hashira/cli.rb
CHANGED
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def self.usage?(options) = %i[help version].include?(options.mode)
|
|
3
|
+
class Hashira::CLI
|
|
4
|
+
def self.run(argv)
|
|
5
|
+
options = Options.parse(argv)
|
|
6
|
+
usage?(options) ? Usage.public_send(options.mode) : new(options).run
|
|
7
|
+
rescue Hashira::Error => error
|
|
8
|
+
failure(error)
|
|
9
|
+
end
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
warn "hashira: #{error.message}"
|
|
16
|
-
1
|
|
17
|
-
end
|
|
11
|
+
def self.usage?(options) = %i[help version].include?(options.mode)
|
|
18
12
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
def self.failure(error)
|
|
14
|
+
warn("hashira: #{error.message}")
|
|
15
|
+
1
|
|
16
|
+
end
|
|
23
17
|
|
|
24
|
-
|
|
18
|
+
def initialize(options)
|
|
19
|
+
@options = options
|
|
20
|
+
@pipeline = options.pipeline
|
|
25
21
|
end
|
|
22
|
+
|
|
23
|
+
def run = Run.new(@pipeline, @options).status
|
|
26
24
|
end
|
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@scorer = scorer
|
|
8
|
-
end
|
|
3
|
+
class Hashira::Complexity::BooleanRun
|
|
4
|
+
def initialize(scorer)
|
|
5
|
+
@scorer = scorer
|
|
6
|
+
end
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
def apply(node)
|
|
9
|
+
@scorer.add(node, 1, "boolean")
|
|
10
|
+
operands(node).each { @scorer.visit(it) }
|
|
11
|
+
end
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
private
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
end
|
|
20
|
-
end
|
|
15
|
+
def operands(node)
|
|
16
|
+
node.compact_child_nodes.flat_map { kin?(it, node) ? operands(it) : [it] }
|
|
21
17
|
end
|
|
18
|
+
|
|
19
|
+
def kin?(child, node) = child.instance_of?(node.class)
|
|
22
20
|
end
|
|
@@ -2,71 +2,73 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
visit(def_node.body, 0)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
attr_reader :increments, :calls
|
|
36
|
-
|
|
37
|
-
def total = @increments.sum(&:cost)
|
|
38
|
-
|
|
39
|
-
def visit(node, nesting)
|
|
40
|
-
return unless node
|
|
41
|
-
|
|
42
|
-
send(HANDLERS.fetch(node.class, :descend), node, nesting)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def add(node, cost, label)
|
|
46
|
-
@increments << Increment.new(line: node.location.start_line, cost:, label:)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
def descend(node, nesting) = node.compact_child_nodes.each { visit(it, nesting) }
|
|
52
|
-
|
|
53
|
-
def on_call(node, nesting)
|
|
54
|
-
@calls += 1
|
|
55
|
-
descend(node, nesting)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def on_block(node, nesting) = node.compact_child_nodes.each { visit(it, nesting + 1) }
|
|
59
|
-
|
|
60
|
-
def on_nester(node, nesting)
|
|
61
|
-
add(node, 1 + nesting, LABELS.fetch(node.class))
|
|
62
|
-
node.compact_child_nodes.each { visit(it, nesting + 1) }
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def on_if(node, nesting) = IfChain.new(self).apply(node, nesting)
|
|
66
|
-
|
|
67
|
-
def on_begin(node, nesting) = RescueScan.new(self).apply(node, nesting)
|
|
68
|
-
|
|
69
|
-
def on_boolean(node, nesting) = BooleanRun.new(self).apply(node, nesting)
|
|
70
|
-
end
|
|
5
|
+
class Hashira::Complexity::CognitiveScore
|
|
6
|
+
HANDLERS = {
|
|
7
|
+
Prism::IfNode => :on_if,
|
|
8
|
+
Prism::UnlessNode => :on_nester,
|
|
9
|
+
Prism::WhileNode => :on_nester,
|
|
10
|
+
Prism::UntilNode => :on_nester,
|
|
11
|
+
Prism::ForNode => :on_nester,
|
|
12
|
+
Prism::CaseNode => :on_nester,
|
|
13
|
+
Prism::CaseMatchNode => :on_nester,
|
|
14
|
+
Prism::BeginNode => :on_begin,
|
|
15
|
+
Prism::AndNode => :on_boolean,
|
|
16
|
+
Prism::OrNode => :on_boolean,
|
|
17
|
+
Prism::BlockNode => :on_block,
|
|
18
|
+
Prism::CallNode => :on_call
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
LABELS = {
|
|
22
|
+
Prism::UnlessNode => "unless", Prism::WhileNode => "while",
|
|
23
|
+
Prism::UntilNode => "until", Prism::ForNode => "for",
|
|
24
|
+
Prism::CaseNode => "case", Prism::CaseMatchNode => "case"
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
def initialize(def_node)
|
|
28
|
+
@increments = []
|
|
29
|
+
@calls = 0
|
|
30
|
+
@nesting = 0
|
|
31
|
+
visit(def_node.body)
|
|
71
32
|
end
|
|
33
|
+
|
|
34
|
+
attr_reader :increments, :calls, :nesting
|
|
35
|
+
|
|
36
|
+
def total = @increments.sum(&:cost)
|
|
37
|
+
|
|
38
|
+
def visit(node)
|
|
39
|
+
return unless node
|
|
40
|
+
__send__(HANDLERS.fetch(node.class, :descend), node)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def add(node, cost, label)
|
|
44
|
+
@increments << Hashira::Complexity::Increment.new(line: node.location.start_line, cost:, label:)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def deeper
|
|
48
|
+
@nesting += 1
|
|
49
|
+
yield
|
|
50
|
+
@nesting -= 1
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def descend(node) = node.compact_child_nodes.each { visit(it) }
|
|
56
|
+
|
|
57
|
+
def on_call(node)
|
|
58
|
+
@calls += 1
|
|
59
|
+
descend(node)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def on_block(node) = deeper { descend(node) }
|
|
63
|
+
|
|
64
|
+
def on_nester(node)
|
|
65
|
+
add(node, 1 + @nesting, LABELS.fetch(node.class))
|
|
66
|
+
deeper { descend(node) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def on_if(node) = Hashira::Complexity::IfChain.new(self).apply(node)
|
|
70
|
+
|
|
71
|
+
def on_begin(node) = Hashira::Complexity::RescueScan.new(self).apply(node)
|
|
72
|
+
|
|
73
|
+
def on_boolean(node) = Hashira::Complexity::BooleanRun.new(self).apply(node)
|
|
72
74
|
end
|
|
@@ -2,44 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
case node
|
|
29
|
-
when Prism::IfNode then branch(node, 1, nesting, "elsif")
|
|
30
|
-
when Prism::ElseNode then otherwise(node, nesting)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def otherwise(node, nesting)
|
|
35
|
-
@scorer.add(node, 1, "else")
|
|
36
|
-
@scorer.visit(node.statements, nesting + 1)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def ternary(node, nesting)
|
|
40
|
-
@scorer.add(node, 1, "ternary")
|
|
41
|
-
node.compact_child_nodes.each { @scorer.visit(it, nesting) }
|
|
42
|
-
end
|
|
5
|
+
class Hashira::Complexity::IfChain
|
|
6
|
+
def initialize(scorer)
|
|
7
|
+
@scorer = scorer
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def apply(node)
|
|
11
|
+
return ternary(node) unless node.if_keyword
|
|
12
|
+
branch(node, 1 + @scorer.nesting, "if")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def branch(node, cost, label)
|
|
18
|
+
@scorer.add(node, cost, label)
|
|
19
|
+
@scorer.visit(node.predicate)
|
|
20
|
+
@scorer.deeper { @scorer.visit(node.statements) }
|
|
21
|
+
tail(node.subsequent)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def tail(node)
|
|
25
|
+
case node
|
|
26
|
+
when Prism::IfNode then branch(node, 1, "elsif")
|
|
27
|
+
when Prism::ElseNode then otherwise(node)
|
|
43
28
|
end
|
|
44
29
|
end
|
|
30
|
+
|
|
31
|
+
def otherwise(node)
|
|
32
|
+
@scorer.add(node, 1, "else")
|
|
33
|
+
@scorer.deeper { @scorer.visit(node.statements) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def ternary(node)
|
|
37
|
+
@scorer.add(node, 1, "ternary")
|
|
38
|
+
node.compact_child_nodes.each { @scorer.visit(it) }
|
|
39
|
+
end
|
|
45
40
|
end
|
|
@@ -1,52 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
"if" => "flatten the branching — guard clauses, early returns, or polymorphism.",
|
|
8
|
-
"elsif" => "replace the elsif ladder with a lookup or polymorphic dispatch.",
|
|
9
|
-
"else" => "flatten the branching — guard clauses, early returns, or polymorphism.",
|
|
10
|
-
"case" => "a case this size often wants polymorphism or a dispatch table.",
|
|
11
|
-
"boolean" => "name the compound condition in a predicate method.",
|
|
12
|
-
"rescue" => "narrow the rescue, or lift error handling to the caller.",
|
|
13
|
-
"while" => "extract the loop body into its own method.",
|
|
14
|
-
"until" => "extract the loop body into its own method.",
|
|
15
|
-
"for" => "extract the loop body into its own method.",
|
|
16
|
-
"unless" => "invert to a guard clause or a named predicate.",
|
|
17
|
-
"ternary" => "extract the nested ternary into a named method."
|
|
18
|
-
}.freeze
|
|
19
|
-
|
|
20
|
-
def initialize(score)
|
|
21
|
-
@score = score
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def to_finding
|
|
25
|
-
Analysis::Finding.new(kind: "complexity", package: @score.subject, cycle: nil,
|
|
26
|
-
message:, evidence:)
|
|
27
|
-
end
|
|
3
|
+
class Hashira::Complexity::MethodFinding
|
|
4
|
+
def initialize(score)
|
|
5
|
+
@score = score
|
|
6
|
+
end
|
|
28
7
|
|
|
29
|
-
|
|
8
|
+
def to_finding
|
|
9
|
+
Hashira::Analysis::Finding.new(kind: "complexity", package: @score.subject, detail:, evidence:)
|
|
10
|
+
end
|
|
30
11
|
|
|
31
|
-
|
|
32
|
-
"#{@score.subject} — cognitive #{@score.cognitive}, #{@score.calls} calls " \
|
|
33
|
-
"(#{@score.file}:#{@score.line}). #{advice}"
|
|
34
|
-
end
|
|
12
|
+
private
|
|
35
13
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
14
|
+
def detail
|
|
15
|
+
{ cognitive: @score.cognitive, calls: @score.calls, site: "#{@score.file}:#{@score.line}", dominant: }
|
|
16
|
+
end
|
|
39
17
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
end
|
|
18
|
+
def evidence
|
|
19
|
+
@score.increments.group_by(&:label).map { |label, incs| lines(label, incs) }
|
|
20
|
+
end
|
|
44
21
|
|
|
45
|
-
|
|
22
|
+
def lines(label, incs)
|
|
23
|
+
lines = incs.map(&:line).uniq
|
|
24
|
+
"#{label} +#{incs.sum(&:cost)} (line#{"s" if lines.size > 1} #{lines.join(", ")})"
|
|
25
|
+
end
|
|
46
26
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
end
|
|
50
|
-
end
|
|
27
|
+
def dominant
|
|
28
|
+
@score.increments.group_by(&:label).transform_values { it.sum(&:cost) }.max_by(&:last).first
|
|
51
29
|
end
|
|
52
30
|
end
|
|
@@ -4,14 +4,16 @@ module Hashira
|
|
|
4
4
|
module Complexity
|
|
5
5
|
Increment = Data.define(:line, :cost, :label)
|
|
6
6
|
|
|
7
|
-
MethodScore =
|
|
8
|
-
|
|
7
|
+
MethodScore =
|
|
8
|
+
Data.define(:subject, :file, :line, :cognitive, :calls, :increments) do
|
|
9
|
+
def to_h = { subject:, file:, line:, cognitive:, calls: }
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
def cells = [subject, cognitive, calls, "#{file}:#{line}"]
|
|
12
|
+
end
|
|
12
13
|
|
|
13
|
-
ClassScore =
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
ClassScore =
|
|
15
|
+
Data.define(:name, :cognitive, :method_count, :peak) do
|
|
16
|
+
def cells = [name, cognitive, method_count, peak]
|
|
17
|
+
end
|
|
16
18
|
end
|
|
17
19
|
end
|
|
@@ -1,28 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@scorer = scorer
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def apply(node, nesting)
|
|
11
|
-
@scorer.visit(node.statements, nesting)
|
|
12
|
-
clauses(node.rescue_clause, nesting)
|
|
13
|
-
@scorer.visit(node.else_clause, nesting)
|
|
14
|
-
@scorer.visit(node.ensure_clause, nesting)
|
|
15
|
-
end
|
|
3
|
+
class Hashira::Complexity::RescueScan
|
|
4
|
+
def initialize(scorer)
|
|
5
|
+
@scorer = scorer
|
|
6
|
+
end
|
|
16
7
|
|
|
17
|
-
|
|
8
|
+
def apply(node)
|
|
9
|
+
@scorer.visit(node.statements)
|
|
10
|
+
clauses(node.rescue_clause)
|
|
11
|
+
@scorer.visit(node.else_clause)
|
|
12
|
+
@scorer.visit(node.ensure_clause)
|
|
13
|
+
end
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
return unless node
|
|
15
|
+
private
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
def clauses(node)
|
|
18
|
+
return unless node
|
|
19
|
+
@scorer.add(node, 1 + @scorer.nesting, "rescue")
|
|
20
|
+
@scorer.deeper { @scorer.visit(node.statements) }
|
|
21
|
+
clauses(node.subsequent)
|
|
27
22
|
end
|
|
28
23
|
end
|