hashira 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +141 -0
  3. data/README.md +53 -5
  4. data/lib/hashira/analysis/catalog.rb +27 -0
  5. data/lib/hashira/analysis/census.rb +42 -35
  6. data/lib/hashira/analysis/constant_registry.rb +43 -0
  7. data/lib/hashira/analysis/cycle_findings.rb +34 -29
  8. data/lib/hashira/analysis/cycle_search.rb +28 -35
  9. data/lib/hashira/analysis/cycles.rb +14 -0
  10. data/lib/hashira/analysis/definition.rb +22 -0
  11. data/lib/hashira/analysis/definitions.rb +22 -18
  12. data/lib/hashira/analysis/edge.rb +4 -3
  13. data/lib/hashira/analysis/edge_map.rb +23 -26
  14. data/lib/hashira/analysis/finding.rb +7 -6
  15. data/lib/hashira/analysis/folder_placement.rb +17 -0
  16. data/lib/hashira/analysis/folding.rb +54 -0
  17. data/lib/hashira/analysis/graph.rb +35 -38
  18. data/lib/hashira/analysis/metric.rb +8 -7
  19. data/lib/hashira/analysis/namespace_placement.rb +28 -0
  20. data/lib/hashira/analysis/namespace_prefix.rb +32 -0
  21. data/lib/hashira/analysis/naming.rb +14 -0
  22. data/lib/hashira/analysis/no_folding.rb +13 -0
  23. data/lib/hashira/analysis/node_walk.rb +3 -3
  24. data/lib/hashira/analysis/placement.rb +19 -0
  25. data/lib/hashira/analysis/references.rb +14 -13
  26. data/lib/hashira/analysis/resolver.rb +28 -0
  27. data/lib/hashira/analysis/roster.rb +25 -0
  28. data/lib/hashira/analysis/rule.rb +10 -14
  29. data/lib/hashira/analysis/sdp_check.rb +8 -12
  30. data/lib/hashira/analysis/sdp_violation_findings.rb +20 -18
  31. data/lib/hashira/analysis/syntax.rb +21 -6
  32. data/lib/hashira/analysis/type_walk.rb +7 -9
  33. data/lib/hashira/churn.rb +11 -13
  34. data/lib/hashira/ci/accepted.rb +27 -31
  35. data/lib/hashira/ci/baseline.rb +22 -24
  36. data/lib/hashira/ci/diff.rb +8 -7
  37. data/lib/hashira/ci/edge_diff_report.rb +13 -17
  38. data/lib/hashira/ci/finding_diff_report.rb +14 -18
  39. data/lib/hashira/ci/gate.rb +19 -23
  40. data/lib/hashira/ci/improvement.rb +9 -14
  41. data/lib/hashira/ci/ratchet.rb +36 -27
  42. data/lib/hashira/ci/ratchet_report.rb +26 -32
  43. data/lib/hashira/cli/arguments.rb +23 -0
  44. data/lib/hashira/cli/command_line.rb +55 -71
  45. data/lib/hashira/cli/fail_on.rb +15 -20
  46. data/lib/hashira/cli/options.rb +9 -4
  47. data/lib/hashira/cli/package_by.rb +14 -0
  48. data/lib/hashira/cli/run.rb +29 -27
  49. data/lib/hashira/cli/skip.rb +14 -20
  50. data/lib/hashira/cli/usage.rb +41 -41
  51. data/lib/hashira/cli.rb +17 -19
  52. data/lib/hashira/complexity/analyzer.rb +30 -32
  53. data/lib/hashira/complexity/boolean_run.rb +11 -15
  54. data/lib/hashira/complexity/cognitive_score.rb +61 -66
  55. data/lib/hashira/complexity/if_chain.rb +33 -38
  56. data/lib/hashira/complexity/method_finding.rb +43 -48
  57. data/lib/hashira/complexity/method_score.rb +9 -7
  58. data/lib/hashira/complexity/rescue_scan.rb +16 -21
  59. data/lib/hashira/complexity/rollup.rb +13 -15
  60. data/lib/hashira/diagram/dot.rb +8 -11
  61. data/lib/hashira/diagram/mermaid.rb +11 -16
  62. data/lib/hashira/diagram/renderer.rb +10 -15
  63. data/lib/hashira/duplication/analyzer.rb +10 -14
  64. data/lib/hashira/duplication/cluster.rb +15 -12
  65. data/lib/hashira/duplication/clusterer.rb +31 -29
  66. data/lib/hashira/duplication/delta.rb +29 -30
  67. data/lib/hashira/duplication/duplication_finding.rb +19 -21
  68. data/lib/hashira/duplication/extractor.rb +15 -19
  69. data/lib/hashira/duplication/fragment.rb +19 -23
  70. data/lib/hashira/duplication/grouping.rb +12 -16
  71. data/lib/hashira/duplication/index.rb +20 -24
  72. data/lib/hashira/duplication/maximal.rb +11 -15
  73. data/lib/hashira/duplication/near_miss.rb +19 -22
  74. data/lib/hashira/duplication/sequence.rb +20 -22
  75. data/lib/hashira/duplication/similarity.rb +28 -35
  76. data/lib/hashira/duplication/union_find.rb +10 -14
  77. data/lib/hashira/duplication/variance.rb +36 -44
  78. data/lib/hashira/error.rb +4 -2
  79. data/lib/hashira/hotspots/file_cost.rb +8 -7
  80. data/lib/hashira/hotspots/rollup.rb +19 -21
  81. data/lib/hashira/pipeline.rb +36 -31
  82. data/lib/hashira/project.rb +53 -32
  83. data/lib/hashira/report/complexity_table.rb +27 -31
  84. data/lib/hashira/report/dependency_map.rb +17 -22
  85. data/lib/hashira/report/finding_lines.rb +15 -19
  86. data/lib/hashira/report/graph_payload.rb +14 -20
  87. data/lib/hashira/report/hotspot_table.rb +32 -37
  88. data/lib/hashira/report/json.rb +30 -32
  89. data/lib/hashira/report/metrics_table.rb +46 -33
  90. data/lib/hashira/report/text.rb +59 -52
  91. data/lib/hashira/version.rb +1 -1
  92. data/lib/hashira.rb +92 -55
  93. metadata +16 -2
  94. data/lib/hashira/analysis/root_namespace.rb +0 -14
