hashira 0.5.0 → 0.5.1
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 +28 -0
- data/README.md +4 -5
- data/lib/hashira/analysis/syntax.rb +6 -2
- data/lib/hashira/churn.rb +1 -1
- data/lib/hashira/cli/command_line.rb +10 -49
- data/lib/hashira/cli/fail_on.rb +1 -7
- data/lib/hashira/cli/flag.rb +29 -0
- data/lib/hashira/cli/flags.rb +50 -0
- data/lib/hashira/cli/format.rb +13 -0
- data/lib/hashira/cli/options.rb +2 -0
- data/lib/hashira/cli/usage.rb +17 -21
- data/lib/hashira/cli.rb +1 -1
- data/lib/hashira/complexity/method_finding.rb +3 -1
- data/lib/hashira/coupling/census.rb +1 -1
- data/lib/hashira/coupling/constant_registry.rb +5 -4
- data/lib/hashira/coupling/definition.rb +5 -1
- data/lib/hashira/coupling/definitions.rb +4 -1
- data/lib/hashira/coupling/roster.rb +4 -0
- data/lib/hashira/coupling/scope.rb +5 -1
- data/lib/hashira/duplication/duplication_finding.rb +3 -1
- data/lib/hashira/pipeline.rb +3 -0
- data/lib/hashira/smells/check.rb +5 -1
- data/lib/hashira/smells/data_clump.rb +2 -0
- data/lib/hashira/smells/instance_variable_assumption.rb +2 -0
- data/lib/hashira/smells/module_initialize.rb +2 -0
- data/lib/hashira/smells/repeated_conditional.rb +2 -0
- data/lib/hashira/smells/report.rb +20 -17
- data/lib/hashira/smells/too_many_instance_variables.rb +2 -0
- data/lib/hashira/version.rb +1 -1
- data/lib/hashira.rb +3 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 698031b3fff8e705bf80379be16ee05ecacfa9c137459ab8beb855cee1c624dd
|
|
4
|
+
data.tar.gz: c50d6bc7ee142f665c8764d56d7fdf07de381b00529715e37682aed6f0f1856d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4ce8b38adba57e0d1bdf6343362efb3e205c6455d5634bf3cd07c2b429870423268fc4263215ad3ea767284a7acc906901259acbbec7570d386db7fc0759626b
|
|
7
|
+
data.tar.gz: 3670d8b067f2535d745c3785d52087f2aa189909511566e8452818da408e82882077fe428a72b16470640f60bddcdcbd0fbc6109fc412d64652648c20b1bcb9c
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.1] - 2026-08-05
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- The churn scan passes `--no-renames` to `git log`, counting a move as a
|
|
13
|
+
delete plus an add. Rename detection needs blob contents, so on a partial
|
|
14
|
+
clone (`--filter=blob:none`) the old command lazy-fetched objects from the
|
|
15
|
+
network one at a time — on a long history the scan stalled for minutes and
|
|
16
|
+
looked like a hang. It also made the tally depend on which blobs git could
|
|
17
|
+
see, so the same tree could report different churn (and different
|
|
18
|
+
findings) run to run. A dogfood run against a large open-source app fell
|
|
19
|
+
from 3m39s to under 9 seconds.
|
|
20
|
+
- A bare reference to a Ruby core constant (`String`, `Regexp`, `File`, …)
|
|
21
|
+
no longer couples to whichever package defines a namespaced namesake such
|
|
22
|
+
as `Sql::Nodes::Regexp` — Ruby would resolve it to the core class, so
|
|
23
|
+
hashira now drops the edge. The registry answers these through `rooted`,
|
|
24
|
+
which skips the shorthand tails: a lexical namesake still shadows the core
|
|
25
|
+
name as Ruby's own lookup does, and a project that reopens the class at
|
|
26
|
+
top level still owns it. Dogfooding against a large open-source library,
|
|
27
|
+
this deleted a phantom `mixed_audience` finding built entirely on `Array`,
|
|
28
|
+
`String`, and `File`.
|
|
29
|
+
- Constants assigned in a class body (`Node = Struct.new(:path)`) now join the
|
|
30
|
+
census as definitions, so a bare reference resolves to the local constant
|
|
31
|
+
instead of a foreign package's namesake — a karat run had minted two phantom
|
|
32
|
+
edges this way. For usage counts they still collapse into their enclosing
|
|
33
|
+
type: `wide_edge` measures classes, not the constants they carry.
|
|
34
|
+
|
|
8
35
|
## [0.5.0] - 2026-08-04
|
|
9
36
|
|
|
10
37
|
### Added
|
|
@@ -266,6 +293,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
266
293
|
- Output formats: text, JSON, Graphviz dot, Mermaid (`--format`, `--json`).
|
|
267
294
|
- `--help` and `--version`.
|
|
268
295
|
|
|
296
|
+
[0.5.1]: https://github.com/giacope/hashira/releases/tag/v0.5.1
|
|
269
297
|
[0.5.0]: https://github.com/giacope/hashira/releases/tag/v0.5.0
|
|
270
298
|
[0.4.0]: https://github.com/giacope/hashira/releases/tag/v0.4.0
|
|
271
299
|
[0.3.0]: https://github.com/giacope/hashira/releases/tag/v0.3.0
|
data/README.md
CHANGED
|
@@ -26,11 +26,10 @@ package TC Ca Ce I Cyc
|
|
|
26
26
|
billing 1 1 1 0.50 YES
|
|
27
27
|
shipping 1 1 1 0.50 YES
|
|
28
28
|
|
|
29
|
-
Findings (
|
|
30
|
-
cycle: billing can reach itself: billing -> shipping -> billing — any change
|
|
31
|
-
|
|
32
|
-
·
|
|
33
|
-
· shipping/rate.rb:3: Billing::Client
|
|
29
|
+
Findings (1):
|
|
30
|
+
cycle: billing can reach itself: billing -> shipping -> billing — any change may ripple back around. The lightest edge on this cycle is billing -> shipping (1 ref).
|
|
31
|
+
· billing/client.rb:5: Shipping::Rate
|
|
32
|
+
· shipping/rate.rb:8: Billing::Client
|
|
34
33
|
```
|
|
35
34
|
|
|
36
35
|
A healthy project reports `Findings (0): none ✓ — structure is healthy`.
|
|
@@ -33,9 +33,13 @@ module Hashira
|
|
|
33
33
|
roots.include?(segments.first(1)) ? segments : (stack.last || []) + segments
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
def direct(type_node)
|
|
36
|
+
def direct(type_node) = statements(type_node).grep(Prism::DefNode)
|
|
37
|
+
|
|
38
|
+
def constants(type_node) = statements(type_node).grep(Prism::ConstantWriteNode)
|
|
39
|
+
|
|
40
|
+
def statements(type_node)
|
|
37
41
|
body = type_node.body
|
|
38
|
-
|
|
42
|
+
body.is_a?(Prism::StatementsNode) ? body.body : [body]
|
|
39
43
|
end
|
|
40
44
|
end
|
|
41
45
|
end
|
data/lib/hashira/churn.rb
CHANGED
|
@@ -1,47 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Hashira::CLI::CommandLine
|
|
4
|
-
DEFAULT_BASELINE = "hashira_baseline.json"
|
|
5
|
-
|
|
6
|
-
FORMATS = %w[text json dot mermaid].freeze
|
|
7
|
-
|
|
8
|
-
CI_FLAGS = { "--update-baseline" => :update, "--ratchet" => :ratchet }.freeze
|
|
9
|
-
|
|
10
4
|
def initialize(argv)
|
|
11
5
|
@arguments = Hashira::CLI::Arguments.new(argv)
|
|
12
6
|
end
|
|
13
7
|
|
|
14
|
-
def options =
|
|
8
|
+
def options = page || parsed
|
|
15
9
|
|
|
16
10
|
private
|
|
17
11
|
|
|
18
|
-
def
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
def page
|
|
13
|
+
flag = Hashira::CLI::FLAGS.select(&:page?).find { seen?(it) }
|
|
14
|
+
Hashira::CLI::Options.page(flag.mode) if flag
|
|
21
15
|
end
|
|
22
16
|
|
|
23
|
-
def
|
|
24
|
-
Hashira::CLI::Options.new(directories: [], mode:, baseline: nil, fail_on: [], skip: [], packaging: :auto)
|
|
25
|
-
end
|
|
17
|
+
def seen?(flag) = flag.names.any? { @arguments.delete(it) }
|
|
26
18
|
|
|
27
19
|
def parsed
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
Hashira::CLI::Options.new(directories: @arguments.rest, **values)
|
|
20
|
+
parses = Hashira::CLI::FLAGS.reject(&:page?).to_h { [it, it.read(@arguments)] }
|
|
21
|
+
Hashira::CLI::Options.new(directories: @arguments.rest, mode: mode(parses), **fields(parses))
|
|
31
22
|
end
|
|
32
23
|
|
|
33
|
-
def
|
|
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
|
|
40
|
-
|
|
41
|
-
def grouping = Hashira::CLI::PackageBy.parse(take("--package-by", ""))
|
|
24
|
+
def fields(parses) = parses.to_h { |flag, value| [flag.field, value] }.except(nil)
|
|
42
25
|
|
|
43
|
-
def mode(
|
|
44
|
-
case (asked =
|
|
26
|
+
def mode(parses)
|
|
27
|
+
case (asked = parses.filter_map { |flag, value| flag.bid(value) }.uniq(&:last))
|
|
45
28
|
in [] then :text
|
|
46
29
|
in [[_flag, mode]] then mode
|
|
47
30
|
else conflict(asked)
|
|
@@ -51,26 +34,4 @@ class Hashira::CLI::CommandLine
|
|
|
51
34
|
def conflict(asked)
|
|
52
35
|
raise(Hashira::Error, "conflicting options: #{asked.map(&:first).join(" and ")}")
|
|
53
36
|
end
|
|
54
|
-
|
|
55
|
-
def requested(fail_on)
|
|
56
|
-
CI_FLAGS.filter_map { |flag, mode| [flag, mode] if delete(flag) } + gate(fail_on) + formats
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def gate(fail_on) = fail_on.empty? ? [] : [["--fail-on", :fail_on]]
|
|
60
|
-
|
|
61
|
-
def formats
|
|
62
|
-
chosen = format
|
|
63
|
-
modes = chosen.empty? ? [] : [["--format #{chosen}", chosen.to_sym]]
|
|
64
|
-
delete("--json") ? modes + [["--json", :json]] : modes
|
|
65
|
-
end
|
|
66
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
def take(flag, default) = @arguments.take(flag, default)
|
|
74
|
-
|
|
75
|
-
def delete(flag) = @arguments.delete(flag)
|
|
76
37
|
end
|
data/lib/hashira/cli/fail_on.rb
CHANGED
|
@@ -3,19 +3,13 @@
|
|
|
3
3
|
require_relative "../pipeline"
|
|
4
4
|
|
|
5
5
|
module Hashira::CLI::FailOn
|
|
6
|
-
SMELLS = %w[
|
|
7
|
-
control_parameter data_clump duplicate_method_call feature_envy instance_variable_assumption
|
|
8
|
-
manual_dispatch module_initialize nil_check repeated_conditional too_many_instance_variables
|
|
9
|
-
utility_function
|
|
10
|
-
].freeze
|
|
11
|
-
|
|
12
6
|
MEASURES = (Hashira::Pipeline::ANALYZERS - %i[coupling smells]).map(&:to_s).freeze
|
|
13
7
|
|
|
14
8
|
KINDS = {
|
|
15
9
|
"cycles" => "cycle", "sdp" => "sdp_violation", "dupe" => "duplication",
|
|
16
10
|
**Hashira::Pipeline::STRUCTURAL.to_h { [it, it] },
|
|
17
11
|
**MEASURES.to_h { [it, it] },
|
|
18
|
-
"smells" => SMELLS, **SMELLS.to_h { [it, it] }
|
|
12
|
+
"smells" => Hashira::Pipeline::SMELLS, **Hashira::Pipeline::SMELLS.to_h { [it, it] }
|
|
19
13
|
}.freeze
|
|
20
14
|
|
|
21
15
|
module_function
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hashira::CLI
|
|
4
|
+
Flag =
|
|
5
|
+
Data.define(:name, :arg, :default, :field, :parse, :mode, :text) do
|
|
6
|
+
def initialize(**attributes) = super(arg: nil, default: "", field: nil, parse: nil, mode: nil, **attributes)
|
|
7
|
+
|
|
8
|
+
def read(arguments) = arg ? take(arguments) : arguments.delete(name)
|
|
9
|
+
|
|
10
|
+
def take(arguments)
|
|
11
|
+
raw = arguments.take(name, default)
|
|
12
|
+
parse ? parse.parse(raw) : raw
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def bid(value)
|
|
16
|
+
case [mode, value]
|
|
17
|
+
in [nil, _] | [_, nil] | [_, []] then nil
|
|
18
|
+
in [:parsed, chosen] then ["#{name} #{chosen}", chosen]
|
|
19
|
+
else [name, mode]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def label = [name, arg].compact.join(" ")
|
|
24
|
+
|
|
25
|
+
def names = name.split(", ")
|
|
26
|
+
|
|
27
|
+
def page? = Hashira::CLI::Usage::PAGES.include?(mode)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "fail_on"
|
|
4
|
+
require_relative "flag"
|
|
5
|
+
require_relative "format"
|
|
6
|
+
require_relative "package_by"
|
|
7
|
+
require_relative "skip"
|
|
8
|
+
|
|
9
|
+
class Hashira::CLI
|
|
10
|
+
FLAGS = [
|
|
11
|
+
Flag.new(
|
|
12
|
+
name: "--format", arg: "FORMAT", parse: Format, mode: :parsed,
|
|
13
|
+
text: ["text (default), json, dot, or mermaid"]
|
|
14
|
+
),
|
|
15
|
+
Flag.new(name: "--json", mode: :json, text: ["shorthand for --format json"]),
|
|
16
|
+
Flag.new(
|
|
17
|
+
name: "--fail-on", arg: "KINDS", field: :fail_on, parse: FailOn, mode: :fail_on,
|
|
18
|
+
text: [
|
|
19
|
+
"exit 1 if findings exist; comma-separated",
|
|
20
|
+
"kinds: cycles, sdp, mixed_audience, wide_edge,",
|
|
21
|
+
"roll_call, complexity, duplication, smells (all",
|
|
22
|
+
"of them), or one smell kind such as feature_envy"
|
|
23
|
+
]
|
|
24
|
+
),
|
|
25
|
+
Flag.new(
|
|
26
|
+
name: "--skip", arg: "ANALYZERS", field: :skip, parse: Skip,
|
|
27
|
+
text: ["drop an analyzer; comma-separated: coupling,", "complexity, duplication, smells"]
|
|
28
|
+
),
|
|
29
|
+
Flag.new(
|
|
30
|
+
name: "--package-by", arg: "WHAT", field: :packaging, parse: PackageBy,
|
|
31
|
+
text: [
|
|
32
|
+
"group coupling by: auto, folder, or namespace",
|
|
33
|
+
"(top-level constant). Default auto: namespace for",
|
|
34
|
+
"Rails apps (config/application.rb in or beside the",
|
|
35
|
+
"analyzed directory), folder otherwise"
|
|
36
|
+
]
|
|
37
|
+
),
|
|
38
|
+
Flag.new(
|
|
39
|
+
name: "--ratchet", mode: :ratchet,
|
|
40
|
+
text: ["fail when edges or findings appear that the", "baseline lacks"]
|
|
41
|
+
),
|
|
42
|
+
Flag.new(name: "--update-baseline", mode: :update, text: ["record the current edges and findings"]),
|
|
43
|
+
Flag.new(
|
|
44
|
+
name: "--baseline", arg: "PATH", field: :baseline, default: "hashira_baseline.json",
|
|
45
|
+
text: ["baseline file (default: hashira_baseline.json)"]
|
|
46
|
+
),
|
|
47
|
+
Flag.new(name: "-h, --help", mode: :help, text: ["print this help"]),
|
|
48
|
+
Flag.new(name: "--version", mode: :version, text: ["print the version"])
|
|
49
|
+
].freeze
|
|
50
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hashira::CLI::Format
|
|
4
|
+
CHOICES = %w[text json dot mermaid].freeze
|
|
5
|
+
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def parse(value)
|
|
9
|
+
return if value.empty?
|
|
10
|
+
return value.to_sym if CHOICES.include?(value)
|
|
11
|
+
raise(Hashira::Error.unknown("--format", value, CHOICES))
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/hashira/cli/options.rb
CHANGED
|
@@ -5,6 +5,8 @@ class Hashira::CLI
|
|
|
5
5
|
Data.define(:directories, :mode, :baseline, :fail_on, :skip, :packaging) do
|
|
6
6
|
def self.parse(argv) = CommandLine.new(argv).options
|
|
7
7
|
|
|
8
|
+
def self.page(mode) = new(directories: [], mode:, baseline: "", fail_on: [], skip: [], packaging: :auto)
|
|
9
|
+
|
|
8
10
|
def pipeline
|
|
9
11
|
Hashira::Pipeline.new(Hashira::Project.detect(directories), enabled: analyzers, packaging:)
|
|
10
12
|
end
|
data/lib/hashira/cli/usage.rb
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "flags"
|
|
4
|
+
|
|
3
5
|
module Hashira::CLI::Usage
|
|
4
|
-
|
|
6
|
+
PAGES = %i[help version].freeze
|
|
7
|
+
|
|
8
|
+
HEADER = <<~TEXT
|
|
5
9
|
Usage: hashira [DIRECTORY ...] [options]
|
|
6
10
|
|
|
7
11
|
Coupling, cognitive-complexity, duplication, and code-smell metrics
|
|
@@ -9,38 +13,30 @@ module Hashira::CLI::Usage
|
|
|
9
13
|
With no directory, auto-detects lib/<gem>.
|
|
10
14
|
|
|
11
15
|
Options:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
16
|
+
TEXT
|
|
17
|
+
|
|
18
|
+
FOOTER = <<~TEXT
|
|
30
19
|
|
|
31
20
|
Findings accepted by design can be recorded in the baseline as
|
|
32
21
|
"accepted": [{"kind": "...", "package": "...", "reason": "..."}]
|
|
33
22
|
— they leave reports and gates, keeping a one-line reminder each.
|
|
34
23
|
Clones are named by "digest" instead of "package", so an acceptance
|
|
35
24
|
survives the lines above it moving. Read digests from --json.
|
|
36
|
-
|
|
25
|
+
TEXT
|
|
37
26
|
|
|
38
27
|
module_function
|
|
39
28
|
|
|
40
|
-
def help = emit(
|
|
29
|
+
def help = emit(HEADER + options + FOOTER)
|
|
41
30
|
|
|
42
31
|
def version = emit("hashira #{Hashira::VERSION}")
|
|
43
32
|
|
|
33
|
+
def options = Hashira::CLI::FLAGS.map { rows(it) }.join
|
|
34
|
+
|
|
35
|
+
def rows(flag)
|
|
36
|
+
first, *rest = flag.text
|
|
37
|
+
[" #{flag.label.ljust(21)}#{first}\n", *rest.map { "#{" " * 23}#{it}\n" }].join
|
|
38
|
+
end
|
|
39
|
+
|
|
44
40
|
def emit(output)
|
|
45
41
|
puts(output)
|
|
46
42
|
0
|
data/lib/hashira/cli.rb
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Hashira::Complexity::MethodFinding
|
|
4
|
+
KIND = "complexity"
|
|
5
|
+
|
|
4
6
|
def initialize(score)
|
|
5
7
|
@score = score
|
|
6
8
|
end
|
|
7
9
|
|
|
8
10
|
def to_finding
|
|
9
|
-
Hashira::Analysis::Finding.new(kind:
|
|
11
|
+
Hashira::Analysis::Finding.new(kind: KIND, package: @score.subject, detail:, evidence:)
|
|
10
12
|
end
|
|
11
13
|
|
|
12
14
|
private
|
|
@@ -38,7 +38,7 @@ class Hashira::Coupling::Census
|
|
|
38
38
|
|
|
39
39
|
private
|
|
40
40
|
|
|
41
|
-
def type?(segments) = @roster.
|
|
41
|
+
def type?(segments) = @roster.type?(segments)
|
|
42
42
|
|
|
43
43
|
def tally
|
|
44
44
|
Hashira::Coupling::Roster.new(@placement.placed.map { |definition, package| [definition, translate(package)] })
|
|
@@ -16,10 +16,9 @@ class Hashira::Coupling::ConstantRegistry
|
|
|
16
16
|
(1...path.length).each { claim(@shorthand, path.drop(it), package) }
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def package(path)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
end
|
|
19
|
+
def package(path) = settle(anchored(path) || enclosing(path))
|
|
20
|
+
|
|
21
|
+
def rooted(path) = settle(@origins[path.join("::")] || enclosing(path))
|
|
23
22
|
|
|
24
23
|
def exact(path) = @origins[path.join("::")]
|
|
25
24
|
|
|
@@ -32,6 +31,8 @@ class Hashira::Coupling::ConstantRegistry
|
|
|
32
31
|
claims[key] = claims.fetch(key, package) == package ? package : AMBIGUOUS
|
|
33
32
|
end
|
|
34
33
|
|
|
34
|
+
def settle(found) = (found unless found == AMBIGUOUS)
|
|
35
|
+
|
|
35
36
|
def anchored(path)
|
|
36
37
|
key = path.join("::")
|
|
37
38
|
@origins[key] || @shorthand[key]
|
|
@@ -16,7 +16,11 @@ module Hashira
|
|
|
16
16
|
|
|
17
17
|
def superclass = node.superclass
|
|
18
18
|
|
|
19
|
-
def
|
|
19
|
+
def module? = node.is_a?(Prism::ModuleNode)
|
|
20
|
+
|
|
21
|
+
def type? = klass? || module?
|
|
22
|
+
|
|
23
|
+
def counted? = klass? || (module? && !Hashira::Analysis::Syntax.direct(node).empty?)
|
|
20
24
|
end
|
|
21
25
|
end
|
|
22
26
|
end
|
|
@@ -26,6 +26,9 @@ class Hashira::Coupling::Definitions
|
|
|
26
26
|
|
|
27
27
|
def scan(file, tree)
|
|
28
28
|
package = @project.package(file)
|
|
29
|
-
Hashira::Analysis::TypeWalk.each(tree, roots: roots)
|
|
29
|
+
Hashira::Analysis::TypeWalk.each(tree, roots: roots) do |node, full|
|
|
30
|
+
yield(node, full, package)
|
|
31
|
+
Hashira::Analysis::Syntax.constants(node).each { yield(it, full + [it.name.to_s], package) }
|
|
32
|
+
end
|
|
30
33
|
end
|
|
31
34
|
end
|
|
@@ -4,6 +4,7 @@ class Hashira::Coupling::Roster
|
|
|
4
4
|
def initialize(placed)
|
|
5
5
|
@registry = Hashira::Coupling::ConstantRegistry.new
|
|
6
6
|
@types = Hash.new(0)
|
|
7
|
+
@typed = Set.new
|
|
7
8
|
counted = Set.new
|
|
8
9
|
placed.each { |definition, package| admit(definition, package, counted) }
|
|
9
10
|
end
|
|
@@ -12,6 +13,8 @@ class Hashira::Coupling::Roster
|
|
|
12
13
|
|
|
13
14
|
def origins = @registry.origins
|
|
14
15
|
|
|
16
|
+
def type?(path) = @typed.include?(path)
|
|
17
|
+
|
|
15
18
|
def packages = @types.keys | @registry.packages
|
|
16
19
|
|
|
17
20
|
private
|
|
@@ -20,6 +23,7 @@ class Hashira::Coupling::Roster
|
|
|
20
23
|
return unless package
|
|
21
24
|
path = definition.path
|
|
22
25
|
@registry.register(path, package)
|
|
26
|
+
@typed << path if definition.type?
|
|
23
27
|
@types[package] += 1 if definition.counted? && counted.add?(path)
|
|
24
28
|
end
|
|
25
29
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Hashira::Coupling::Scope
|
|
4
|
+
CORE = Object.constants.select { Object.const_source_location(it) == [] }.to_set(&:to_s).freeze
|
|
5
|
+
|
|
4
6
|
def initialize(registry, catalog, placement)
|
|
5
7
|
@registry = registry
|
|
6
8
|
@catalog = catalog
|
|
@@ -10,7 +12,7 @@ class Hashira::Coupling::Scope
|
|
|
10
12
|
def resolve(segments, nesting)
|
|
11
13
|
return if @placement.skip?(segments)
|
|
12
14
|
scoped = nesting.reverse_each.filter_map { descend(it, segments) }.first
|
|
13
|
-
scoped ? qualify(scoped) :
|
|
15
|
+
scoped ? qualify(scoped) : outward(@catalog.strip(segments))
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
def pinpoint(segments)
|
|
@@ -20,6 +22,8 @@ class Hashira::Coupling::Scope
|
|
|
20
22
|
|
|
21
23
|
private
|
|
22
24
|
|
|
25
|
+
def outward(path) = CORE.include?(path.first) ? @registry.rooted(path) : @registry.package(path)
|
|
26
|
+
|
|
23
27
|
def descend(entry, segments)
|
|
24
28
|
segments.length.downto(1).filter_map { @registry.exact(@catalog.strip(entry + segments.first(it))) }.first
|
|
25
29
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Hashira::Duplication::DuplicationFinding
|
|
4
|
+
KIND = "duplication"
|
|
5
|
+
|
|
4
6
|
def initialize(cluster, churn)
|
|
5
7
|
@cluster = cluster
|
|
6
8
|
@churn = churn
|
|
@@ -8,7 +10,7 @@ class Hashira::Duplication::DuplicationFinding
|
|
|
8
10
|
|
|
9
11
|
def to_finding
|
|
10
12
|
site = @cluster.canonical
|
|
11
|
-
Hashira::Analysis::Finding.new(kind:
|
|
13
|
+
Hashira::Analysis::Finding.new(kind: KIND, package: site.location, digest: site.digest, detail:, evidence:)
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
private
|
data/lib/hashira/pipeline.rb
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
require_relative "coupling/report"
|
|
5
|
+
require_relative "smells/report"
|
|
5
6
|
|
|
6
7
|
class Hashira::Pipeline
|
|
7
8
|
ANALYZERS = %i[coupling complexity duplication smells].freeze
|
|
8
9
|
|
|
9
10
|
STRUCTURAL = Hashira::Coupling::Report::RULES.map { it::KIND }.freeze
|
|
10
11
|
|
|
12
|
+
SMELLS = Hashira::Smells::Report::CHECKS.map(&:kind).sort.freeze
|
|
13
|
+
|
|
11
14
|
def initialize(project, enabled: ANALYZERS, packaging: :auto)
|
|
12
15
|
@project = project
|
|
13
16
|
@enabled = enabled
|
data/lib/hashira/smells/check.rb
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Hashira::Smells::Check
|
|
4
|
+
def self.kind = name.split("::").last.gsub(/(?<=[a-z])(?=[A-Z])/, "_").downcase
|
|
5
|
+
|
|
6
|
+
def self.judge? = false
|
|
7
|
+
|
|
4
8
|
def initialize(subject)
|
|
5
9
|
@subject = subject
|
|
6
10
|
end
|
|
@@ -14,7 +18,7 @@ class Hashira::Smells::Check
|
|
|
14
18
|
|
|
15
19
|
attr_reader :subject
|
|
16
20
|
|
|
17
|
-
def kind = self.class.
|
|
21
|
+
def kind = self.class.kind
|
|
18
22
|
|
|
19
23
|
def label = subject.subject
|
|
20
24
|
|
|
@@ -1,31 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "check"
|
|
4
|
+
require_relative "control_parameter"
|
|
5
|
+
require_relative "data_clump"
|
|
6
|
+
require_relative "duplicate_method_call"
|
|
7
|
+
require_relative "feature_envy"
|
|
8
|
+
require_relative "instance_variable_assumption"
|
|
9
|
+
require_relative "manual_dispatch"
|
|
10
|
+
require_relative "module_initialize"
|
|
11
|
+
require_relative "nil_check"
|
|
12
|
+
require_relative "repeated_conditional"
|
|
13
|
+
require_relative "too_many_instance_variables"
|
|
14
|
+
require_relative "utility_function"
|
|
15
|
+
|
|
3
16
|
class Hashira::Smells::Report
|
|
17
|
+
CHECKS = Hashira::Smells::Check.subclasses.sort_by(&:name).freeze
|
|
18
|
+
|
|
19
|
+
JUDGES = CHECKS.select(&:judge?).freeze
|
|
20
|
+
|
|
21
|
+
PROBES = CHECKS.reject(&:judge?).freeze
|
|
22
|
+
|
|
4
23
|
def initialize(project, trees)
|
|
5
24
|
@types = Hashira::Smells::Census.new(project, trees).types
|
|
6
25
|
end
|
|
7
26
|
|
|
8
|
-
def findings = @findings ||= sniff(@types,
|
|
27
|
+
def findings = @findings ||= sniff(@types, JUDGES) + sniff(@types.flat_map(&:defs), PROBES)
|
|
9
28
|
|
|
10
29
|
private
|
|
11
30
|
|
|
12
31
|
def sniff(subjects, checks) = subjects.flat_map { |subject| verdicts(subject, checks) }
|
|
13
32
|
|
|
14
33
|
def verdicts(subject, checks) = checks.filter_map { |check| check.new(subject).finding }
|
|
15
|
-
|
|
16
|
-
def judges
|
|
17
|
-
[
|
|
18
|
-
Hashira::Smells::DataClump, Hashira::Smells::InstanceVariableAssumption,
|
|
19
|
-
Hashira::Smells::ModuleInitialize, Hashira::Smells::RepeatedConditional,
|
|
20
|
-
Hashira::Smells::TooManyInstanceVariables
|
|
21
|
-
]
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def probes
|
|
25
|
-
[
|
|
26
|
-
Hashira::Smells::ControlParameter, Hashira::Smells::DuplicateMethodCall,
|
|
27
|
-
Hashira::Smells::FeatureEnvy, Hashira::Smells::ManualDispatch,
|
|
28
|
-
Hashira::Smells::NilCheck, Hashira::Smells::UtilityFunction
|
|
29
|
-
]
|
|
30
|
-
end
|
|
31
34
|
end
|
|
@@ -10,6 +10,8 @@ class Hashira::Smells::TooManyInstanceVariables < Hashira::Smells::Check
|
|
|
10
10
|
Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
|
|
11
11
|
].freeze
|
|
12
12
|
|
|
13
|
+
def self.judge? = true
|
|
14
|
+
|
|
13
15
|
private
|
|
14
16
|
|
|
15
17
|
def smelly? = subject.kind == :class && names.size > LIMIT
|
data/lib/hashira/version.rb
CHANGED
data/lib/hashira.rb
CHANGED
|
@@ -47,6 +47,9 @@ require_relative "hashira/cli"
|
|
|
47
47
|
require_relative "hashira/cli/arguments"
|
|
48
48
|
require_relative "hashira/cli/command_line"
|
|
49
49
|
require_relative "hashira/cli/fail_on"
|
|
50
|
+
require_relative "hashira/cli/flag"
|
|
51
|
+
require_relative "hashira/cli/flags"
|
|
52
|
+
require_relative "hashira/cli/format"
|
|
50
53
|
require_relative "hashira/cli/options"
|
|
51
54
|
require_relative "hashira/cli/package_by"
|
|
52
55
|
require_relative "hashira/cli/run"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hashira
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Giacomo GK
|
|
@@ -44,6 +44,9 @@ files:
|
|
|
44
44
|
- lib/hashira/cli/arguments.rb
|
|
45
45
|
- lib/hashira/cli/command_line.rb
|
|
46
46
|
- lib/hashira/cli/fail_on.rb
|
|
47
|
+
- lib/hashira/cli/flag.rb
|
|
48
|
+
- lib/hashira/cli/flags.rb
|
|
49
|
+
- lib/hashira/cli/format.rb
|
|
47
50
|
- lib/hashira/cli/options.rb
|
|
48
51
|
- lib/hashira/cli/package_by.rb
|
|
49
52
|
- lib/hashira/cli/run.rb
|