hashira 0.9.0 → 0.10.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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/README.md +34 -4
  4. data/exe/hashira +1 -1
  5. data/lib/hashira/boundaries.rb +64 -0
  6. data/lib/hashira/ci/baseline.rb +6 -3
  7. data/lib/hashira/ci/{edge_diff_report.rb → edge_changes.rb} +1 -1
  8. data/lib/hashira/ci/{finding_diff_report.rb → finding_changes.rb} +1 -1
  9. data/lib/hashira/ci/ratchet_report.rb +2 -2
  10. data/lib/hashira/cli/flag.rb +1 -1
  11. data/lib/hashira/cli/flags.rb +1 -1
  12. data/lib/hashira/cli/options.rb +16 -6
  13. data/lib/hashira/cli/run.rb +1 -6
  14. data/lib/hashira/{cli.rb → cli/session.rb} +7 -5
  15. data/lib/hashira/coupling/{cycle_findings.rb → cycle.rb} +1 -1
  16. data/lib/hashira/coupling/{roll_call_findings.rb → echo.rb} +1 -1
  17. data/lib/hashira/coupling/{sdp_violation_findings.rb → imbalance.rb} +3 -3
  18. data/lib/hashira/coupling/{mixed_audience_findings.rb → mixed_audience.rb} +1 -1
  19. data/lib/hashira/coupling/report.rb +5 -5
  20. data/lib/hashira/coupling/{wide_edge_findings.rb → wide_edge.rb} +1 -1
  21. data/lib/hashira/pipeline.rb +8 -1
  22. data/lib/hashira/project.rb +4 -1
  23. data/lib/hashira/report/smell_phrases.rb +3 -3
  24. data/lib/hashira/smells/{instance_variable_assumption.rb → assumed_state.rb} +1 -1
  25. data/lib/hashira/smells/boundary_sprawl.rb +3 -2
  26. data/lib/hashira/smells/{duplicate_method_call.rb → repeated_call.rb} +1 -1
  27. data/lib/hashira/smells/report.rb +10 -7
  28. data/lib/hashira/smells/{too_many_instance_variables.rb → state_sprawl.rb} +1 -1
  29. data/lib/hashira/version.rb +1 -1
  30. data/lib/hashira.rb +15 -11
  31. metadata +13 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2750af6be7a5e6885421144e92ab4c12e554ece76c110b76d5fd2b52b72a2411
4
- data.tar.gz: b2ab7386f4907e297b3cfc623b535808df4f6865423bd9d4f8bae4b487b81317
3
+ metadata.gz: 28f5f325c96b2579257212ed846fba744718bf8bbdc16ea46f0f0225abdb15af
4
+ data.tar.gz: 587bc337f5e0bbd94243d7e6cb7b4d4268a8617d913750fdf85156f0f4168c78
5
5
  SHA512:
6
- metadata.gz: 6a4c401b5d242175899bcf4e1e6792f74b77139346b336564c2496e1b97dd716b5014f147b94fbc076b1870020893c8d99b8f08dadd0ef5e648fa427a6fb3a91
7
- data.tar.gz: 812c604c2cafe644d8ab0692b670f74f1c63dde77a1084f235919c1eee72d1a24c53936c403f7e84c2a46c0145c4bd702c6f183e926f9d5f27135d54b366aca6
6
+ metadata.gz: 28516d160aa0888bf1e9de0e00f80b1992444554309cc2a414a68dfc3c9272580e314b6593ec4b1a113b50185b7aea99bd82af7c1ac7143da9fb802b115aec46
7
+ data.tar.gz: eb711b0c77caff20baca0e36d8526126dde23e40bda06c6ed8efe5b1fd62d180e372b3a4447d66ab5be3b19abdf000e34ae94629ea45142373840992589cd6b7
data/CHANGELOG.md CHANGED
@@ -5,6 +5,30 @@ 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.10.0] - 2026-08-30
9
+
10
+ ### Added
11
+
12
+ - **Interpreted foreign models are verified architecture, not accepted debt.**
13
+ A baseline can declare a foreign root with `role: interpreted_model`, its one
14
+ API entrypoint, and a reason. Type dispatch over that model no longer emits
15
+ `boundary_sprawl`, while a missing root call or a call which bypasses the
16
+ entrypoint is rejected as misuse. Baseline schema 6 preserves these
17
+ declarations on `--update-baseline`. Hashira now declares Prism this way and
18
+ dogfoods with zero findings and zero acceptances.
19
+
20
+ ### Changed
21
+
22
+ - `boundary_sprawl` now distinguishes a missing adapter from a program which
23
+ deliberately interprets a foreign data model. Undeclared foreign roots keep
24
+ the same 12-method, 3-file threshold and behavior.
25
+
26
+ - Three smell kinds are renamed to name the smell, not the mechanism:
27
+ `duplicate_method_call` is now `repeated_call`, `too_many_instance_variables`
28
+ is `state_sprawl`, and `instance_variable_assumption` is `assumed_state`. A
29
+ saved baseline reports the old kinds as resolved and the new ones as new;
30
+ accept once to move on.
31
+
8
32
  ## [0.9.0] - 2026-08-20