@@ -1,92 +1,76 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- class CLI
5
- class CommandLine
6
- DEFAULT_BASELINE = "hashira_baseline.json"
3
+ class Hashira::CLI::CommandLine
4
+ DEFAULT_BASELINE = "hashira_baseline.json"
7
5
 
8
- FORMATS = %w[text json dot mermaid].freeze
6
+ FORMATS = %w[text json dot mermaid].freeze
9
7
 
10
- CI_FLAGS = { "--update-baseline" => :update_baseline, "--ratchet" => :ratchet }.freeze
8
+ CI_FLAGS = { "--update-baseline" => :update, "--ratchet" => :ratchet }.freeze
11
9
 
12
- def initialize(argv)
13
- @arguments = argv.dup
14
- end
15
-
16
- def options = usage_options || parsed_options
17
-
18
- private
19
-
20
- def usage_options
21
- return usage(:help) if delete("--help") || delete("-h")
22
-
23
- usage(:version) if delete("--version")
24
- end
10
+ def initialize(argv)
11
+ @arguments = Hashira::CLI::Arguments.new(argv)
12
+ end
25
13
 
26
- def usage(mode) = Options.new(directories: [], mode:, baseline: nil, fail_on: [], skip: [])
14
+ def options = shortcuts || parsed
27
15
 
28
- def parsed_options
29
- values = flag_values
30
- mode = parse_mode(values[:fail_on])
31
- reject_unknown_flags
32
- Options.new(directories: @arguments, mode:, **values)
33
- end
16
+ private
34
17
 
35
- def flag_values
36
- { skip: Skip.parse(take_value("--skip")),
37
- fail_on: FailOn.parse(take_value("--fail-on")),
38
- baseline: take_value("--baseline") || DEFAULT_BASELINE }
39
- end
18
+ def shortcuts
19
+ return usage(:help) if delete("--help") || delete("-h")
20
+ usage(:version) if delete("--version")
21
+ end
40
22
 
41
- def parse_mode(fail_on)
42
- case (requested = requested_modes(fail_on).uniq(&:last))
43
- in [] then :text
44
- in [[_flag, mode]] then mode
45
- else conflict(requested)
46
- end
47
- end
23
+ def usage(mode)
24
+ Hashira::CLI::Options.new(directories: [], mode:, baseline: nil, fail_on: [], skip: [], packaging: :auto)
25
+ end
48
26
 
