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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +141 -0
- data/README.md +53 -5
- data/lib/hashira/analysis/catalog.rb +27 -0
- data/lib/hashira/analysis/census.rb +42 -35
- data/lib/hashira/analysis/constant_registry.rb +43 -0
- data/lib/hashira/analysis/cycle_findings.rb +34 -29
- data/lib/hashira/analysis/cycle_search.rb +28 -35
- data/lib/hashira/analysis/cycles.rb +14 -0
- data/lib/hashira/analysis/definition.rb +22 -0
- data/lib/hashira/analysis/definitions.rb +22 -18
- data/lib/hashira/analysis/edge.rb +4 -3
- data/lib/hashira/analysis/edge_map.rb +23 -26
- data/lib/hashira/analysis/finding.rb +7 -6
- data/lib/hashira/analysis/folder_placement.rb +17 -0
- data/lib/hashira/analysis/folding.rb +54 -0
- data/lib/hashira/analysis/graph.rb +35 -38
- data/lib/hashira/analysis/metric.rb +8 -7
- data/lib/hashira/analysis/namespace_placement.rb +28 -0
- data/lib/hashira/analysis/namespace_prefix.rb +32 -0
- data/lib/hashira/analysis/naming.rb +14 -0
- data/lib/hashira/analysis/no_folding.rb +13 -0
- data/lib/hashira/analysis/node_walk.rb +3 -3
- data/lib/hashira/analysis/placement.rb +19 -0
- data/lib/hashira/analysis/references.rb +14 -13
- data/lib/hashira/analysis/resolver.rb +28 -0
- data/lib/hashira/analysis/roster.rb +25 -0
- data/lib/hashira/analysis/rule.rb +10 -14
- data/lib/hashira/analysis/sdp_check.rb +8 -12
- data/lib/hashira/analysis/sdp_violation_findings.rb +20 -18
- 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 +36 -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 +15 -20
- data/lib/hashira/cli/options.rb +9 -4
- data/lib/hashira/cli/package_by.rb +14 -0
- data/lib/hashira/cli/run.rb +29 -27
- data/lib/hashira/cli/skip.rb +14 -20
- data/lib/hashira/cli/usage.rb +41 -41
- data/lib/hashira/cli.rb +17 -19
- data/lib/hashira/complexity/analyzer.rb +30 -32
- data/lib/hashira/complexity/boolean_run.rb +11 -15
- data/lib/hashira/complexity/cognitive_score.rb +61 -66
- data/lib/hashira/complexity/if_chain.rb +33 -38
- data/lib/hashira/complexity/method_finding.rb +43 -48
- 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/diagram/dot.rb +8 -11
- data/lib/hashira/diagram/mermaid.rb +11 -16
- data/lib/hashira/diagram/renderer.rb +10 -15
- data/lib/hashira/duplication/analyzer.rb +10 -14
- data/lib/hashira/duplication/cluster.rb +15 -12
- data/lib/hashira/duplication/clusterer.rb +31 -29
- data/lib/hashira/duplication/delta.rb +29 -30
- data/lib/hashira/duplication/duplication_finding.rb +19 -21
- data/lib/hashira/duplication/extractor.rb +15 -19
- data/lib/hashira/duplication/fragment.rb +19 -23
- data/lib/hashira/duplication/grouping.rb +12 -16
- 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 +28 -35
- data/lib/hashira/duplication/union_find.rb +10 -14
- data/lib/hashira/duplication/variance.rb +36 -44
- 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 +36 -31
- data/lib/hashira/project.rb +53 -32
- 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 +30 -32
- data/lib/hashira/report/metrics_table.rb +46 -33
- data/lib/hashira/report/text.rb +59 -52
- data/lib/hashira/version.rb +1 -1
- data/lib/hashira.rb +92 -55
- metadata +16 -2
- data/lib/hashira/analysis/root_namespace.rb +0 -14
|
@@ -1,92 +1,76 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class CommandLine
|
|
6
|
-
DEFAULT_BASELINE = "hashira_baseline.json"
|
|
3
|
+
class Hashira::CLI::CommandLine
|
|
4
|
+
DEFAULT_BASELINE = "hashira_baseline.json"
|
|
7
5
|
|
|
8
|
-
|
|
6
|
+
FORMATS = %w[text json dot mermaid].freeze
|
|
9
7
|
|
|
10
|
-
|
|
8
|
+
CI_FLAGS = { "--update-baseline" => :update, "--ratchet" => :ratchet }.freeze
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
14
|
+
def options = shortcuts || parsed
|
|
27
15
|
|
|
28
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
41
|
+
def grouping = Hashira::CLI::PackageBy.parse(take("--package-by", ""))
|
|
59
42
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
73
|
+
def take(flag, default) = @arguments.take(flag, default)
|
|
85
74
|
|
|
86
|
-
|
|
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
|
data/lib/hashira/cli/fail_on.rb
CHANGED
|
@@ -1,28 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module Hashira
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
11
|
+
module_function
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
end
|
|
13
|
+
def parse(list)
|
|
14
|
+
return [] unless list
|
|
15
|
+
list.split(",").map { kind(it.strip) }.uniq
|
|
16
|
+
end
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
data/lib/hashira/cli/options.rb
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
data/lib/hashira/cli/run.rb
CHANGED
|
@@ -1,44 +1,46 @@
|
|
|
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
|
+
|
|
15
|
+
def graph = @pipeline.graph
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
def findings = @findings ||= Hashira::CI::Accepted.load(@options.baseline).screen(@pipeline.findings)
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
def update = ratchet.update
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
def check = ratchet.check
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
def guard = gate.check
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
def diagram = renderer.display
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
def json = report(Hashira::Report::Json)
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
def text = report(Hashira::Report::Text)
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
def report(kind) = kind.new(view).print
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
def ratchet = Hashira::CI::Ratchet.new(graph, findings.all, @options.baseline)
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
def gate = Hashira::CI::Gate.new(findings, @options.fail_on)
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
def renderer = Hashira::Diagram::Renderer.new(graph, @options.mode)
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
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,45 @@
|
|
|
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, 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
|
-
|
|
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,45 +1,43 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class Analyzer
|
|
6
|
-
THRESHOLD = 10
|
|
3
|
+
class Hashira::Complexity::Analyzer
|
|
4
|
+
THRESHOLD = 10
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
11
|
+
def methods = @scores.sort_by { -it.cognitive }
|
|
18
12
|
|
|
19
|
-
|
|
13
|
+
def classes = Hashira::Complexity::Rollup.new(@scores).classes.sort_by { -it.cognitive }
|
|
20
14
|
|
|
21
|
-
|
|
15
|
+
def findings = flagged.map { Hashira::Complexity::MethodFinding.new(it).to_finding }
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
rel = @project.relative(path)
|
|
25
|
-
methods_in(tree).map { |full, node| build_score(rel, full, node) }
|
|
26
|
-
end
|
|
17
|
+
private
|
|
27
18
|
|
|
28
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, nesting)
|
|
9
|
+
@scorer.add(node, 1, "boolean")
|
|
10
|
+
operands(node).each { @scorer.visit(it, nesting) }
|
|
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 { it.instance_of?(node.class) ? operands(it) : [it] }
|
|
21
17
|
end
|
|
22
18
|
end
|