9
33
 
10
34
  ### Fixed
data/README.md CHANGED
@@ -340,7 +340,9 @@ What each one catches:
340
340
  the method" needs a destination you own.
341
341
  - **boundary_sprawl** — 12+ methods across 3+ files each type-guard against the
342
342
  same foreign root (`Prism`, `ActiveRecord`, ...). One method inspecting a
343
- foreign type is a fact of life; a sprawl of them is a missing adapter.
343
+ foreign type is a fact of life; a sprawl of them usually means a missing
344
+ adapter. An analyzer or interpreter which deliberately understands a foreign
345
+ data model can declare and verify that boundary instead.
344
346
  - **utility_function** — a public instance method that touches no instance state;
345
347
  it isn't really a method of this class. Private stateless helpers are fine, and
346
348
  `module_function` modules are exempt — that's what they're for.
@@ -348,7 +350,7 @@ What each one catches:
348
350
  caller already knew which branch it wanted.
349
351
  - **data_clump** — the same two-plus parameters travel through three or more
350
352
  methods; a value object is missing.
351
- - **duplicate_method_call** — the identical receiver-and-arguments call repeated
353
+ - **repeated_call** — the identical receiver-and-arguments call repeated
352
354
  inside one method; name the result once. Quiet wherever naming it would be
353
355
  wrong: calls that mint a fresh value every time (`"".b`, `rand`, `dup`,
354
356
  `SecureRandom.hex`) are meant to differ, and a repeat no single run can reach
@@ -356,9 +358,9 @@ What each one catches:
356
358
  — has nothing to hoist.
357
359
  - **repeated_conditional** — one class testing the same condition in three or
358
360
  more places; polymorphism is overdue.
359
- - **too_many_instance_variables** — more than four per class. Memoization
361
+ - **state_sprawl** — more than four per class. Memoization
360
362
  (`@x ||=`) doesn't count as state.
361
- - **instance_variable_assumption** — an ivar read that nothing the class can
363
+ - **assumed_state** — an ivar read that nothing the class can
362
364
  reach ever assigns: not `initialize`, not another of its own methods, not an
363
365
  `attr_writer`, not a reopening of the class, not a module it mixes in or a
364
366
  class it inherits. Usually a typo, or state some other object is expected to
@@ -547,6 +549,34 @@ English, so the codes are distinct:
547
549
  baseline is stale. Failing on `1` while treating `3` as a nudge lets a build
548
550
  block regressions without blocking progress.
549
551
 
552
+ ### Declaring interpreted models
553
+
554
+ Some foreign models are the subject of the program rather than a dependency to
555
+ hide. A compiler interprets an AST; a serializer interprets a schema. Declare
556
+ that architecture in the baseline instead of accepting a `boundary_sprawl`
557
+ finding:
558
+
559
+ ```json
560
+ "boundaries": [
561
+ {
562
+ "root": "Prism",
563
+ "role": "interpreted_model",
564
+ "entrypoint": "lib/my_tool/trees.rb",
565
+ "reason": "the Prism AST is the input model"
566
+ }
567
+ ]
568
+ ```
569
+
570
+ Type dispatch over that root no longer counts as boundary sprawl. The
571
+ declaration is checked, not waived: the root API must be called, every such call
572
+ must go through the named entrypoint, and every field needs a value. A missing,
573
+ unknown, unused, or bypassed declaration exits as misuse. `--update-baseline`
574
+ preserves declarations.
575
+
576
+ This is deliberately narrower than acceptance. A declared boundary says what
577
+ architecture the project conforms to; an accepted finding records why a real
578
+ violation is allowed to remain.
579
+
550
580
  ### Accepting by design
551
581
 
552
582
  Anything deliberate goes in the baseline with a reason. It leaves reports and
data/exe/hashira CHANGED
@@ -6,4 +6,4 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
6
 
7
7
  require "hashira"
8
8
 
