hashira 0.7.0 → 0.9.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 +94 -0
- data/README.md +57 -3
- data/exe/hashira +1 -1
- data/lib/hashira/analysis/finding.rb +11 -1
- data/lib/hashira/analysis/syntax.rb +6 -6
- data/lib/hashira/analysis/type_walk.rb +4 -0
- data/lib/hashira/churn.rb +1 -10
- data/lib/hashira/ci/accepted.rb +9 -7
- data/lib/hashira/ci/baseline.rb +53 -33
- data/lib/hashira/ci/comparison.rb +33 -0
- data/lib/hashira/ci/diff.rb +0 -15
- data/lib/hashira/ci/gate.rb +3 -3
- data/lib/hashira/ci/mark.rb +7 -0
- data/lib/hashira/ci/ratchet.rb +12 -4
- data/lib/hashira/ci/ratchet_report.rb +4 -4
- data/lib/hashira/ci/scope.rb +0 -1
- data/lib/hashira/ci/slice.rb +21 -0
- data/lib/hashira/ci/sweep.rb +13 -0
- data/lib/hashira/cli/arguments.rb +12 -8
- data/lib/hashira/cli/choice.rb +9 -0
- data/lib/hashira/cli/command_line.rb +8 -5
- data/lib/hashira/cli/fail_on.rb +6 -6
- data/lib/hashira/cli/flag.rb +3 -1
- data/lib/hashira/cli/flags.rb +10 -0
- data/lib/hashira/cli/format.rb +1 -1
- data/lib/hashira/cli/needs.rb +13 -2
- data/lib/hashira/cli/only.rb +16 -0
- data/lib/hashira/cli/options.rb +8 -10
- data/lib/hashira/cli/package_by.rb +1 -1
- data/lib/hashira/cli/run.rb +9 -8
- data/lib/hashira/cli.rb +22 -21
- data/lib/hashira/complexity/cognitive_score.rb +44 -33
- data/lib/hashira/complexity/method_finding.rb +7 -5
- data/lib/hashira/complexity/method_score.rb +1 -1
- data/lib/hashira/complexity/scores.rb +11 -9
- data/lib/hashira/coupling/audiences.rb +3 -3
- data/lib/hashira/coupling/catalog.rb +8 -6
- data/lib/hashira/coupling/census.rb +42 -22
- data/lib/hashira/coupling/cycle_search.rb +11 -7
- data/lib/hashira/coupling/definitions.rb +1 -7
- data/lib/hashira/coupling/edge_map.rb +9 -8
- data/lib/hashira/coupling/folding.rb +5 -5
- data/lib/hashira/coupling/graph.rb +16 -9
- data/lib/hashira/coupling/metric.rb +3 -1
- data/lib/hashira/coupling/naming.rb +4 -4
- data/lib/hashira/coupling/references.rb +12 -12
- data/lib/hashira/coupling/report.rb +7 -9
- data/lib/hashira/coupling/roll_call.rb +1 -1
- data/lib/hashira/coupling/roll_call_findings.rb +1 -1
- data/lib/hashira/coupling/roster.rb +37 -12
- data/lib/hashira/coupling/rule.rb +1 -1
- data/lib/hashira/coupling/sdp_violation_findings.rb +3 -1
- data/lib/hashira/duplication/clones.rb +1 -1
- data/lib/hashira/duplication/clusters.rb +10 -7
- data/lib/hashira/duplication/delta.rb +1 -4
- data/lib/hashira/duplication/duplication_finding.rb +4 -2
- data/lib/hashira/duplication/fragment.rb +20 -2
- data/lib/hashira/duplication/harvest.rb +7 -5
- data/lib/hashira/duplication/index.rb +3 -2
- data/lib/hashira/duplication/literal.rb +25 -0
- data/lib/hashira/duplication/similarity.rb +2 -2
- data/lib/hashira/duplication/variance.rb +7 -1
- data/lib/hashira/error.rb +0 -3
- data/lib/hashira/focus.rb +29 -0
- data/lib/hashira/git_log.rb +19 -0
- data/lib/hashira/hotspots/file_cost.rb +1 -1
- data/lib/hashira/hotspots/rollup.rb +2 -2
- data/lib/hashira/pipeline.rb +22 -14
- data/lib/hashira/project.rb +35 -34
- data/lib/hashira/report/columns.rb +14 -8
- data/lib/hashira/report/graph_payload.rb +1 -1
- data/lib/hashira/report/hotspot_table.rb +1 -1
- data/lib/hashira/report/json.rb +2 -1
- data/lib/hashira/report/metrics_table.rb +3 -3
- data/lib/hashira/report/phrases.rb +23 -18
- data/lib/hashira/report/smell_phrases.rb +2 -2
- data/lib/hashira/smells/branches.rb +62 -0
- data/lib/hashira/smells/census.rb +11 -6
- data/lib/hashira/smells/check.rb +1 -5
- data/lib/hashira/smells/contexts.rb +7 -5
- data/lib/hashira/smells/control_parameter.rb +1 -1
- data/lib/hashira/smells/data_clump.rb +2 -4
- data/lib/hashira/smells/duplicate_method_call.rb +25 -5
- data/lib/hashira/smells/feature_envy.rb +3 -3
- data/lib/hashira/smells/foreign.rb +14 -14
- data/lib/hashira/smells/instance_variable_assumption.rb +5 -15
- data/lib/hashira/smells/kind.rb +9 -0
- data/lib/hashira/smells/lineage.rb +92 -0
- data/lib/hashira/smells/manual_dispatch.rb +1 -1
- data/lib/hashira/smells/module_initialize.rb +0 -2
- data/lib/hashira/smells/nil_check.rb +1 -1
- data/lib/hashira/smells/ownership.rb +19 -7
- data/lib/hashira/smells/param_check.rb +2 -2
- data/lib/hashira/smells/refs.rb +13 -7
- data/lib/hashira/smells/repeated_conditional.rb +1 -3
- data/lib/hashira/smells/report.rb +16 -7
- data/lib/hashira/smells/too_many_instance_variables.rb +3 -3
- data/lib/hashira/smells/visibility.rb +18 -11
- data/lib/hashira/trees.rb +8 -4
- data/lib/hashira/version.rb +1 -1
- data/lib/hashira.rb +12 -0
- metadata +13 -1
|
@@ -1,28 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Hashira::CLI::Arguments
|
|
4
|
-
def initialize(
|
|
5
|
-
@
|
|
4
|
+
def initialize(given)
|
|
5
|
+
@given = given
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
def take(flag, default)
|
|
9
|
-
position =
|
|
9
|
+
position = argv.index(flag)
|
|
10
10
|
return default unless position
|
|
11
|
-
_flag, value =
|
|
11
|
+
_flag, value = argv.slice!(position, 2)
|
|
12
12
|
vet(flag, value)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def vet(flag, value)
|
|
16
16
|
raise(Hashira::Error, "#{flag} needs a value") if value.to_s.strip.empty?
|
|
17
|
-
raise(Hashira::Error, "#{flag} given more than once") if
|
|
17
|
+
raise(Hashira::Error, "#{flag} given more than once") if argv.include?(flag)
|
|
18
18
|
value
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def delete(flag) =
|
|
21
|
+
def delete(flag) = argv.delete(flag)
|
|
22
22
|
|
|
23
23
|
def rest
|
|
24
|
-
stray =
|
|
24
|
+
stray = argv.find { it.start_with?("-") }
|
|
25
25
|
raise(Hashira::Error, "unknown option #{stray}") if stray
|
|
26
|
-
|
|
26
|
+
argv
|
|
27
27
|
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def argv = @_argv ||= @given.dup
|
|
28
32
|
end
|
|
@@ -2,23 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
class Hashira::CLI::CommandLine
|
|
4
4
|
def initialize(argv)
|
|
5
|
-
@
|
|
5
|
+
@argv = argv
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
def options = page || parsed
|
|
9
9
|
|
|
10
10
|
private
|
|
11
11
|
|
|
12
|
+
def arguments = @_arguments ||= Hashira::CLI::Arguments.new(@argv)
|
|
13
|
+
|
|
12
14
|
def page
|
|
13
15
|
flag = Hashira::CLI::FLAGS.select(&:page?).find { seen?(it) }
|
|
14
|
-
Hashira::CLI::Options.
|
|
16
|
+
Hashira::CLI::Options.build(flag.mode) if flag
|
|
15
17
|
end
|
|
16
18
|
|
|
17
|
-
def seen?(flag) = flag.names.any? {
|
|
19
|
+
def seen?(flag) = flag.names.any? { arguments.delete(it) }
|
|
18
20
|
|
|
19
21
|
def parsed
|
|
20
|
-
parses = Hashira::CLI::FLAGS.reject(&:page?).to_h { [it, it.read(
|
|
21
|
-
Hashira::CLI::Options.new(directories:
|
|
22
|
+
parses = Hashira::CLI::FLAGS.reject(&:page?).to_h { [it, it.read(arguments)] }
|
|
23
|
+
Hashira::CLI::Options.new(directories: arguments.rest, mode: mode(parses), **fields(parses))
|
|
24
|
+
.tap { Hashira::CLI::Needs.check(it) }
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
def fields(parses) = parses.to_h { |flag, value| [flag.field, value] }.except(nil)
|
data/lib/hashira/cli/fail_on.rb
CHANGED
|
@@ -5,12 +5,12 @@ require_relative "../pipeline"
|
|
|
5
5
|
module Hashira::CLI::FailOn
|
|
6
6
|
MEASURES = (Hashira::Pipeline::ANALYZERS - %i[coupling smells]).map(&:to_s).freeze
|
|
7
7
|
|
|
8
|
-
KINDS = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
KINDS = { "cycles" => "cycle", "sdp" => "sdp_violation", "dupe" => "duplication" }
|
|
9
|
+
.merge(Hashira::Pipeline::STRUCTURAL.to_h { [it, it] })
|
|
10
|
+
.merge(MEASURES.to_h { [it, it] })
|
|
11
|
+
.merge("smells" => Hashira::Pipeline::SMELLS)
|
|
12
|
+
.merge(Hashira::Pipeline::SMELLS.to_h { [it, it] })
|
|
13
|
+
.freeze
|
|
14
14
|
|
|
15
15
|
OWNERS = {
|
|
16
16
|
**Hashira::Pipeline::STRUCTURAL.to_h { [it, :coupling] },
|
data/lib/hashira/cli/flag.rb
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Hashira::CLI
|
|
4
|
+
BLANK = %i[arg field parse mode].to_h { [it, nil] }.freeze
|
|
5
|
+
|
|
4
6
|
Flag =
|
|
5
7
|
Data.define(:name, :arg, :default, :field, :parse, :mode, :text) do
|
|
6
|
-
def initialize(**attributes) = super(
|
|
8
|
+
def initialize(**attributes) = super(**BLANK, default: "", **attributes)
|
|
7
9
|
|
|
8
10
|
def read(arguments) = arg ? take(arguments) : arguments.delete(name)
|
|
9
11
|
|
data/lib/hashira/cli/flags.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "fail_on"
|
|
4
4
|
require_relative "flag"
|
|
5
5
|
require_relative "format"
|
|
6
|
+
require_relative "only"
|
|
6
7
|
require_relative "package_by"
|
|
7
8
|
require_relative "skip"
|
|
8
9
|
require_relative "top"
|
|
@@ -31,6 +32,15 @@ class Hashira::CLI
|
|
|
31
32
|
name: "--skip", arg: "ANALYZERS", field: :skip, parse: Skip,
|
|
32
33
|
text: ["drop an analyzer; comma-separated: coupling,", "complexity, duplication, smells"]
|
|
33
34
|
),
|
|
35
|
+
Flag.new(
|
|
36
|
+
name: "--only", arg: "PATHS", field: :only, parse: Only,
|
|
37
|
+
text: [
|
|
38
|
+
"keep only the findings that name these files;",
|
|
39
|
+
"comma-separated. The whole project is still read,",
|
|
40
|
+
"so cross-file signals stay right. Meant for hooks:",
|
|
41
|
+
"hashira --only $CHANGED --ratchet"
|
|
42
|
+
]
|
|
43
|
+
),
|
|
34
44
|
Flag.new(
|
|
35
45
|
name: "--top", arg: "N", field: :top, parse: Top,
|
|
36
46
|
text: [
|
data/lib/hashira/cli/format.rb
CHANGED
data/lib/hashira/cli/needs.rb
CHANGED
|
@@ -12,10 +12,21 @@ module Hashira::CLI::Needs
|
|
|
12
12
|
skip = options.skip
|
|
13
13
|
drawing(mode, skip)
|
|
14
14
|
gate(options.fail_on, skip)
|
|
15
|
-
shaping(mode)
|
|
15
|
+
shaping(options, mode)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def shaping(mode)
|
|
18
|
+
def shaping(options, mode)
|
|
19
|
+
compacting(mode) if options.compact
|
|
20
|
+
focusing(mode) unless options.only.empty?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def focusing(mode)
|
|
24
|
+
raise(Hashira::Error, "--only narrows the findings, but --update-baseline records them all") if mode == :update
|
|
25
|
+
return unless DIAGRAMS.include?(mode)
|
|
26
|
+
raise(Hashira::Error, "--format #{mode} draws the coupling graph, which --only cannot narrow")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def compacting(mode)
|
|
19
30
|
raise(Hashira::Error, "--compact shapes JSON, but this run emits #{mode}") unless mode == :json
|
|
20
31
|
end
|
|
21
32
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hashira::CLI::Only
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
def parse(value)
|
|
7
|
+
return [] if value.empty?
|
|
8
|
+
value.split(",").map { vet(it.strip) }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def vet(path)
|
|
12
|
+
here = path.delete_prefix("#{Dir.pwd}/").delete_prefix("./")
|
|
13
|
+
raise(Hashira::Error, "--only #{path.inspect} is not a file here") unless File.file?(here)
|
|
14
|
+
here
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/hashira/cli/options.rb
CHANGED
|
@@ -1,25 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Hashira::CLI
|
|
4
|
-
PAGE = { directories: [], baseline: "", fail_on: [], skip: [], packaging: :auto, top: nil, compact: nil }.freeze
|
|
5
|
-
|
|
6
4
|
Options =
|
|
7
|
-
Data.define(:directories, :mode, :baseline, :fail_on, :skip, :packaging, :top, :compact) do
|
|
5
|
+
Data.define(:directories, :mode, :baseline, :fail_on, :skip, :only, :packaging, :top, :compact) do
|
|
8
6
|
def self.parse(argv) = CommandLine.new(argv).options
|
|
9
7
|
|
|
10
|
-
def self.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
def self.build(mode)
|
|
9
|
+
new(
|
|
10
|
+
directories: [], baseline: "", fail_on: [], skip: [], only: [],
|
|
11
|
+
packaging: :auto, top: nil, compact: nil, mode:
|
|
12
|
+
)
|
|
15
13
|
end
|
|
16
14
|
|
|
17
15
|
def pipeline
|
|
18
|
-
chosen = Hashira::Project.
|
|
16
|
+
chosen = Hashira::Project.new(directories)
|
|
19
17
|
told = Hashira::Report::Notices.new
|
|
20
18
|
told.scanning(chosen.files.size)
|
|
21
19
|
told.rails if directories.empty? && File.exist?("config/application.rb")
|
|
22
|
-
Hashira::Pipeline.new(chosen, enabled: analyzers, packaging:)
|
|
20
|
+
Hashira::Pipeline.new(chosen, enabled: analyzers, packaging:, only:)
|
|
23
21
|
end
|
|
24
22
|
|
|
25
23
|
def analyzers = Hashira::Pipeline::ANALYZERS - skip
|
|
@@ -9,6 +9,6 @@ module Hashira::CLI::PackageBy
|
|
|
9
9
|
return :auto if value.empty?
|
|
10
10
|
choice = value.to_sym
|
|
11
11
|
return choice if CHOICES.include?(choice)
|
|
12
|
-
raise(Hashira::
|
|
12
|
+
raise(Hashira::CLI::Choice.unknown("--package-by", value, CHOICES))
|
|
13
13
|
end
|
|
14
14
|
end
|
data/lib/hashira/cli/run.rb
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Hashira::CLI::Run
|
|
4
|
-
MODES = { update: :update, ratchet: :check, fail_on: :guard, json: :json, dot: :diagram, mermaid: :diagram }.freeze
|
|
5
|
-
|
|
6
4
|
def initialize(pipeline, options)
|
|
7
5
|
@pipeline = pipeline
|
|
8
6
|
@options = options
|
|
9
7
|
end
|
|
10
8
|
|
|
9
|
+
MODES = { update: :update, ratchet: :check, fail_on: :guard }
|
|
10
|
+
.merge(json: :json, dot: :diagram, mermaid: :diagram).freeze
|
|
11
|
+
|
|
11
12
|
def status = __send__(MODES.fetch(@options.mode, :text))
|
|
12
13
|
|
|
13
14
|
private
|
|
14
15
|
|
|
15
16
|
def graph = @pipeline.graph
|
|
16
17
|
|
|
17
|
-
def findings = @
|
|
18
|
+
def findings = @_findings ||= accepted.screen(@pipeline.findings)
|
|
18
19
|
|
|
19
20
|
def accepted
|
|
20
21
|
path = @options.baseline
|
|
21
|
-
stop = Hashira::CI::Baseline.
|
|
22
|
+
stop = Hashira::CI::Baseline.new(path).trouble
|
|
22
23
|
raise(Hashira::Error, stop) if stop
|
|
23
|
-
Hashira::CI::Accepted.
|
|
24
|
+
Hashira::CI::Accepted.build(path)
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
def update
|
|
@@ -32,7 +33,7 @@ class Hashira::CLI::Run
|
|
|
32
33
|
def check
|
|
33
34
|
stop = ratchet.blocker
|
|
34
35
|
raise(Hashira::Error, stop) if stop
|
|
35
|
-
ratchet.check
|
|
36
|
+
ratchet.check(@pipeline.focus)
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
def guard = gate.check
|
|
@@ -55,10 +56,10 @@ class Hashira::CLI::Run
|
|
|
55
56
|
told.unparsed(broken.size, broken.first(3).join(", ")) unless broken.empty?
|
|
56
57
|
end
|
|
57
58
|
|
|
58
|
-
def ratchet = @
|
|
59
|
+
def ratchet = @_ratchet ||= Hashira::CI::Ratchet.new(graph, findings.all, baseline)
|
|
59
60
|
|
|
60
61
|
def baseline
|
|
61
|
-
Hashira::CI::Baseline.
|
|
62
|
+
Hashira::CI::Baseline.new(@options.baseline, analyzers: @options.analyzers, targets:)
|
|
62
63
|
end
|
|
63
64
|
|
|
64
65
|
def targets = @pipeline.project.directories
|
data/lib/hashira/cli.rb
CHANGED
|
@@ -4,40 +4,41 @@ class Hashira::CLI
|
|
|
4
4
|
MISUSE = 2
|
|
5
5
|
BROKEN = 70
|
|
6
6
|
|
|
7
|
-
def
|
|
8
|
-
|
|
7
|
+
def initialize(argv)
|
|
8
|
+
@argv = argv
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def status
|
|
12
|
+
dispatch(Options.parse(@argv))
|
|
9
13
|
rescue Hashira::Error => error
|
|
10
14
|
failure(error)
|
|
11
15
|
rescue StandardError => error
|
|
12
16
|
crash(error)
|
|
13
17
|
end
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def dispatch(options) = usage?(options) ? Usage.public_send(options.mode) : timed(options)
|
|
22
|
+
|
|
23
|
+
def usage?(options) = Usage::PAGES.include?(options.mode)
|
|
24
|
+
|
|
25
|
+
def timed(options)
|
|
26
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
27
|
+
pipeline = options.pipeline
|
|
28
|
+
Run.new(pipeline, options).status.tap do
|
|
29
|
+
Hashira::Report::Notices.new.finished(pipeline.project.files.size, elapsed(started))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
16
32
|
|
|
17
|
-
def
|
|
33
|
+
def elapsed(started) = format("%.1f", Process.clock_gettime(Process::CLOCK_MONOTONIC) - started)
|
|
18
34
|
|
|
19
|
-
def
|
|
35
|
+
def failure(error)
|
|
20
36
|
Kernel.warn("hashira: #{error.message}")
|
|
21
37
|
MISUSE
|
|
22
38
|
end
|
|
23
39
|
|
|
24
|
-
def
|
|
40
|
+
def crash(error)
|
|
25
41
|
Hashira::Report::Notices.new.crashed(error)
|
|
26
42
|
BROKEN
|
|
27
43
|
end
|
|
28
|
-
|
|
29
|
-
def initialize(options)
|
|
30
|
-
@started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
31
|
-
@options = options
|
|
32
|
-
@notices = Hashira::Report::Notices.new
|
|
33
|
-
@pipeline = options.pipeline
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def run
|
|
37
|
-
Run.new(@pipeline, @options).status.tap { @notices.finished(@pipeline.project.files.size, elapsed) }
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
private
|
|
41
|
-
|
|
42
|
-
def elapsed = format("%.1f", Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started)
|
|
43
44
|
end
|
|
@@ -3,37 +3,35 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
class Hashira::Complexity::CognitiveScore
|
|
6
|
-
HANDLERS = {
|
|
7
|
-
Prism::
|
|
8
|
-
Prism::UnlessNode => :on_nester,
|
|
9
|
-
Prism::
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Prism::CaseMatchNode =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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)
|
|
6
|
+
HANDLERS = { Prism::IfNode => :on_if, Prism::BeginNode => :on_begin, Prism::BlockNode => :on_block }
|
|
7
|
+
.merge(Prism::AndNode => :on_boolean, Prism::OrNode => :on_boolean, Prism::CallNode => :on_call)
|
|
8
|
+
.merge(Prism::UnlessNode => :on_nester, Prism::WhileNode => :on_nester, Prism::UntilNode => :on_nester)
|
|
9
|
+
.merge(Prism::ForNode => :on_nester, Prism::CaseNode => :on_nester, Prism::CaseMatchNode => :on_nester)
|
|
10
|
+
.freeze
|
|
11
|
+
|
|
12
|
+
LABELS = { Prism::UnlessNode => "unless", Prism::WhileNode => "while", Prism::UntilNode => "until" }
|
|
13
|
+
.merge(Prism::ForNode => "for", Prism::CaseNode => "case", Prism::CaseMatchNode => "case").freeze
|
|
14
|
+
|
|
15
|
+
def initialize(node)
|
|
16
|
+
@node = node
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def increments
|
|
20
|
+
walked
|
|
21
|
+
@_increments
|
|
32
22
|
end
|
|
33
23
|
|
|
34
|
-
|
|
24
|
+
def calls
|
|
25
|
+
walked
|
|
26
|
+
@_calls
|
|
27
|
+
end
|
|
35
28
|
|
|
36
|
-
def
|
|
29
|
+
def nesting
|
|
30
|
+
walked
|
|
31
|
+
@_nesting
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def total = increments.sum(&:cost)
|
|
37
35
|
|
|
38
36
|
def visit(node)
|
|
39
37
|
return unless node
|
|
@@ -41,28 +39,41 @@ class Hashira::Complexity::CognitiveScore
|
|
|
41
39
|
end
|
|
42
40
|
|
|
43
41
|
def add(node, cost, label)
|
|
44
|
-
@
|
|
42
|
+
@_increments << Hashira::Complexity::Increment.new(line: node.location.start_line, cost:, label:)
|
|
45
43
|
end
|
|
46
44
|
|
|
47
45
|
def deeper
|
|
48
|
-
@
|
|
46
|
+
@_nesting += 1
|
|
49
47
|
yield
|
|
50
|
-
@
|
|
48
|
+
@_nesting -= 1
|
|
51
49
|
end
|
|
52
50
|
|
|
53
51
|
private
|
|
54
52
|
|
|
53
|
+
def walked
|
|
54
|
+
return if @_walked
|
|
55
|
+
@_walked = true
|
|
56
|
+
blank
|
|
57
|
+
visit(@node.body)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def blank
|
|
61
|
+
@_increments = []
|
|
62
|
+
@_calls = 0
|
|
63
|
+
@_nesting = 0
|
|
64
|
+
end
|
|
65
|
+
|
|
55
66
|
def descend(node) = node.compact_child_nodes.each { visit(it) }
|
|
56
67
|
|
|
57
68
|
def on_call(node)
|
|
58
|
-
@
|
|
69
|
+
@_calls += 1
|
|
59
70
|
descend(node)
|
|
60
71
|
end
|
|
61
72
|
|
|
62
73
|
def on_block(node) = deeper { descend(node) }
|
|
63
74
|
|
|
64
75
|
def on_nester(node)
|
|
65
|
-
add(node, 1 + @
|
|
76
|
+
add(node, 1 + @_nesting, LABELS.fetch(node.class))
|
|
66
77
|
deeper { descend(node) }
|
|
67
78
|
end
|
|
68
79
|
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
class Hashira::Complexity::MethodFinding
|
|
4
4
|
KIND = "complexity"
|
|
5
5
|
|
|
6
|
+
Effort = Data.define(:cognitive, :calls, :site, :dominant)
|
|
7
|
+
|
|
6
8
|
def initialize(score)
|
|
7
9
|
@score = score
|
|
8
10
|
end
|
|
@@ -14,16 +16,16 @@ class Hashira::Complexity::MethodFinding
|
|
|
14
16
|
private
|
|
15
17
|
|
|
16
18
|
def detail
|
|
17
|
-
|
|
19
|
+
Effort.new(cognitive: @score.cognitive, calls: @score.calls, site: "#{@score.file}:#{@score.line}", dominant:)
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def evidence
|
|
21
|
-
@score.increments.group_by(&:label).map { |label,
|
|
23
|
+
@score.increments.group_by(&:label).map { |label, increments| lines(label, increments) }
|
|
22
24
|
end
|
|
23
25
|
|
|
24
|
-
def lines(label,
|
|
25
|
-
lines =
|
|
26
|
-
"#{label} +#{
|
|
26
|
+
def lines(label, increments)
|
|
27
|
+
lines = increments.map(&:line).uniq
|
|
28
|
+
"#{label} +#{increments.sum(&:cost)} (line#{"s" if lines.size > 1} #{lines.join(", ")})"
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
def dominant
|
|
@@ -6,7 +6,7 @@ module Hashira
|
|
|
6
6
|
|
|
7
7
|
MethodScore =
|
|
8
8
|
Data.define(:subject, :file, :line, :cognitive, :calls, :increments) do
|
|
9
|
-
def to_h =
|
|
9
|
+
def to_h = super.except(:increments)
|
|
10
10
|
|
|
11
11
|
def cells = [subject, cognitive, calls, "#{file}:#{line}"]
|
|
12
12
|
end
|
|
@@ -5,35 +5,37 @@ class Hashira::Complexity::Scores
|
|
|
5
5
|
|
|
6
6
|
def initialize(project, trees)
|
|
7
7
|
@project = project
|
|
8
|
-
@
|
|
8
|
+
@trees = trees
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def ranked =
|
|
11
|
+
def ranked = scores.sort_by { -it.cognitive }
|
|
12
12
|
|
|
13
|
-
def classes = Hashira::Complexity::Rollup.new(
|
|
13
|
+
def classes = Hashira::Complexity::Rollup.new(scores).classes.sort_by { -it.cognitive }
|
|
14
14
|
|
|
15
15
|
def findings = flagged.map { Hashira::Complexity::MethodFinding.new(it).to_finding }
|
|
16
16
|
|
|
17
17
|
private
|
|
18
18
|
|
|
19
|
+
def scores = @_scores ||= @trees.flat_map { |path, tree| harvest(path, tree) }
|
|
20
|
+
|
|
19
21
|
def flagged = ranked.select { it.cognitive >= THRESHOLD }
|
|
20
22
|
|
|
21
23
|
def harvest(path, tree)
|
|
22
|
-
|
|
23
|
-
sites(tree).map { |full, node| score(
|
|
24
|
+
relative = @project.relative(path)
|
|
25
|
+
sites(tree).map { |full, node| score(relative, full, node) }
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def sites(tree)
|
|
27
29
|
found = []
|
|
28
|
-
Hashira::Analysis::TypeWalk.each(tree) do |
|
|
29
|
-
Hashira::Analysis::Syntax.direct(
|
|
30
|
+
Hashira::Analysis::TypeWalk.each(tree) do |type, full|
|
|
31
|
+
Hashira::Analysis::Syntax.direct(type).each { found << [full, it] }
|
|
30
32
|
end
|
|
31
33
|
found
|
|
32
34
|
end
|
|
33
35
|
|
|
34
|
-
def score(
|
|
36
|
+
def score(relative, full, node)
|
|
35
37
|
Hashira::Complexity::MethodScore.new(
|
|
36
|
-
subject: subject(full, node), file:
|
|
38
|
+
subject: subject(full, node), file: relative, line: node.location.start_line,
|
|
37
39
|
**tallies(Hashira::Complexity::CognitiveScore.new(node))
|
|
38
40
|
)
|
|
39
41
|
end
|
|
@@ -11,13 +11,13 @@ class Hashira::Coupling::Audiences
|
|
|
11
11
|
|
|
12
12
|
def split? = parts.size >= 2
|
|
13
13
|
|
|
14
|
-
def parts = @
|
|
14
|
+
def parts = @_parts ||= [common, *slices].compact.select { it.constants.size >= MIN }
|
|
15
15
|
|
|
16
16
|
private
|
|
17
17
|
|
|
18
|
-
def clients = @
|
|
18
|
+
def clients = @_clients ||= @usage.keys.sort
|
|
19
19
|
|
|
20
|
-
def shared = @
|
|
20
|
+
def shared = @_shared ||= tally.select { |_constant, users| users.size * 2 > clients.size }.keys.to_set
|
|
21
21
|
|
|
22
22
|
def tally
|
|
23
23
|
clients.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |client, seen|
|
|
@@ -5,15 +5,13 @@ class Hashira::Coupling::Catalog
|
|
|
5
5
|
|
|
6
6
|
def initialize(definitions)
|
|
7
7
|
@definitions = definitions
|
|
8
|
-
@naming = Hashira::Coupling::Naming.new(definitions)
|
|
9
|
-
@entries = definitions.map { |node, full, folder| entry(node, full, folder) }
|
|
10
8
|
end
|
|
11
9
|
|
|
12
|
-
def each(&) =
|
|
10
|
+
def each(&) = entries.each(&)
|
|
13
11
|
|
|
14
|
-
def prefix =
|
|
12
|
+
def prefix = naming.segments
|
|
15
13
|
|
|
16
|
-
def strip(path) =
|
|
14
|
+
def strip(path) = naming.strip(path)
|
|
17
15
|
|
|
18
16
|
def roots = @definitions.roots
|
|
19
17
|
|
|
@@ -21,7 +19,11 @@ class Hashira::Coupling::Catalog
|
|
|
21
19
|
|
|
22
20
|
private
|
|
23
21
|
|
|
22
|
+
def naming = @_naming ||= Hashira::Coupling::Naming.new(@definitions)
|
|
23
|
+
|
|
24
|
+
def entries = @_entries ||= @definitions.map { |node, full, folder| entry(node, full, folder) }
|
|
25
|
+
|
|
24
26
|
def entry(node, full, folder)
|
|
25
|
-
Hashira::Coupling::Definition.new(node:, path:
|
|
27
|
+
Hashira::Coupling::Definition.new(node:, path: naming.strip(full), folder:)
|
|
26
28
|
end
|
|
27
29
|
end
|