49
- def conflict(requested)
50
- raise Error, "conflicting options: #{requested.map(&:first).join(" and ")}"
51
- end
27
+ def parsed
28
+ values = flags
29
+ values[:mode] = mode(values[:fail_on])
30
+ Hashira::CLI::Options.new(directories: @arguments.rest, **values)
31
+ end
52
32
 
53
- def requested_modes(fail_on)
54
- ci = CI_FLAGS.filter_map { |flag, mode| [flag, mode] if delete(flag) }
55
- ci + fail_on_mode(fail_on) + format_modes
56
- end
33
+ def flags
34
+ {
35
+ skip: Hashira::CLI::Skip.parse(take("--skip", "")),
36
+ fail_on: Hashira::CLI::FailOn.parse(take("--fail-on", "")),
37
+ baseline: take("--baseline", DEFAULT_BASELINE), packaging: grouping
38
+ }
39
+ end
57
40
 
58
- def fail_on_mode(fail_on) = fail_on.empty? ? [] : [["--fail-on", :fail_on]]
41
+ def grouping = Hashira::CLI::PackageBy.parse(take("--package-by", ""))
59
42
 
60
- def format_modes
61
- format = take_format
62
- modes = format ? [["--format #{format}", format.to_sym]] : []
63
- delete("--json") ? modes + [["--json", :json]] : modes
64
- end
43
+ def mode(fail_on)
44
+ case (asked = requested(fail_on).uniq(&:last))
45
+ in [] then :text
46
+ in [[_flag, mode]] then mode
47
+ else conflict(asked)
48
+ end
49
+ end
65
50
 
66
- def take_format
67
- format = take_value("--format")
68
- return format unless format
69
- raise Error, "unknown --format #{format.inspect} (use: #{FORMATS.join(", ")})" unless FORMATS.include?(format)
51
+ def conflict(asked)
52
+ raise(Hashira::Error, "conflicting options: #{asked.map(&:first).join(" and ")}")
53
+ end
70
54
 
71
- format
72
- end
55
+ def requested(fail_on)
56
+ CI_FLAGS.filter_map { |flag, mode| [flag, mode] if delete(flag) } + gate(fail_on) + formats
57
+ end
73
58
 
74
- def take_value(flag)
75
- position = @arguments.index(flag)
76
- return nil unless position
59
+ def gate(fail_on) = fail_on.empty? ? [] : [["--fail-on", :fail_on]]
77
60
 
78
- _flag, value = @arguments.slice!(position, 2)
79
- raise Error, "#{flag} needs a value" unless value
61
+ def formats
62
+ chosen = format
63
+ modes = chosen.empty? ? [] : [["--format #{chosen}", chosen.to_sym]]
64
+ delete("--json") ? modes + [["--json", :json]] : modes
65
+ end
80
66
 
81
- value
82
- end
67
+ def format
68
+ wanted = take("--format", "")
69
+ return wanted if wanted.empty? || FORMATS.include?(wanted)
70
+ raise(Hashira::Error.unknown("--format", wanted, FORMATS))
71
+ end
83
72
 
84
- def delete(flag) = @arguments.delete(flag)
73
+ def take(flag, default) = @arguments.take(flag, default)
85
74
 
86
- def reject_unknown_flags
87
- stray = @arguments.find { it.start_with?("-") }
88
- raise Error, "unknown option #{stray}" if stray
89
- end
90
- end
91
- end
75
+ def delete(flag) = @arguments.delete(flag)
92
76
  end
@@ -1,28 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- class CLI
5
- module FailOn
6
- KINDS = {
7
- "cycles" => "cycle", "cycle" => "cycle",
8
- "sdp" => "sdp_violation", "sdp_violation" => "sdp_violation",
9
- "complexity" => "complexity",
10
- "duplication" => "duplication", "dupe" => "duplication"
11
- }.freeze
3
+ module Hashira::CLI::FailOn
4
+ KINDS = {
5
+ "cycles" => "cycle", "cycle" => "cycle",
6
+ "sdp" => "sdp_violation", "sdp_violation" => "sdp_violation",
7
+ "complexity" => "complexity",
8
+ "duplication" => "duplication", "dupe" => "duplication"
9
+ }.freeze
12
10
 
13
- module_function
11
+ module_function
14
12
 