9
- exit Hashira::CLI.new(ARGV).status
9
+ exit Hashira::CLI::Session.new(ARGV).status
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ class Hashira::Boundaries
6
+ ROLE = "interpreted_model"
7
+
8
+ Declaration =
9
+ Data.define(:root, :role, :entrypoint, :reason) do
10
+ def verify
11
+ missing = members.find { !present?(public_send(it)) }
12
+ root, role, _entrypoint, _reason = deconstruct
13
+ raise(Hashira::Error, "boundary #{root || "(unknown)"} #{missing} is missing") if missing
14
+ raise(Hashira::Error, "boundary #{root} has unknown role #{role.inspect}") unless role == Hashira::Boundaries::ROLE
15
+ end
16
+ private
17
+ def present?(value) = value.is_a?(String) && !value.empty?
18
+ end
19
+
20
+ def initialize(records, trees)
21
+ @records = records
22
+ @trees = trees
23
+ end
24
+
25
+ def interpreted
26
+ return @_interpreted if @_interpreted
27
+ verify
28
+ @_interpreted = entries.map(&:root)
29
+ end
30
+
31
+ private
32
+
33
+ def entries = @_entries ||= @records.map { declaration(it) }
34
+
35
+ def declaration(record)
36
+ raise(Hashira::Error, "boundary declaration is not an object") unless record.is_a?(Hash)
37
+ Declaration.new(*%w[root role entrypoint reason].map { record[it] })
38
+ end
39
+
40
+ def verify
41
+ entries.each do
42
+ it.verify
43
+ route(it)
44
+ end
45
+ end
46
+
47
+ def route(entry)
48
+ root, _role, entrypoint, _reason = entry.deconstruct
49
+ files = calls(root)
50
+ raise(Hashira::Error, "boundary #{root} has no root calls") if files.empty?
51
+ bypasses = files.reject { same?(it, entrypoint) }
52
+ raise(Hashira::Error, "boundary #{root} bypasses #{entrypoint}: #{bypasses.join(", ")}") unless bypasses.empty?
53
+ end
54
+
55
+ def calls(root)
56
+ @trees.filter_map { |file, tree| file if Hashira::Analysis::NodeWalk.collect(tree).any? { invocation?(it, root) } }
57
+ end
58
+
59
+ def invocation?(node, root)
60
+ node.is_a?(Prism::CallNode) && Hashira::Analysis::Syntax.segments(node.receiver).first == root
61
+ end
62
+
63
+ def same?(file, entrypoint) = File.expand_path(file) == File.expand_path(entrypoint)
64
+ end
@@ -3,7 +3,7 @@
3
3
  require "json"
4
4
 
5
5
  class Hashira::CI::Baseline
6
- SCHEMA_VERSION = 5
6
+ SCHEMA_VERSION = 6
7
7
 
8
8
  def initialize(path, analyzers: [], targets: [])
9
9
  @path = path
@@ -33,6 +33,8 @@ class Hashira::CI::Baseline
33
33
 
34
34
  def accepted = recorded.fetch("accepted", [])
35
35
 
36
+ def boundaries = recorded.fetch("boundaries", [])
37
+
36
38
  def packaging = recorded.fetch("packaging", "folder")
37
39
 
38
40
  def analyzers = recorded.fetch("analyzers", wanted[:analyzers])
@@ -75,7 +77,8 @@ class Hashira::CI::Baseline
75
77
  end
76
78
 
77
79
  def kept
78
- entries = Hashira::CI::Accepted.new(accepted).entries
79
- entries.empty? ? {} : { accepted: entries }
80
+ optional(:accepted, Hashira::CI::Accepted.new(accepted).entries).merge(optional(:boundaries, boundaries))
80
81
  end
82
+
83
+ def optional(name, values) = values.empty? ? {} : { name => values }
81
84
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Hashira::CI::EdgeDiffReport
3
+ class Hashira::CI::EdgeChanges
4
4
  def initialize(graph, io: $stdout)
5
5
  @graph = graph
6
6
  @io = io
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Hashira::CI::FindingDiffReport
3
+ class Hashira::CI::FindingChanges
4
4
  def initialize(findings, io: $stdout)
5
5
  @findings = findings
6
6
  @io = io
@@ -26,8 +26,8 @@ class Hashira::CI::RatchetReport
26
26
  def verdict(*diffs) = diffs.compact.any?(&:worse?) ? Hashira::CI::Status::WORSE : Hashira::CI::Status::BETTER
27
27
 
28
28
  def details(edges, findings)
29
- Hashira::CI::EdgeDiffReport.new(@graph, io: @io).print(edges)
30
- Hashira::CI::FindingDiffReport.new(@findings, io: @io).print(findings) if findings
29
+ Hashira::CI::EdgeChanges.new(@graph, io: @io).print(edges)
30
+ Hashira::CI::FindingChanges.new(@findings, io: @io).print(findings) if findings
31
31
  end
32
32
 
33
33
  def advice(*diffs)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Hashira::CLI
3
+ module Hashira::CLI
4
4
  BLANK = %i[arg field parse mode].to_h { [it, nil] }.freeze
5
5
 
6
6
  Flag =
@@ -8,7 +8,7 @@ require_relative "package_by"
8
8
  require_relative "skip"
9
9
  require_relative "top"
10
10
 