15
- def parse(list)
16
- return [] unless list
17
-
18
- list.split(",").map { kind(it.strip) }.uniq
19
- end
13
+ def parse(list)
14
+ return [] unless list
15
+ list.split(",").map { kind(it.strip) }.uniq
16
+ end
20
17
 
21
- def kind(name)
22
- KINDS.fetch(name) do
23
- raise Error, "unknown --fail-on kind #{name.inspect} (use: #{KINDS.keys.join(", ")})"
24
- end
25
- end
18
+ def kind(name)
19
+ KINDS.fetch(name) do
20
+ raise(Hashira::Error, "unknown --fail-on kind #{name.inspect} (use: #{KINDS.keys.join(", ")})")
26
21
  end
27
22
  end
28
23
  end
@@ -1,9 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- class CLI
5
- Options = Data.define(:directories, :mode, :baseline, :fail_on, :skip) do
3
+ class Hashira::CLI
4
+ Options =
5
+ Data.define(:directories, :mode, :baseline, :fail_on, :skip, :packaging) do
6
6
  def self.parse(argv) = CommandLine.new(argv).options
7
+
8
+ def pipeline
9
+ Hashira::Pipeline.new(Hashira::Project.detect(directories), enabled: analyzers, packaging:)
10
+ end
11
+
12
+ def analyzers = Hashira::Pipeline::ANALYZERS - skip
7
13
  end
8
- end
9
14
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hashira::CLI::PackageBy
4
+ CHOICES = %i[auto folder namespace].freeze
5
+
6
+ module_function
7
+
8
+ def parse(value)
9
+ return :auto if value.empty?
10
+ choice = value.to_sym
11
+ return choice if CHOICES.include?(choice)
12
+ raise(Hashira::Error.unknown("--package-by", value, CHOICES))
13
+ end
14
+ end
@@ -1,44 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- class CLI
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
- def initialize(pipeline, options)
11
- @pipeline = pipeline
12
- @options = options
13
- end
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
+
15
+ def graph = @pipeline.graph
14
16
 
15
- def exit_code = send(MODES.fetch(@options.mode, :print_text))
17
+ def findings = @findings ||= Hashira::CI::Accepted.load(@options.baseline).screen(@pipeline.findings)
16
18
 
17
- private
19
+ def update = ratchet.update
18
20
 
19
- def graph = @pipeline.graph
21
+ def check = ratchet.check
20
22
 
21
- def ratchet = CI::Ratchet.new(graph, findings.all, @options.baseline)
23
+ def guard = gate.check
22
24
 
23
- def update_baseline = ratchet.update
25
+ def diagram = renderer.display
24
26
 
25
- def check_ratchet = ratchet.check
27
+ def json = report(Hashira::Report::Json)
26
28
 
27
- def findings = @findings ||= CI::Accepted.load(@options.baseline).screen(@pipeline.findings)
29
+ def text = report(Hashira::Report::Text)
28
30
 
29
- def check_gate = CI::Gate.new(findings, @options.fail_on).check
31
+ def report(kind) = kind.new(view).print
30
32
 
31
- def print_json = Report::Json.new(view).print
33
+ def ratchet = Hashira::CI::Ratchet.new(graph, findings.all, @options.baseline)
32
34
 
33
- def print_diagram = Diagram::Renderer.new(graph, @options.mode).display
35
+ def gate = Hashira::CI::Gate.new(findings, @options.fail_on)
34
36
 
35
- def print_text = Report::Text.new(view).print
37
+ def renderer = Hashira::Diagram::Renderer.new(graph, @options.mode)
36
38
 
37
- def view
38
- Report::View.new(project: @pipeline.project, graph: (graph if @pipeline.enabled?(:coupling)),
39
- complexity: @pipeline.complexity, duplication: @pipeline.duplication,
40
- hotspots: @pipeline.hotspots, findings:)
41
- end
42
- end
39
+ def view
40
+ Hashira::Report::View.new(
41
+ project: @pipeline.project, graph: (graph if @pipeline.enabled?(:coupling)),
42
+ complexity: @pipeline.complexity, duplication: @pipeline.duplication,
43
+ hotspots: @pipeline.hotspots, findings:
44
+ )
43
45
  end
44
46
  end
@@ -1,27 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- class CLI
5
- module Skip
6
- module_function
3
+ module Hashira::CLI::Skip
4
+ module_function
7
5
 
8
- def parse(list)
9
- return [] unless list
10
-
11
- keys = list.split(",").map { key(it.strip) }
12
- raise Error, "cannot skip every analyzer" if (Pipeline::ANALYZERS - keys).empty?
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
- def key(name)
18
- symbol = name.to_sym
19
- Pipeline::ANALYZERS.include?(symbol) ? symbol : unknown(name)
20
- end
13
+ def key(name)
14
+ symbol = name.to_sym
15
+ Hashira::Pipeline::ANALYZERS.include?(symbol) ? symbol : unknown(name)
16
+ end
21
17
 
22
- def unknown(name)
23
- raise Error, "unknown --skip #{name.inspect} (use: #{Pipeline::ANALYZERS.join(", ")})"
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
@@ -1,45 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- class CLI
5
- module Usage
6
- TEXT = <<~HELP
7
- Usage: hashira [DIRECTORY ...] [options]
8
-
9
- Coupling, cognitive-complexity, and duplication metrics for Ruby, via
10
- Prism, rolled up per file as a ranked list of hotspots. With no
11
- directory, auto-detects lib/<gem>.
12
-
13
- Options:
14
- --format FORMAT text (default), json, dot, or mermaid
15
- --json shorthand for --format json
16
- --fail-on KINDS exit 1 if findings exist; comma-separated
17
- kinds: cycles, sdp, complexity, duplication
18
- --skip ANALYZERS drop an analyzer; comma-separated: coupling, complexity, duplication
19
- --ratchet fail when edges or findings appear that the
20
- baseline lacks
21
- --update-baseline record the current edges and findings
22
- --baseline PATH baseline file (default: hashira_baseline.json)
23
- -h, --help print this help
24
- --version print the version
25
-
26
- Findings accepted by design can be recorded in the baseline as
27
- "accepted": [{"kind": "...", "package": "...", "reason": "..."}]
28
- they leave reports and gates, keeping a one-line reminder each.
29
- Clones are named by "digest" instead of "package", so an acceptance
30
- survives the lines above it moving. Read digests from --json.
31
- HELP
32
-
33
- module_function
34
-
35
- def help = emit(TEXT)
36
-
37
- def version = emit("hashira #{VERSION}")
38
-
39
- def emit(output)
40
- puts output
41
- 0
42
- end
43
- end
3
+ module Hashira::CLI::Usage
4
+ TEXT = <<~HELP
5
+ Usage: hashira [DIRECTORY ...] [options]
6
+
7
+ Coupling, cognitive-complexity, and duplication metrics for Ruby, via
8
+ Prism, rolled up per file as a ranked list of hotspots. With no
9
+ 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, complexity, duplication
16
+ --skip ANALYZERS drop an analyzer; comma-separated: coupling, complexity, duplication
17
+ --package-by WHAT group coupling by: auto, folder, or namespace
18
+ (top-level constant). Default auto: namespace for
19
+ Rails apps (config/application.rb in or beside the
20
+ analyzed directory), folder otherwise
21
+ --ratchet fail when edges or findings appear that the
22
+ baseline lacks
23
+ --update-baseline record the current edges and findings
24
+ --baseline PATH baseline file (default: hashira_baseline.json)
25
+ -h, --help print this help
26
+ --version print the version
27
+
28
+ Findings accepted by design can be recorded in the baseline as
29
+ "accepted": [{"kind": "...", "package": "...", "reason": "..."}]
30
+ they leave reports and gates, keeping a one-line reminder each.
31
+ Clones are named by "digest" instead of "package", so an acceptance
32
+ survives the lines above it moving. Read digests from --json.
33
+ HELP
34
+
35
+ module_function
36
+
37
+ def help = emit(TEXT)
38
+
39
+ def version = emit("hashira #{Hashira::VERSION}")
40
+
41
+ def emit(output)
42
+ puts(output)
43
+ 0
44
44
  end
45
45
  end
data/lib/hashira/cli.rb CHANGED
@@ -1,26 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- class CLI
5
- def self.run(argv)
6
- options = Options.parse(argv)
7
- usage?(options) ? Usage.public_send(options.mode) : new(options).run
8
- rescue Error => error
9
- report_failure(error)
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
- def self.report_failure(error)
15
- warn "hashira: #{error.message}"
16
- 1
17
- end
11
+ def self.usage?(options) = %i[help version].include?(options.mode)
18
12
 