11
- class Hashira::CLI
11
+ module Hashira::CLI
12
12
  FLAGS = [
13
13
  Flag.new(
14
14
  name: "--format", arg: "FORMAT", parse: Format, mode: :parsed,
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Hashira::CLI
3
+ module Hashira::CLI
4
4
  Options =
5
5
  Data.define(:directories, :mode, :baseline, :fail_on, :skip, :only, :packaging, :top, :compact) do
6
6
  def self.parse(argv) = CommandLine.new(argv).options
@@ -13,13 +13,23 @@ class Hashira::CLI
13
13
  end
14
14
 
15
15
  def pipeline
16
- chosen = Hashira::Project.new(directories)
17
- told = Hashira::Report::Notices.new
18
- told.scanning(chosen.files.size)
19
- told.rails if directories.empty? && File.exist?("config/application.rb")
20
- Hashira::Pipeline.new(chosen, enabled: analyzers, packaging:, only:)
16
+ chosen = Hashira::Project.new(directories, boundaries: decisions.boundaries)
17
+ announce(chosen)
18
+ Hashira::Pipeline.new(chosen, enabled: analyzers, packaging:, only:).verify
21
19
  end
22
20
 
23
21
  def analyzers = Hashira::Pipeline::ANALYZERS - skip
22
+ private
23
+ def decisions
24
+ recorded = Hashira::CI::Baseline.new(baseline)
25
+ raise(Hashira::Error, recorded.trouble) if recorded.trouble
26
+ recorded
27
+ end
28
+
29
+ def announce(project)
30
+ told = Hashira::Report::Notices.new
31
+ told.scanning(project.files.size)
32
+ told.rails if directories.empty? && File.exist?("config/application.rb")
33
+ end
24
34
  end
25
35
  end
@@ -17,12 +17,7 @@ class Hashira::CLI::Run
17
17
 
18
18
  def findings = @_findings ||= accepted.screen(@pipeline.findings)
19
19
 
20
- def accepted
21
- path = @options.baseline
22
- stop = Hashira::CI::Baseline.new(path).trouble
23
- raise(Hashira::Error, stop) if stop
24
- Hashira::CI::Accepted.build(path)
25
- end
20
+ def accepted = Hashira::CI::Accepted.build(@options.baseline)
26
21
 
27
22
  def update
28
23
  ratchet.update
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Hashira::CLI
3
+ class Hashira::CLI::Session
4
4
  MISUSE = 2
5
5
  BROKEN = 70
6
6
 
@@ -9,7 +9,7 @@ class Hashira::CLI
9
9
  end
10
10
 
11
11
  def status
12
- dispatch(Options.parse(@argv))
12
+ dispatch(Hashira::CLI::Options.parse(@argv))
13
13
  rescue Hashira::Error => error
14
14
  failure(error)
15
15
  rescue StandardError => error
@@ -18,14 +18,16 @@ class Hashira::CLI
18
18
 
19
19
  private
20
20
 
21
- def dispatch(options) = usage?(options) ? Usage.public_send(options.mode) : timed(options)
21
+ def dispatch(options)
22
+ usage?(options) ? Hashira::CLI::Usage.public_send(options.mode) : timed(options)
23
+ end
22
24
 
23
- def usage?(options) = Usage::PAGES.include?(options.mode)
25
+ def usage?(options) = Hashira::CLI::Usage::PAGES.include?(options.mode)
24
26
 
25
27
  def timed(options)
26
28
  started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
27
29
  pipeline = options.pipeline
28
- Run.new(pipeline, options).status.tap do
30
+ Hashira::CLI::Run.new(pipeline, options).status.tap do
29
31
  Hashira::Report::Notices.new.finished(pipeline.project.files.size, elapsed(started))
30
32
  end
31
33
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "rule"
4
4
 
5
- class Hashira::Coupling::CycleFindings < Hashira::Coupling::Rule
5
+ class Hashira::Coupling::Cycle < Hashira::Coupling::Rule
6
6
  KIND = "cycle"
7
7
 
8
8
  def list
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "rule"
4
4
 
5
- class Hashira::Coupling::RollCallFindings < Hashira::Coupling::Rule
5
+ class Hashira::Coupling::Echo < Hashira::Coupling::Rule
6
6
  KIND = "roll_call"
7
7
 
8
8
  def list = rolls.map { entry(it) }
@@ -2,10 +2,10 @@
2
2
 
3
3
  require_relative "rule"
4
4
 
5
- class Hashira::Coupling::SdpViolationFindings < Hashira::Coupling::Rule
5
+ class Hashira::Coupling::Imbalance < Hashira::Coupling::Rule
6
6
  KIND = "sdp_violation"
7
7
 
8
- Imbalance = Data.define(:from, :to, :from_instability, :to_instability)
8
+ Gap = Data.define(:from, :to, :from_instability, :to_instability)
9
9
 
10
10
  def list
11
11
  ranked.map { |from, to| violation(from, to) }
@@ -20,7 +20,7 @@ class Hashira::Coupling::SdpViolationFindings < Hashira::Coupling::Rule
20
20
  end
21
21
 
22
22
  def detail(from, to)
23
- Imbalance.new(from:, to:, from_instability: instability(from), to_instability: instability(to))
23
+ Gap.new(from:, to:, from_instability: instability(from), to_instability: instability(to))
24
24
  end
25
25
 
26
26
  def instability(package) = metrics[package].instability
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "rule"
4
4
 
5
- class Hashira::Coupling::MixedAudienceFindings < Hashira::Coupling::Rule
5
+ class Hashira::Coupling::MixedAudience < Hashira::Coupling::Rule
6
6
  KIND = "mixed_audience"
7
7
 
8
8
  def list
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "cycle_findings"
4
- require_relative "mixed_audience_findings"
5
- require_relative "roll_call_findings"
3
+ require_relative "cycle"
4
+ require_relative "mixed_audience"
5
+ require_relative "echo"
6
6
  require_relative "rule"
7
- require_relative "sdp_violation_findings"
8
- require_relative "wide_edge_findings"
7
+ require_relative "imbalance"
8
+ require_relative "wide_edge"
9
9
 
10
10
  class Hashira::Coupling::Report
11
11
  RULES = Hashira::Coupling::Rule.subclasses.sort_by(&:name).freeze
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "rule"
4
4
 
5
- class Hashira::Coupling::WideEdgeFindings < Hashira::Coupling::Rule
5
+ class Hashira::Coupling::WideEdge < Hashira::Coupling::Rule
6
6
  KIND = "wide_edge"
7
7
 
8
8
  WIDTH = 5
@@ -22,6 +22,11 @@ class Hashira::Pipeline
22
22
 
23
23
  attr_reader :project
24
24
 
25
+ def verify
26
+ boundaries.interpreted
27
+ self
28
+ end
29
+
25
30
  def unparsed = parsed.unparsed
26
31
 
27
32
  def graph = coupling.graph
@@ -30,7 +35,7 @@ class Hashira::Pipeline
30
35
 
31
36
  def duplication = @_duplication ||= analyzed(:duplication, Hashira::Duplication::Clones, churn)
32
37
 
33
- def smells = @_smells ||= analyzed(:smells, Hashira::Smells::Report)
38
+ def smells = @_smells ||= analyzed(:smells, Hashira::Smells::Report, boundaries)
34
39
 
35
40
  def hotspots
36
41
  @_hotspots ||= Hashira::Hotspots::Rollup.new(complexity, duplication, churn) if complexity || duplication
@@ -52,6 +57,8 @@ class Hashira::Pipeline
52
57
 
53
58
  def parsed = @_parsed ||= Hashira::Trees.new(@project)
54
59
 
60
+ def boundaries = @_boundaries ||= Hashira::Boundaries.new(@project.boundaries, parsed.all)
61
+
55
62
  def coupling
56
63
  @_coupling ||= Hashira::Coupling::Report.new(@project, parsed.all, packaging: settle(@packaging))
57
64
  end
@@ -3,10 +3,13 @@
3
3
  class Hashira::Project
4
4
  ROOT_PACKAGE = "(root)"
5
5
 
6
- def initialize(requested)
6
+ def initialize(requested, boundaries: [])
7
7
  @requested = requested
8
+ @boundaries = boundaries
8
9
  end
9
10
 
11
+ attr_reader :boundaries
12
+
10
13
  def directories = @_directories ||= resolved
11
14
 
12
15
  def rails? = directories.any? { config?(File.expand_path(it)) }
@@ -14,7 +14,7 @@ module Hashira::Report::Phrases
14
14
  "Introduce a parameter object."
15
15
  end
16
16
 
17
- def on_duplicate_method_call(finding)
17
+ def on_repeated_call(finding)
18
18
  "#{finding.package} repeats identical calls (#{finding.detail[:site]}). " \
19
19
  "Name the result in a local variable."
20
20
  end
@@ -32,7 +32,7 @@ module Hashira::Report::Phrases
32
32
  "The behavior may belong on #{names.first}."
33
33
  end
34
34
 
35
- def on_instance_variable_assumption(finding)
35
+ def on_assumed_state(finding)
36
36
  "#{finding.package} reads instance variables nothing in the class assigns (#{finding.detail[:site]}). " \
37
37
  "Assign them where the object is built, or pass the data explicitly."
38
38
  end
@@ -56,7 +56,7 @@ module Hashira::Report::Phrases
56
56
  tally(finding, "branches on the same test %d times", "Replace the scattered checks with polymorphism.")
57
57
  end
58
58
 
59
- def on_too_many_instance_variables(finding)
59
+ def on_state_sprawl(finding)
60
60
  tally(finding, "holds %d instance variables", "Split the class, or gather related fields into value objects.")
61
61
  end
62
62
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "prism"
4
4
 
5
- class Hashira::Smells::InstanceVariableAssumption < Hashira::Smells::Check
5
+ class Hashira::Smells::AssumedState < Hashira::Smells::Check
6
6
  MAYBES = [
7
7
  Prism::LocalVariableOrWriteNode, Prism::InstanceVariableOrWriteNode,
8
8
  Prism::ClassVariableOrWriteNode, Prism::GlobalVariableOrWriteNode,
@@ -9,13 +9,14 @@ class Hashira::Smells::BoundarySprawl
9
9
 
10
10
  SHOWN = 4
11
11
 
12
- def initialize(subjects, ownership)
12
+ def initialize(subjects, ownership, interpreted = [])
13
13
  @subjects = subjects
14
14
  @ownership = ownership
15
+ @interpreted = interpreted
15
16
  end
16
17
 
17
18
  def findings
18
- reaches.filter_map { |root, contexts| finding(root, contexts) }
19
+ reaches.except(*@interpreted).filter_map { |root, contexts| finding(root, contexts) }
19
20
  end
20
21
 
21
22
  private
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "prism"
4
4
 
5
- class Hashira::Smells::DuplicateMethodCall < Hashira::Smells::Check
5
+ class Hashira::Smells::RepeatedCall < Hashira::Smells::Check
6
6
  LIMIT = 1
7
7
 
8
8
  MINTS = %i[new dup clone allocate rand srand].freeze
@@ -4,33 +4,34 @@ require_relative "boundary_sprawl"
4
4
  require_relative "check"
5
5
  require_relative "control_parameter"
6
6
  require_relative "data_clump"
7
- require_relative "duplicate_method_call"
7
+ require_relative "repeated_call"
8
8
  require_relative "feature_envy"
9
9
  require_relative "foreign"
10
- require_relative "instance_variable_assumption"
10
+ require_relative "assumed_state"
11
11
  require_relative "kind"
12
12
  require_relative "manual_dispatch"
13
13
  require_relative "module_initialize"
14
14
  require_relative "nil_check"
15
15
  require_relative "ownership"
16
16
  require_relative "repeated_conditional"
17
- require_relative "too_many_instance_variables"
17
+ require_relative "state_sprawl"
18
18
  require_relative "utility_function"
19
19
 
20
20
  class Hashira::Smells::Report
21
21
  CHECKS = Hashira::Smells::Check.subclasses.sort_by(&:name).freeze
22
22
 
23
23
  JUDGES = [
24
- Hashira::Smells::DataClump, Hashira::Smells::InstanceVariableAssumption,
24
+ Hashira::Smells::DataClump, Hashira::Smells::AssumedState,
25
25
  Hashira::Smells::ModuleInitialize, Hashira::Smells::RepeatedConditional,
26
- Hashira::Smells::TooManyInstanceVariables
26
+ Hashira::Smells::StateSprawl
27
27
  ].freeze
28
28
 
29
29
  PROBES = (CHECKS - JUDGES).freeze
30
30
 
31
- def initialize(project, trees)
31
+ def initialize(project, trees, boundaries = nil)
32
32
  @project = project
33
33
  @trees = trees
34
+ @boundaries = boundaries
34
35
  end
35
36
 
36
37
  def findings = @_findings ||= sniff(types, JUDGES) + sniff(methods, PROBES) + sprawl
@@ -43,7 +44,9 @@ class Hashira::Smells::Report
43
44
 
44
45
  def methods = types.flat_map(&:defs)
45
46
 
46
- def sprawl = Hashira::Smells::BoundarySprawl.new(methods, census.ownership).findings
47
+ def sprawl = Hashira::Smells::BoundarySprawl.new(methods, census.ownership, interpreted).findings
48
+
49
+ def interpreted = @boundaries ? @boundaries.interpreted : []
47
50
 
48
51
  def sniff(subjects, checks) = subjects.flat_map { |subject| verdicts(subject, checks) }
49
52
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "prism"
4
4
 
5
- class Hashira::Smells::TooManyInstanceVariables < Hashira::Smells::Check
5
+ class Hashira::Smells::StateSprawl < Hashira::Smells::Check
6
6
  LIMIT = 4
7
7
 
8
8
  COUNTED = [
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hashira
4
- VERSION = "0.9.0"
4
+ VERSION = "0.10.0"
5
5
  end
data/lib/hashira.rb CHANGED
@@ -7,6 +7,9 @@ module Hashira
7
7
  module CI
8
8
  end
9
9
 
10
+ module CLI
11
+ end
12
+
10
13
  module Coupling
11
14
  end
12
15
 
@@ -39,8 +42,8 @@ require_relative "hashira/ci/accepted"
39
42
  require_relative "hashira/ci/baseline"
40
43
  require_relative "hashira/ci/comparison"
41
44
  require_relative "hashira/ci/diff"
42
- require_relative "hashira/ci/edge_diff_report"
43
- require_relative "hashira/ci/finding_diff_report"
45
+ require_relative "hashira/ci/edge_changes"
46
+ require_relative "hashira/ci/finding_changes"
44
47
  require_relative "hashira/ci/gate"
45
48
  require_relative "hashira/ci/improvement"
46
49
  require_relative "hashira/ci/mark"
@@ -50,7 +53,6 @@ require_relative "hashira/ci/scope"
50
53
  require_relative "hashira/ci/slice"
51
54
  require_relative "hashira/ci/status"
52
55
  require_relative "hashira/ci/sweep"
53
- require_relative "hashira/cli"
54
56
  require_relative "hashira/cli/arguments"
55
57
  require_relative "hashira/cli/choice"
56
58
  require_relative "hashira/cli/command_line"
@@ -63,6 +65,7 @@ require_relative "hashira/cli/only"
63
65
  require_relative "hashira/cli/options"
64
66
  require_relative "hashira/cli/package_by"
65
67
  require_relative "hashira/cli/run"
68
+ require_relative "hashira/cli/session"
66
69
  require_relative "hashira/cli/skip"
67
70
  require_relative "hashira/cli/usage"
68
71
  require_relative "hashira/complexity/boolean_run"
@@ -77,7 +80,7 @@ require_relative "hashira/coupling/audiences"
77
80
  require_relative "hashira/coupling/catalog"
78
81
  require_relative "hashira/coupling/census"
79
82
  require_relative "hashira/coupling/constant_registry"
80
- require_relative "hashira/coupling/cycle_findings"
83
+ require_relative "hashira/coupling/cycle"
81
84
  require_relative "hashira/coupling/cycle_search"
82
85
  require_relative "hashira/coupling/cycles"
83
86
  require_relative "hashira/coupling/definition"
@@ -88,7 +91,7 @@ require_relative "hashira/coupling/folder_placement"
88
91
  require_relative "hashira/coupling/folding"
89
92
  require_relative "hashira/coupling/graph"
90
93
  require_relative "hashira/coupling/metric"
91
- require_relative "hashira/coupling/mixed_audience_findings"
94
+ require_relative "hashira/coupling/mixed_audience"
92
95
  require_relative "hashira/coupling/namespace_placement"
93
96
  require_relative "hashira/coupling/namespace_prefix"
94
97
  require_relative "hashira/coupling/naming"
@@ -97,13 +100,13 @@ require_relative "hashira/coupling/placement"
97
100
  require_relative "hashira/coupling/references"
98
101
  require_relative "hashira/coupling/report"
99
102
  require_relative "hashira/coupling/roll_call"
100
- require_relative "hashira/coupling/roll_call_findings"
103
+ require_relative "hashira/coupling/echo"
101
104
  require_relative "hashira/coupling/roster"
102
105
  require_relative "hashira/coupling/rule"
103
106
  require_relative "hashira/coupling/scope"
104
107
  require_relative "hashira/coupling/sdp_check"
105
- require_relative "hashira/coupling/sdp_violation_findings"
106
- require_relative "hashira/coupling/wide_edge_findings"
108
+ require_relative "hashira/coupling/imbalance"
109
+ require_relative "hashira/coupling/wide_edge"
107
110
  require_relative "hashira/coupling/words"
108
111
  require_relative "hashira/diagram/dot"
109
112
  require_relative "hashira/diagram/mermaid"
@@ -125,6 +128,7 @@ require_relative "hashira/duplication/similarity"
125
128
  require_relative "hashira/duplication/union_find"
126
129
  require_relative "hashira/duplication/variance"
127
130
  require_relative "hashira/error"
131
+ require_relative "hashira/boundaries"
128
132
  require_relative "hashira/focus"
129
133
  require_relative "hashira/hotspots/file_cost"
130
134
  require_relative "hashira/hotspots/rollup"
@@ -151,9 +155,9 @@ require_relative "hashira/smells/conditions"
151
155
  require_relative "hashira/smells/contexts"
152
156
  require_relative "hashira/smells/control_parameter"
153
157
  require_relative "hashira/smells/data_clump"
154
- require_relative "hashira/smells/duplicate_method_call"
158
+ require_relative "hashira/smells/repeated_call"
155
159
  require_relative "hashira/smells/feature_envy"
156
- require_relative "hashira/smells/instance_variable_assumption"
160
+ require_relative "hashira/smells/assumed_state"
157
161
  require_relative "hashira/smells/lineage"
158
162
  require_relative "hashira/smells/manual_dispatch"
159
163
  require_relative "hashira/smells/module_initialize"
@@ -163,7 +167,7 @@ require_relative "hashira/smells/refs"
163
167
  require_relative "hashira/smells/repeated_conditional"
164
168
  require_relative "hashira/smells/report"
165
169
  require_relative "hashira/smells/scope"
166
- require_relative "hashira/smells/too_many_instance_variables"
170
+ require_relative "hashira/smells/state_sprawl"
167
171
  require_relative "hashira/smells/utility_function"
168
172
  require_relative "hashira/smells/visibility"
169
173
  require_relative "hashira/trees"
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.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Giacomo GK
@@ -30,13 +30,14 @@ files:
30
30
  - lib/hashira/analysis/node_walk.rb
31
31
  - lib/hashira/analysis/syntax.rb
32
32
  - lib/hashira/analysis/type_walk.rb
33
+ - lib/hashira/boundaries.rb
33
34
  - lib/hashira/churn.rb
34
35
  - lib/hashira/ci/accepted.rb
35
36
  - lib/hashira/ci/baseline.rb
36
37
  - lib/hashira/ci/comparison.rb
37
38
  - lib/hashira/ci/diff.rb
38
- - lib/hashira/ci/edge_diff_report.rb
39
- - lib/hashira/ci/finding_diff_report.rb
39
+ - lib/hashira/ci/edge_changes.rb
40
+ - lib/hashira/ci/finding_changes.rb
40
41
  - lib/hashira/ci/gate.rb
41
42
  - lib/hashira/ci/improvement.rb
42
43
  - lib/hashira/ci/mark.rb
@@ -46,7 +47,6 @@ files:
46
47
  - lib/hashira/ci/slice.rb
47
48
  - lib/hashira/ci/status.rb
48
49
  - lib/hashira/ci/sweep.rb
49
- - lib/hashira/cli.rb
50
50
  - lib/hashira/cli/arguments.rb
51
51
  - lib/hashira/cli/choice.rb
52
52
  - lib/hashira/cli/command_line.rb
@@ -59,6 +59,7 @@ files:
59
59
  - lib/hashira/cli/options.rb
60
60
  - lib/hashira/cli/package_by.rb
61
61
  - lib/hashira/cli/run.rb
62
+ - lib/hashira/cli/session.rb
62
63
  - lib/hashira/cli/skip.rb
63
64
  - lib/hashira/cli/top.rb
64
65
  - lib/hashira/cli/usage.rb
@@ -74,18 +75,20 @@ files:
74
75
  - lib/hashira/coupling/catalog.rb
75
76
  - lib/hashira/coupling/census.rb
76
77
  - lib/hashira/coupling/constant_registry.rb
77
- - lib/hashira/coupling/cycle_findings.rb
78
+ - lib/hashira/coupling/cycle.rb
78
79
  - lib/hashira/coupling/cycle_search.rb
79
80
  - lib/hashira/coupling/cycles.rb
80
81
  - lib/hashira/coupling/definition.rb
81
82
  - lib/hashira/coupling/definitions.rb
83
+ - lib/hashira/coupling/echo.rb
82
84
  - lib/hashira/coupling/edge.rb
83
85
  - lib/hashira/coupling/edge_map.rb
84
86
  - lib/hashira/coupling/folder_placement.rb
85
87
  - lib/hashira/coupling/folding.rb
86
88
  - lib/hashira/coupling/graph.rb
89
+ - lib/hashira/coupling/imbalance.rb
87
90
  - lib/hashira/coupling/metric.rb
88
- - lib/hashira/coupling/mixed_audience_findings.rb
91
+ - lib/hashira/coupling/mixed_audience.rb
89
92
  - lib/hashira/coupling/namespace_placement.rb
90
93
  - lib/hashira/coupling/namespace_prefix.rb
91
94
  - lib/hashira/coupling/naming.rb
@@ -94,13 +97,11 @@ files:
94
97
  - lib/hashira/coupling/references.rb
95
98
  - lib/hashira/coupling/report.rb
96
99
  - lib/hashira/coupling/roll_call.rb
97
- - lib/hashira/coupling/roll_call_findings.rb
98
100
  - lib/hashira/coupling/roster.rb
99
101
  - lib/hashira/coupling/rule.rb
100
102
  - lib/hashira/coupling/scope.rb
101
103
  - lib/hashira/coupling/sdp_check.rb
102
- - lib/hashira/coupling/sdp_violation_findings.rb
103
- - lib/hashira/coupling/wide_edge_findings.rb
104
+ - lib/hashira/coupling/wide_edge.rb
104
105
  - lib/hashira/coupling/words.rb
105
106
  - lib/hashira/diagram/dot.rb
106
107
  - lib/hashira/diagram/mermaid.rb
@@ -141,6 +142,7 @@ files:
141
142
  - lib/hashira/report/smell_phrases.rb
142
143
  - lib/hashira/report/text.rb
143
144
  - lib/hashira/report/view.rb
145
+ - lib/hashira/smells/assumed_state.rb
144
146
  - lib/hashira/smells/boundary_sprawl.rb
145
147
  - lib/hashira/smells/branches.rb
146
148
  - lib/hashira/smells/census.rb
@@ -149,10 +151,8 @@ files:
149
151
  - lib/hashira/smells/contexts.rb
150
152
  - lib/hashira/smells/control_parameter.rb
151
153
  - lib/hashira/smells/data_clump.rb
152
- - lib/hashira/smells/duplicate_method_call.rb
153
154
  - lib/hashira/smells/feature_envy.rb
154
155
  - lib/hashira/smells/foreign.rb
155
- - lib/hashira/smells/instance_variable_assumption.rb
156
156
  - lib/hashira/smells/kind.rb
157
157
  - lib/hashira/smells/lineage.rb
158
158
  - lib/hashira/smells/manual_dispatch.rb
@@ -161,10 +161,11 @@ files:
161
161
  - lib/hashira/smells/ownership.rb
162
162
  - lib/hashira/smells/param_check.rb
163
163
  - lib/hashira/smells/refs.rb
164
+ - lib/hashira/smells/repeated_call.rb
164
165
  - lib/hashira/smells/repeated_conditional.rb
165
166
  - lib/hashira/smells/report.rb
166
167
  - lib/hashira/smells/scope.rb
167
- - lib/hashira/smells/too_many_instance_variables.rb
168
+ - lib/hashira/smells/state_sprawl.rb
168
169
  - lib/hashira/smells/utility_function.rb
169
170
  - lib/hashira/smells/visibility.rb
170
171
  - lib/hashira/trees.rb