19
- def initialize(options)
20
- @options = options
21
- @pipeline = Pipeline.new(Project.detect(options.directories), enabled: Pipeline::ANALYZERS - options.skip)
22
- end
13
+ def self.failure(error)
14
+ warn("hashira: #{error.message}")
15
+ 1
16
+ end
23
17
 
24
- def run = Run.new(@pipeline, @options).exit_code
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,45 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module Complexity
5
- class Analyzer
6
- THRESHOLD = 10
3
+ class Hashira::Complexity::Analyzer
4
+ THRESHOLD = 10
7
5
 
8
- def initialize(project, trees)
9
- @project = project
10
- @scores = trees.flat_map { |path, tree| scores_for(path, tree) }
11
- end
12
-
13
- def methods = @scores.sort_by { -it.cognitive }
14
-
15
- def classes = Rollup.new(@scores).classes.sort_by { -it.cognitive }
6
+ def initialize(project, trees)
7
+ @project = project
8
+ @scores = trees.flat_map { |path, tree| harvest(path, tree) }
9
+ end
16
10
 
17
- def findings = flagged.map { MethodFinding.new(it).to_finding }
11
+ def methods = @scores.sort_by { -it.cognitive }
18
12
 
19
- private
13
+ def classes = Hashira::Complexity::Rollup.new(@scores).classes.sort_by { -it.cognitive }
20
14
 
21
- def flagged = methods.select { it.cognitive >= THRESHOLD }
15
+ def findings = flagged.map { Hashira::Complexity::MethodFinding.new(it).to_finding }
22
16
 
23
- def scores_for(path, tree)
24
- rel = @project.relative(path)
25
- methods_in(tree).map { |full, node| build_score(rel, full, node) }
26
- end
17
+ private
27
18
 
28
- def methods_in(tree)
29
- found = []
30
- Analysis::TypeWalk.each_definition(tree) do |type_node, full|
31
- Analysis::Syntax.direct_definitions(type_node).each { found << [full, it] }
32
- end
33
- found
34
- end
19
+ def flagged = methods.select { it.cognitive >= THRESHOLD }
35
20
 
36
- def build_score(rel, full, node)
37
- score = CognitiveScore.new(node)
38
- MethodScore.new(subject: subject(full, node), file: rel, line: node.location.start_line,
39
- cognitive: score.total, calls: score.calls, increments: score.increments)
40
- end
21
+ def harvest(path, tree)
22
+ rel = @project.relative(path)
23
+ sites(tree).map { |full, node| score(rel, full, node) }
24
+ end
41
25
 
42
- def subject(full, node) = "#{full.join("::")}#{node.receiver ? "." : "#"}#{node.name}"
26
+ def sites(tree)
27
+ found = []
28
+ Hashira::Analysis::TypeWalk.each(tree) do |type_node, full|
29
+ Hashira::Analysis::Syntax.direct(type_node).each { found << [full, it] }
43
30
  end
31
+ found
44
32
  end
33
+
34
+ def score(rel, full, node)
35
+ score = Hashira::Complexity::CognitiveScore.new(node)
36
+ Hashira::Complexity::MethodScore.new(
37
+ subject: subject(full, node), file: rel, line: node.location.start_line,
38
+ cognitive: score.total, calls: score.calls, increments: score.increments
39
+ )
40
+ end
41
+
42
+ def subject(full, node) = "#{full.join("::")}#{node.receiver ? "." : "#"}#{node.name}"
45
43
  end
@@ -1,22 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hashira
4
- module Complexity
5
- class BooleanRun
6
- def initialize(scorer)
7
- @scorer = scorer
8
- end
3
+ class Hashira::Complexity::BooleanRun
4
+ def initialize(scorer)
5
+ @scorer = scorer
6
+ end
9
7
 
10
- def apply(node, nesting)
11
- @scorer.add(node, 1, "boolean")
12
- operands(node).each { @scorer.visit(it, nesting) }
13
- end
8
+ def apply(node, nesting)
9
+ @scorer.add(node, 1, "boolean")
10
+ operands(node).each { @scorer.visit(it, nesting) }
11
+ end
14
12
 
15
- private
13
+ private
16
14
 
17
- def operands(node)
18
- node.compact_child_nodes.flat_map { it.instance_of?(node.class) ? operands(it) : [it] }
19
- end
20
- end
15
+ def operands(node)
16
+ node.compact_child_nodes.flat_map { it.instance_of?(node.class) ? operands(it) : [it] }
21
17
  end
22
18
  end