crap4ruby 0.1.0 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55e5f45f41b35122c6ad4b4894e3d5e496af3842c4a4f28e8172806c45d8d620
4
- data.tar.gz: '0279b34834aeba13649a0f124b6064b44a92fdffa868ad5f1a893e56a9067473'
3
+ metadata.gz: ef4ff5757bc98e35d4f9a59b34c2de273c8372b4b3019f0f1cd2234393900150
4
+ data.tar.gz: 7d05a4a6376b345e41ccaaba2311392e22608c4712ae61bae210a8ad392f540a
5
5
  SHA512:
6
- metadata.gz: 555c402719177c91525c68611ee3e5e88ae0c674bf74a1f2b477857d4e97e017cfe50a2674d829e46a9fd7c16a252d69aaf97953ebb811d6a98a733596898157
7
- data.tar.gz: 0e9ea5f5d64473c6547faaad0cc4126a0d7a1c0236c37c0a457cdb1e69ae5d828160619ddce843033df76c723c45440f64049b071a5bf74846ad11447700a1d4
6
+ metadata.gz: d0f0ae1b378c537b8eaac2972fa8bf866aeb3b0771fe0e66062b7e51309e2d08ae45c91a697ed948f5cb2e3837b2d0d7c21064c2fbf3542c9a764a1295bed790
7
+ data.tar.gz: b9454920d722a74389ae717afaa0cf01205b16ba08aeea24a814161ebf3b519ad2388d2b82a2a78bb9e8b244b723f5f8fcfd8c7198b9e0c55bb4e93564a7c5e3
data/CHANGELOG.md CHANGED
@@ -1,7 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ All notable changes are recorded here using [Keep a Changelog 2.0.0](https://keepachangelog.com/en/2.0.0/).
4
+ Version numbers follow [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html) as defined in the [versioning policy](VERSIONING.md).
5
+
3
6
  ## [Unreleased]
4
7
 
8
+ ## [0.1.1] - 2026-10-28
9
+
10
+ ### Changed
11
+
12
+ - Document single-file analysis, contribution and private reporting policies, and the public compatibility policy.
13
+ - Add a YARD maintainer reference and link to gem documentation from RubyGems metadata.
14
+
5
15
  ## [0.1.0] - 2026-10-28
6
16
 
7
17
  ### Added
@@ -16,5 +26,6 @@
16
26
 
17
27
  - Split CLI execution, AST discovery, and coverage loading into smaller methods without changing their behavior or scoring rules.
18
28
 
19
- [Unreleased]: https://github.com/svyatov/crap4ruby/compare/v0.1.0...HEAD
29
+ [Unreleased]: https://github.com/svyatov/crap4ruby/compare/v0.1.1...HEAD
30
+ [0.1.1]: https://github.com/svyatov/crap4ruby/compare/v0.1.0...v0.1.1
20
31
  [0.1.0]: https://github.com/svyatov/crap4ruby/releases/tag/v0.1.0
@@ -0,0 +1,90 @@
1
+ # Contributor Covenant 3.0 Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We pledge to make our community welcoming, safe, and equitable for all.
6
+
7
+ We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or religion, national or social origin, socio-economic position, level of education, or other status. The same privileges of participation are extended to everyone who participates in good faith and in accordance with this Covenant.
8
+
9
+
10
+ ## Encouraged Behaviors
11
+
12
+ While acknowledging differences in social norms, we all strive to meet our community's expectations for positive behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture, background, or native language.
13
+
14
+ With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including:
15
+
16
+ 1. Respecting the **purpose of our community**, our activities, and our ways of gathering.
17
+ 2. Engaging **kindly and honestly** with others.
18
+ 3. Respecting **different viewpoints** and experiences.
19
+ 4. **Taking responsibility** for our actions and contributions.
20
+ 5. Gracefully giving and accepting **constructive feedback**.
21
+ 6. Committing to **repairing harm** when it occurs.
22
+ 7. Behaving in other ways that promote and sustain the **well-being of our community**.
23
+
24
+
25
+ ## Restricted Behaviors
26
+
27
+ We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct.
28
+
29
+ 1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any clear request to stop.
30
+ 2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of people.
31
+ 3. **Stereotyping or discrimination.** Characterizing anyone’s personality or behavior on the basis of immutable identities or traits.
32
+ 4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or purpose of the community.
33
+ 5. **Violating confidentiality**. Sharing or acting on someone's personal or private information without their permission.
34
+ 6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group.
35
+ 7. Behaving in other ways that **threaten the well-being** of our community.
36
+
37
+ ### Other Restrictions
38
+
39
+ 1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade enforcement actions.
40
+ 2. **Failing to credit sources.** Not properly crediting the sources of content you contribute.
41
+ 3. **Promotional materials**. Sharing marketing or other commercial content in a way that is outside the norms of the community.
42
+ 4. **Irresponsible communication.** Failing to responsibly present content which includes, links or describes any other restricted behaviors.
43
+
44
+
45
+ ## Reporting an Issue
46
+
47
+ Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm.
48
+
49
+ When an incident does occur, it is important to report it promptly. To report a possible violation, email [leonid@svyatov.com](mailto:leonid@svyatov.com). Leonid Svyatov monitors this address and will respond within 14 days.
50
+
51
+ Community Moderators take reports of violations seriously and will make every effort to respond in a timely manner. They will investigate all reports of code of conduct violations, reviewing messages, logs, and recordings, or interviewing witnesses and other participants. Community Moderators will keep investigation and enforcement actions as transparent as possible while prioritizing safety and confidentiality. In order to honor these values, enforcement actions are carried out in private with the involved parties, but communicating to the whole community may be part of a mutually agreed upon resolution.
52
+
53
+
54
+ ## Addressing and Repairing Harm
55
+
56
+ If an investigation by the Community Moderators finds that this Code of Conduct has been violated, the following enforcement ladder may be used to determine how best to repair harm, based on the incident's impact on the individuals involved and the community as a whole. Depending on the severity of a violation, lower rungs on the ladder may be skipped.
57
+
58
+ 1) Warning
59
+ 1) Event: A violation involving a single incident or series of incidents.
60
+ 2) Consequence: A private, written warning from the Community Moderators.
61
+ 3) Repair: Examples of repair include a private written apology, acknowledgement of responsibility, and seeking clarification on expectations.
62
+ 2) Temporarily Limited Activities
63
+ 1) Event: A repeated incidence of a violation that previously resulted in a warning, or the first incidence of a more serious violation.
64
+ 2) Consequence: A private, written warning with a time-limited cooldown period designed to underscore the seriousness of the situation and give the community members involved time to process the incident. The cooldown period may be limited to particular communication channels or interactions with particular community members.
65
+ 3) Repair: Examples of repair may include making an apology, using the cooldown period to reflect on actions and impact, and being thoughtful about re-entering community spaces after the period is over.
66
+ 3) Temporary Suspension
67
+ 1) Event: A pattern of repeated violation which the Community Moderators have tried to address with warnings, or a single serious violation.
68
+ 2) Consequence: A private written warning with conditions for return from suspension. In general, temporary suspensions give the person being suspended time to reflect upon their behavior and possible corrective actions.
69
+ 3) Repair: Examples of repair include respecting the spirit of the suspension, meeting the specified conditions for return, and being thoughtful about how to reintegrate with the community when the suspension is lifted.
70
+ 4) Permanent Ban
71
+ 1) Event: A pattern of repeated code of conduct violations that other steps on the ladder have failed to resolve, or a violation so serious that the Community Moderators determine there is no way to keep the community safe with this person as a member.
72
+ 2) Consequence: Access to all community spaces, tools, and communication channels is removed. In general, permanent bans should be rarely used, should have strong reasoning behind them, and should only be resorted to if working through other remedies has failed to change the behavior.
73
+ 3) Repair: There is no possible repair in cases of this severity.
74
+
75
+ This enforcement ladder is intended as a guideline. It does not limit the ability of Community Managers to use their discretion and judgment, in keeping with the best interests of our community.
76
+
77
+
78
+ ## Scope
79
+
80
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public or other spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
81
+
82
+
83
+ ## Attribution
84
+
85
+ This Code of Conduct is adapted from the Contributor Covenant, version 3.0, permanently available at [https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/).
86
+
87
+ Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA 4.0. To view a copy of this license, visit [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/)
88
+
89
+ For answers to common questions about Contributor Covenant, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are provided at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). Additional enforcement and community guideline resources can be found at [https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources). The enforcement ladder was inspired by the work of [Mozilla’s code of conduct team](https://github.com/mozilla/inclusion).
90
+
data/CONTRIBUTING.md CHANGED
@@ -1,15 +1,44 @@
1
1
  # Contributing
2
2
 
3
- Use Ruby 3.3 or newer. Install dependencies and run the suite:
3
+ Outside contributions are welcome. This guide defines the project's [acceptance criteria](#acceptance-criteria).
4
+
5
+ ## Development
6
+
7
+ Use Ruby 3.3 or newer. Install [Betterleaks](https://github.com/betterleaks/betterleaks) with the `betterleaks` command on your PATH.
8
+ Set up dependencies and Git hooks, then run the suite:
4
9
 
5
10
  ```sh
6
- bundle install
11
+ bin/setup
7
12
  bundle exec rake
8
13
  ```
9
14
 
10
- The default check runs tests with fresh SimpleCov coverage and requires every library method's CRAP score to be at most 10.
15
+ Before each commit, hooks scan staged changes for secrets and whitespace errors.
16
+ Commit messages must use Conventional Commits.
17
+ Before each push, hooks run `bundle exec rake`.
18
+ Put machine-specific hook settings in the ignored `lefthook-local.yml` file.
19
+
20
+ The default check runs Steep, requires 100% typed calls across every Ruby file, runs tests with fresh SimpleCov coverage, and validates YARD documentation.
21
+ Every library method's CRAP score must be at most 10.
11
22
  Use `bundle exec rake test` for tests alone. Open `coverage/index.html` to inspect line coverage.
12
23
 
24
+ Run `bundle exec rake types` for type checks alone.
25
+ Keep library signatures in `sig/` and signatures for tests and development tools in `sig-development/`.
26
+ The type gate rejects unchecked Ruby files, untyped signatures, and any call that Steep cannot type.
27
+ Add new source locations to `Steepfile` when the gate reports an unchecked file.
28
+
29
+ Document public library methods and data readers with YARD summaries, parameter types, return types, and relevant exceptions.
30
+ Explain private methods when their constraints or side effects need context.
31
+ Keep types consistent with `sig/` and include examples when they clarify usage.
32
+ The Ruby reference is for maintainers and does not extend the [public compatibility policy](VERSIONING.md).
33
+
34
+ Run `bundle exec rake docs` to generate the reference, including private methods, then open `doc/index.html`.
35
+ Run `bundle exec rake docs:check` to reject YARD warnings, undocumented public methods, and readers without explicit documentation and return types.
36
+ Generated files in `doc/` and `.yardoc/` are ignored.
37
+
38
+ After changing dependencies, use Ruby 3.3 and the Bundler version recorded in `Gemfile.lock`.
39
+ Run `bundle exec rake lock` to refresh both lockfiles and their checksums.
40
+ CI installs the release tools from `gemfiles/release/Gemfile.lock` in frozen mode.
41
+
13
42
  Add a regression test for a behavior change. Use temporary projects for filesystem, Git, coverage, and CLI tests.
14
43
  Keep analyzed code out of the tool's process. Preserve the previous snapshot when analysis fails.
15
44
 
@@ -17,3 +46,21 @@ For defect reports, include the Ruby version, command, expected result, actual r
17
46
  Coverage defects also need a matching excerpt from `.resultset.json`. Remove application secrets before sharing examples.
18
47
 
19
48
  Changes use Conventional Commits. Keep code, tests, and documentation for one behavior in a focused commit.
49
+
50
+ ## Acceptance criteria
51
+
52
+ - Keep each change focused on one behavior or correction.
53
+ - Include tests for new functionality and regression tests for behavior changes.
54
+ - Pass `bundle exec rake`, including the method CRAP limit.
55
+ - Update documentation when user-visible behavior changes.
56
+ - Follow the [code of conduct](CODE_OF_CONDUCT.md).
57
+
58
+ ## Submit a change
59
+
60
+ Fork the repository and create a branch from `main`. Commit your change, push the branch to your fork, and open a pull request against `svyatov/crap4ruby:main`.
61
+ Describe the behavior being changed and any effect on existing users. Link the related issue when one exists.
62
+
63
+ ## Decisions and succession
64
+
65
+ Leonid Svyatov (@svyatov) is the sole maintainer and decides which changes to merge and release.
66
+ No succession arrangement exists if he stops maintaining the project.
data/README.md CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  crap4ruby identifies complex, under-tested Ruby methods using the Change Risk Anti-Pattern (CRAP) metric.
4
4
 
5
- - Analyzes Ruby 3.3+ source with Prism without loading application code.
6
- - Reads SimpleCov reports, including legacy formats and multiple test suites.
7
- - Prints method scores and writes the snapshot format used by [crapper](https://github.com/unclebob/crapper).
5
+ - Analyzes Ruby 3.3+ methods without loading application code.
6
+ - Ranks methods using complexity and test coverage, rather than complexity alone.
7
+ - Reads existing SimpleCov reports and merges coverage across test suites.
8
+ - Checks one file, directories, or only files changed in Git.
8
9
 
9
10
  ## Install
10
11
 
@@ -14,12 +15,22 @@ Ruby 3.3 or newer is required.
14
15
  gem install crap4ruby
15
16
  ```
16
17
 
17
- In the project you want to measure:
18
+ In an empty directory, create a sample and analyze it:
19
+
20
+ ```sh
21
+ printf 'def eligible?(age) = age >= 18\n' > sample.rb
22
+ crap4ruby --no-coverage sample.rb
23
+ # Reports eligible? with CC 1; coverage and CRAP are N/A.
24
+ # Writes .metrics/crap.edn.
25
+ ```
26
+
27
+ In an existing project, select the source you want to measure:
18
28
 
19
29
  ```sh
20
30
  crap4ruby --no-coverage
21
31
  crap4ruby
22
32
  crap4ruby --threshold 30
33
+ crap4ruby app/models/user.rb
23
34
  crap4ruby app/models lib/billing
24
35
  ```
25
36
 
@@ -35,7 +46,7 @@ Run these commands from this checkout with Ruby 3.3 or newer:
35
46
  ```sh
36
47
  bundle install
37
48
  gem build crap4ruby.gemspec
38
- gem install ./crap4ruby-0.1.0.gem
49
+ gem install ./crap4ruby-0.1.1.gem
39
50
  ```
40
51
 
41
52
  To use Bundler in the measured project, add this checkout to that project's `Gemfile`:
@@ -48,7 +59,7 @@ Then run `bundle install` and `bundle exec crap4ruby` in that project.
48
59
 
49
60
  ## Set up coverage
50
61
 
51
- [SimpleCov](https://github.com/simplecov-ruby/simplecov) must start before the application loads. Add it to the measured project's test dependencies:
62
+ [SimpleCov](https://github.com/simplecov-ruby/simplecov) must start before the application loads. Add it to the measured project's `Gemfile`:
52
63
 
53
64
  ```ruby
54
65
  gem "simplecov", require: false, group: :test
@@ -104,7 +115,7 @@ Every successful analysis replaces the snapshot, including filtered analyses and
104
115
 
105
116
  ## Complexity and coverage
106
117
 
107
- The formula uses a coverage fraction between 0 and 1:
118
+ The scoring engine uses this formula, with coverage expressed as a fraction between 0 and 1:
108
119
 
109
120
  ```text
110
121
  CRAP = CC^2 * (1 - coverage)^3 + CC
@@ -178,11 +189,14 @@ The implementation is tested on CRuby 3.3, 3.4, and 4.0. Other Ruby implementati
178
189
 
179
190
  ## Releases
180
191
 
192
+ The [versioning policy](VERSIONING.md) defines the public interface, version changes, and deprecation rules.
193
+
181
194
  Signed `v*` tags trigger the release workflow. The tag must match `Crap4Ruby::VERSION`.
182
195
  Tests and the CRAP limit run before the gem is built. Publication requires approval in GitHub's `release` environment.
183
196
  RubyGems trusted publishing authenticates the workflow. Sigstore signs the built gem, and RubyGems stores its attestation.
184
197
 
185
198
  Report defects or ask questions through [GitHub issues](https://github.com/svyatov/crap4ruby/issues).
199
+ Leonid Svyatov maintains this project. Security fixes target the latest release; see the [security policy](SECURITY.md).
186
200
 
187
201
  ## License
188
202
 
data/SECURITY.md ADDED
@@ -0,0 +1,15 @@
1
+ # Security policy
2
+
3
+ ## Report a vulnerability
4
+
5
+ Send private reports to [leonid@svyatov.com](mailto:leonid@svyatov.com). Do not disclose an unpatched vulnerability in a public issue.
6
+
7
+ Include the affected crap4ruby and Ruby versions, a minimal reproduction, and the expected security impact.
8
+ Remove credentials and private application data from examples.
9
+
10
+ Leonid Svyatov monitors this address and will respond within 14 days.
11
+ The response will confirm receipt and describe the next investigation step; it does not promise a fix within that period.
12
+
13
+ ## Supported versions
14
+
15
+ Security fixes target the latest release. Reproduce the issue with that release when possible, but report suspected vulnerabilities in older releases too.
data/VERSIONING.md ADDED
@@ -0,0 +1,30 @@
1
+ # Versioning policy
2
+
3
+ crap4ruby uses [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html) for its documented public interface.
4
+
5
+ ## Public interface
6
+
7
+ The compatibility policy covers:
8
+
9
+ - Documented CLI options, positional file selection, and exit codes.
10
+ - Documented complexity, coverage, and CRAP scoring semantics.
11
+ - The `.metrics/crap.edn` schema, including field meanings and value types.
12
+ - The minimum supported Ruby version.
13
+
14
+ Terminal table spacing, diagnostic wording, and Ruby classes are implementation details.
15
+ YARD documents the Ruby interfaces for maintainers; those interfaces are outside this compatibility policy.
16
+ Use the CLI and documented snapshot format when integrating with other tools.
17
+
18
+ ## Version changes
19
+
20
+ During initial development (`0.y.z`), incompatible public changes increase MINOR. Compatible additions, fixes, and deprecations increase PATCH.
21
+ Starting with `1.0.0`, incompatible public changes increase MAJOR, compatible additions and deprecations increase MINOR, and compatible fixes increase PATCH.
22
+
23
+ Each release tag, gem version, and changelog entry must identify the same version. Published versions are never replaced.
24
+ Documentation and CI maintenance alone do not require a release.
25
+
26
+ ## Deprecation
27
+
28
+ Deprecate a public interface in a release before removing it in a later release.
29
+ The notice must name its replacement or migration path and the earliest removal version, using a CLI diagnostic where applicable.
30
+ Removal requires a later MINOR release during initial development or a later MAJOR release after `1.0.0`.
@@ -1,7 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Crap4Ruby
4
+ # Finds explicit Ruby method definitions without executing source code.
4
5
  class Analyzer
6
+ # Analyzes method bodies and parameter defaults with Prism.
7
+ #
8
+ # @param source [String] Ruby source text
9
+ # @param path [String] source label used in results and parse errors
10
+ # @return [Array<MethodInfo>] methods in traversal order, including nested definitions
11
+ # @raise [Error] if source cannot be parsed
12
+ # @example Analyze a method without loading it
13
+ # Analyzer.new.call("def eligible?(age) = age >= 18", path: "person.rb")
5
14
  def call(source, path:)
6
15
  result = Prism.parse(source)
7
16
  unless result.success?
@@ -13,6 +22,13 @@ module Crap4Ruby
13
22
 
14
23
  private
15
24
 
25
+ # Tracks lexical owners; skips owners that cannot be resolved statically.
26
+ #
27
+ # @param node [Prism::Node, nil] subtree to visit
28
+ # @param scope [Array<String>] lexical namespace components
29
+ # @param singleton [Boolean] whether definitions belong to a singleton class
30
+ # @param path [String] source label
31
+ # @return [Array<MethodInfo>] discovered method definitions
16
32
  def discover(node, scope, singleton, path)
17
33
  return [] unless node
18
34
 
@@ -30,6 +46,13 @@ module Crap4Ruby
30
46
  end
31
47
  end
32
48
 
49
+ # Measures a definition independently from definitions nested within it.
50
+ #
51
+ # @param node [Prism::DefNode] method definition
52
+ # @param scope [Array<String>] lexical namespace components
53
+ # @param singleton [Boolean] whether the enclosing scope is a singleton class
54
+ # @param path [String] source label
55
+ # @return [Array<MethodInfo>] this method and its nested definitions
33
56
  def discover_method(node, scope, singleton, path)
34
57
  owner = node.receiver && !node.receiver.is_a?(Prism::SelfNode) ? qualify(node.receiver, scope) : scope
35
58
  return [] unless owner
@@ -41,6 +64,13 @@ module Crap4Ruby
41
64
  discover(node.body, scope, singleton, path)
42
65
  end
43
66
 
67
+ # Resolves constant syntax without looking up runtime constants.
68
+ # Declarations append names; references can reuse a lexical ancestor.
69
+ #
70
+ # @param node [Prism::Node, nil] namespace expression
71
+ # @param scope [Array<String>] lexical namespace components
72
+ # @param declaration [Boolean] whether the expression declares a namespace
73
+ # @return [Array<String>, nil] qualified components, or nil for dynamic expressions
44
74
  def qualify(node, scope, declaration: false)
45
75
  case node
46
76
  when Prism::ParenthesesNode
@@ -50,10 +80,16 @@ module Crap4Ruby
50
80
  ancestor = scope.rindex(name) unless declaration
51
81
  ancestor ? scope.take(ancestor + 1) : scope + [name]
52
82
  when Prism::ConstantPathNode
53
- (node.parent ? qualify(node.parent, scope) : [])&.+([node.name.to_s])
83
+ # @type var parent: Array[String]?
84
+ parent = node.parent ? qualify(node.parent, scope) : []
85
+ parent&.+([node.name.to_s])
54
86
  end
55
87
  end
56
88
 
89
+ # Counts branch operations, including safe navigation, outside nested definitions.
90
+ #
91
+ # @param node [Prism::Node, nil] subtree to measure
92
+ # @return [Integer] number of decisions, excluding the method's base complexity
57
93
  def decisions(node)
58
94
  return 0 unless node
59
95
  return 0 if boundary?(node)
@@ -62,7 +98,12 @@ module Crap4Ruby
62
98
  :when_node, :in_node, :rescue_node, :rescue_modifier_node, :and_node, :or_node].include?(node.type) ||
63
99
  node.type.to_s.match?(/_(?:and|or)_write_node\z/)
64
100
  count = decision ? 1 : 0
65
- count += 1 if node.respond_to?(:safe_navigation?) && node.safe_navigation?
101
+ case node
102
+ when Prism::CallNode, Prism::CallAndWriteNode, Prism::CallOrWriteNode,
103
+ Prism::CallOperatorWriteNode, Prism::CallTargetNode, Prism::IndexAndWriteNode,
104
+ Prism::IndexOrWriteNode, Prism::IndexOperatorWriteNode, Prism::IndexTargetNode
105
+ count += 1 if node.safe_navigation?
106
+ end
66
107
  count + node.compact_child_nodes.sum { |child| decisions(child) }
67
108
  end
68
109
 
@@ -70,11 +111,15 @@ module Crap4Ruby
70
111
  [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].any? { |type| node.is_a?(type) }
71
112
  end
72
113
 
114
+ # Selects body lines without nested methods, classes, or modules.
115
+ #
116
+ # @param node [Prism::Node, nil] method body
117
+ # @return [Array<Integer>] one-based source lines; empty for an absent body
73
118
  def body_lines(node)
74
119
  return [] unless node
75
120
 
76
121
  lines = (node.location.start_line..node.location.end_line).to_a
77
- excluded = []
122
+ excluded = [] #: Array[Integer]
78
123
  collect_boundaries(node, excluded)
79
124
  lines - excluded
80
125
  end
data/lib/crap4ruby/cli.rb CHANGED
@@ -3,12 +3,25 @@
3
3
  require "optparse"
4
4
 
5
5
  module Crap4Ruby
6
+ # Coordinates file discovery, coverage, scoring, and CLI output.
6
7
  class CLI
8
+ # Sets output destinations without running an analysis.
9
+ #
10
+ # @param stdout [#puts, #write] destination for reports, help, and version output
11
+ # @param stderr [#puts, #write] destination for diagnostics and coverage command output
7
12
  def initialize(stdout: $stdout, stderr: $stderr)
8
13
  @stdout = stdout
9
14
  @stderr = stderr
10
15
  end
11
16
 
17
+ # Runs the requested analysis and replaces the snapshot after successful scoring.
18
+ # Help and version requests produce output without writing a snapshot.
19
+ # Expected usage, input, filesystem, and command errors are printed to stderr.
20
+ #
21
+ # @param arguments [Array<String>] command-line tokens; option parsing mutates this array
22
+ # @return [Integer] 0 on success, 1 on a handled error, or 2 when a score exceeds the threshold
23
+ # @example Analyze complexity without coverage
24
+ # CLI.new.run(["--no-coverage", "--source-root", "lib"])
12
25
  def run(arguments)
13
26
  options, parser, paths = parse_options(arguments)
14
27
  return @stdout.puts(parser) || 0 if options[:help]
@@ -35,18 +48,37 @@ module Crap4Ruby
35
48
 
36
49
  private
37
50
 
51
+ # Removes parsed options from arguments and returns the remaining path selectors.
52
+ #
53
+ # @param arguments [Array<String>] mutable command-line tokens
54
+ # @return [Array(Hash, OptionParser, Array<String>)] options, parser, and remaining selectors
55
+ # @raise [OptionParser::ParseError] if an option or threshold is invalid
38
56
  def parse_options(arguments)
57
+ # @type var options: options
39
58
  options = {root: Dir.pwd, source_roots: [], changed: false, coverage: "coverage/.resultset.json"}
40
59
  parser = OptionParser.new do |cli|
41
60
  cli.banner = "Usage: crap4ruby [options] [file-or-directory-or-filter ...]"
42
- cli.on("--root PATH", "Project root (default: current directory)") { |path| options[:root] = path }
43
- cli.on("--source-root PATH", "Analyze this directory; repeat for multiple roots") { |path| options[:source_roots] << path }
61
+ cli.on("--root PATH", "Project root (default: current directory)") do |path|
62
+ # @type var path: String
63
+ options[:root] = path
64
+ end
65
+ cli.on("--source-root PATH", "Analyze this directory; repeat for multiple roots") do |path|
66
+ # @type var path: String
67
+ options[:source_roots] << path
68
+ end
44
69
  cli.on("--changed", "Analyze Git additions and edits, including untracked files") { options[:changed] = true }
45
- cli.on("--coverage PATH", "SimpleCov resultset (default: coverage/.resultset.json)") { |path| options[:coverage] = path }
70
+ cli.on("--coverage PATH", "SimpleCov resultset (default: coverage/.resultset.json)") do |path|
71
+ # @type var path: String
72
+ options[:coverage] = path
73
+ end
46
74
  cli.on("--no-coverage", "Complexity only; coverage and CRAP are N/A") { options[:no_coverage] = true }
47
75
  cli.on("--use-existing-coverage", "Read reports without running tests (the default)") { options[:existing] = true }
48
- cli.on("--coverage-command COMMAND", "Run a shell command that refreshes the resultset") { |command| options[:command] = command }
76
+ cli.on("--coverage-command COMMAND", "Run a shell command that refreshes the resultset") do |command|
77
+ # @type var command: String
78
+ options[:command] = command
79
+ end
49
80
  cli.on("--threshold NUMBER", "Exit 2 when CRAP exceeds this value") do |value|
81
+ # @type var value: String
50
82
  number = Float(value)
51
83
  raise OptionParser::InvalidArgument, "threshold must be finite and nonnegative" unless number.finite? && number >= 0
52
84
 
@@ -60,12 +92,22 @@ module Crap4Ruby
60
92
  [options, parser, parser.parse(arguments)]
61
93
  end
62
94
 
95
+ # Rejects a coverage command when coverage is disabled or explicitly read-only.
96
+ #
97
+ # @param options [Hash] parsed CLI options
98
+ # @return [void]
99
+ # @raise [Error] if coverage options conflict
63
100
  def validate_options(options)
64
101
  if options[:command] && (options[:no_coverage] || options[:existing])
65
102
  raise Error, "--coverage-command cannot be combined with --no-coverage or --use-existing-coverage"
66
103
  end
67
104
  end
68
105
 
106
+ # Compares unrounded, known CRAP scores with a strict upper limit.
107
+ #
108
+ # @param entries [Array<Entry>] scored methods
109
+ # @param threshold [Float, nil] maximum allowed score, or nil to disable the check
110
+ # @return [Integer] 2 after reporting a violation to stderr, otherwise 0
69
111
  def threshold_status(entries, threshold)
70
112
  if threshold && entries.any? { |entry| entry.crap && entry.crap > threshold }
71
113
  @stderr.puts("CRAP threshold exceeded: #{threshold}")
@@ -81,6 +123,15 @@ module Crap4Ruby
81
123
  Coverage.load(report, root: root)
82
124
  end
83
125
 
126
+ # Runs a shell command at root and forwards its combined output to stderr.
127
+ # Requires success and a changed report stamp before coverage can be consumed.
128
+ #
129
+ # @param command [String] command passed to `sh -c` with the caller's environment
130
+ # @param root [String] command working directory
131
+ # @param report [String] expected coverage report path
132
+ # @return [void]
133
+ # @raise [Error] if the command is empty, fails, or leaves the report stamp unchanged
134
+ # @raise [SystemCallError] if the command cannot start or report metadata cannot be read
84
135
  def run_coverage(command, root, report)
85
136
  raise Error, "--coverage-command must not be empty" if command.strip.empty?
86
137
 
@@ -94,6 +145,11 @@ module Crap4Ruby
94
145
  raise Error, "coverage command did not refresh #{report}" if report_stamp(report) == before
95
146
  end
96
147
 
148
+ # Captures metadata used to detect a newly generated or refreshed report.
149
+ #
150
+ # @param path [String] coverage report path
151
+ # @return [Array(Integer, Time, Time, Integer), nil] inode, modification time,
152
+ # change time, and size; nil if the path does not exist
97
153
  def report_stamp(path)
98
154
  stat = File.stat(path)
99
155
  [stat.ino, stat.mtime, stat.ctime, stat.size]
@@ -3,7 +3,14 @@
3
3
  require "json"
4
4
 
5
5
  module Crap4Ruby
6
+ # Merges SimpleCov line coverage across named test suites.
6
7
  class Coverage
8
+ # Reads a SimpleCov resultset, accepting legacy arrays and line-coverage objects.
9
+ #
10
+ # @param path [String] JSON resultset path, relative to the working directory or absolute
11
+ # @param root [String] base directory for source paths inside the report
12
+ # @return [Coverage] merged coverage
13
+ # @raise [Error] if the report cannot be read, parsed, or validated
7
14
  def self.load(path, root:)
8
15
  new(JSON.parse(File.read(path)), root: root)
9
16
  rescue SystemCallError => error
@@ -12,6 +19,11 @@ module Crap4Ruby
12
19
  raise Error, "#{path}: invalid SimpleCov report (#{error.message})"
13
20
  end
14
21
 
22
+ # Validates and merges suite reports by the maximum hit count for each line.
23
+ #
24
+ # @param suites [Hash{String => Hash}] parsed resultset keyed by suite name
25
+ # @param root [String] base directory for relative source paths
26
+ # @raise [Error] if suites or line coverage are malformed
15
27
  def initialize(suites, root:)
16
28
  raise Error, "expected named test suites" unless suites.is_a?(Hash) && !suites.empty?
17
29
 
@@ -19,9 +31,16 @@ module Crap4Ruby
19
31
  suites.each_value { |suite| add_suite(suite, root) }
20
32
  end
21
33
 
34
+ # Measures executable body lines with positive hit counts.
35
+ # Null and out-of-range lines do not enter the denominator.
36
+ #
37
+ # @param method [MethodInfo] method whose path and body lines select coverage
38
+ # @return [Float, nil] fraction from 0 to 1; zero for an absent file,
39
+ # or nil when a reported file has no measurable body lines
22
40
  def fraction(method)
23
41
  path = File.expand_path(method.path)
24
42
  path = File.realpath(path) if File.exist?(path)
43
+ # @type var lines: Array[Integer?]?
25
44
  lines = @files[path]
26
45
  return 0.0 unless lines
27
46
 
@@ -40,6 +59,13 @@ module Crap4Ruby
40
59
  suite["coverage"].each { |path, data| add_file(path, data, root) }
41
60
  end
42
61
 
62
+ # Normalizes a source path and merges line counts across suites.
63
+ #
64
+ # @param path [String] source path from a suite report
65
+ # @param data [Array<Integer, nil>, Hash] legacy counts or an object with a `lines` array
66
+ # @param root [String] base directory for relative source paths
67
+ # @return [void]
68
+ # @raise [Error] if line counts are invalid
43
69
  def add_file(path, data, root)
44
70
  key = File.expand_path(path, root)
45
71
  key = File.realpath(key) if File.exist?(key)
@@ -47,6 +73,8 @@ module Crap4Ruby
47
73
  unless lines.is_a?(Array) && lines.all? { |hit| hit.nil? || (hit.is_a?(Integer) && hit >= 0) }
48
74
  raise Error, "#{path}: expected nonnegative line counts or null"
49
75
  end
76
+ # Every element was validated above; Steep cannot narrow an array through all?.
77
+ lines = lines #: Array[Integer?]
50
78
  previous = @files[key] || []
51
79
  @files[key] = Array.new([previous.size, lines.size].max) do |index|
52
80
  [previous[index], lines[index]].compact.max
@@ -4,9 +4,22 @@ require "find"
4
4
  require "open3"
5
5
 
6
6
  module Crap4Ruby
7
+ # Selects Ruby source files from explicit paths, directories, and Git changes.
7
8
  module Discovery
9
+ # Directory names excluded during recursive traversal.
8
10
  EXCLUDED = %w[test tests spec features vendor node_modules target coverage tmp log pkg build dist].freeze
9
11
 
12
+ # Combines source roots and existing paths, then applies filters and Git selection.
13
+ # Explicit files bypass traversal exclusions but must have the `.rb` extension.
14
+ # Defaults to the project root when no source roots or existing paths are given.
15
+ #
16
+ # @param root [String] absolute project directory
17
+ # @param source_roots [Array<String>] source directories, absolute or relative to root
18
+ # @param paths [Array<String>] existing paths or substring filters relative to root
19
+ # @param changed [Boolean] restrict results to staged, unstaged, and untracked files
20
+ # @return [Array<String>] sorted, unique real paths; substring filters use OR
21
+ # @raise [Error] if a source root is not a directory or Git fails
22
+ # @raise [SystemCallError] if paths cannot be read or resolved
10
23
  def self.call(root:, source_roots:, paths:, changed:)
11
24
  existing, filters = paths.partition { |path| File.exist?(File.expand_path(path, root)) }
12
25
  roots = source_roots.map do |path|
@@ -24,8 +37,14 @@ module Crap4Ruby
24
37
  files
25
38
  end
26
39
 
40
+ # Walks a directory without following symlinks or entering excluded directories.
41
+ # Hidden files and test/spec filenames are excluded; extensions are not filtered.
42
+ #
43
+ # @param root [String] directory to traverse, including an explicitly selected hidden directory
44
+ # @return [Array<String>] candidate file paths in traversal order
45
+ # @raise [SystemCallError] if traversal fails
27
46
  def self.walk(root)
28
- paths = []
47
+ paths = [] #: Array[String]
29
48
  Find.find(root) do |path|
30
49
  name = File.basename(path)
31
50
  if File.directory?(path)
@@ -37,6 +56,13 @@ module Crap4Ruby
37
56
  paths
38
57
  end
39
58
 
59
+ # Runs Git in the project directory without a shell.
60
+ #
61
+ # @param root [String] working directory for Git
62
+ # @param arguments [Array<String>] command and arguments
63
+ # @return [String] standard output, preserving NUL separators
64
+ # @raise [Error] if Git exits unsuccessfully
65
+ # @raise [SystemCallError] if Git cannot be started
40
66
  def self.git(root, *arguments)
41
67
  output, error, status = Open3.capture3("git", "-C", root, *arguments)
42
68
  raise Error, "git: #{error.strip}" unless status.success?
@@ -44,11 +70,18 @@ module Crap4Ruby
44
70
  output
45
71
  end
46
72
 
73
+ # Finds existing staged, unstaged, and untracked files under the project directory.
74
+ # Deleted files are omitted and renames are treated as deletion and addition.
75
+ #
76
+ # @param root [String] directory inside a Git working tree
77
+ # @return [Array<String>] real paths, without extension or traversal filtering
78
+ # @raise [Error] if Git cannot inspect the working tree
79
+ # @raise [SystemCallError] if Git cannot start or a path cannot be resolved
47
80
  def self.changed_files(root)
48
81
  top = git(root, "rev-parse", "--show-toplevel").chomp
49
82
  git(root, "status", "--porcelain=v1", "-z", "--no-renames", "--untracked-files=all", "--", ".")
50
83
  .split("\0").filter_map do |entry|
51
- path = File.join(top, entry[3..])
84
+ path = File.join(top, entry[3..].to_s)
52
85
  File.realpath(path) if File.file?(path)
53
86
  end
54
87
  end
@@ -4,11 +4,21 @@ require "fileutils"
4
4
  require "tempfile"
5
5
 
6
6
  module Crap4Ruby
7
+ # Formats analysis results and atomically replaces the project snapshot.
7
8
  module Report
9
+ # Ranks known scores first, from highest to lowest.
10
+ # Ties use namespace, method name, then source path in ascending order.
11
+ #
12
+ # @param entries [Array<Entry>] unsorted results
13
+ # @return [Array<Entry>] new sorted array, without modifying the input
8
14
  def self.sorted(entries)
9
15
  entries.sort_by { |entry| [entry.crap ? 0 : 1, -(entry.crap || 0), entry.namespace, entry.name, entry.path] }
10
16
  end
11
17
 
18
+ # Formats a terminal table, preserving entry order and displaying unknown values as N/A.
19
+ #
20
+ # @param entries [Array<Entry>] results in display order
21
+ # @return [String] table with one decimal place for percentages and scores, ending with a newline
12
22
  def self.table(entries)
13
23
  rows = [["Function", "Namespace", "CC", "Cov%", "CRAP"]] + entries.map do |entry|
14
24
  [entry.name, entry.namespace, entry.complexity.to_s,
@@ -23,6 +33,11 @@ module Crap4Ruby
23
33
  (["CRAP Report", "==========="] + lines).join("\n") + "\n"
24
34
  end
25
35
 
36
+ # Serializes the crapper-compatible snapshot, preserving entry order.
37
+ # Source paths are omitted; unknown numeric values become EDN nil.
38
+ #
39
+ # @param entries [Array<Entry>] results in snapshot order
40
+ # @return [String] EDN map containing entries, ending with a newline
26
41
  def self.edn(entries)
27
42
  rows = entries.map do |entry|
28
43
  " {:name #{JSON.generate(entry.name)}, :namespace #{JSON.generate(entry.namespace)}, " \
@@ -31,6 +46,10 @@ module Crap4Ruby
31
46
  rows.empty? ? "{:entries []}\n" : "{:entries [\n#{rows.join("\n")}\n]}\n"
32
47
  end
33
48
 
49
+ # Formats an EDN number with at most four decimal places and at least one.
50
+ #
51
+ # @param value [Float, nil] percentage or CRAP score
52
+ # @return [String] decimal representation, or `nil` for an unknown value
34
53
  def self.number(value)
35
54
  return "nil" if value.nil?
36
55
 
@@ -38,6 +57,13 @@ module Crap4Ruby
38
57
  text.end_with?(".") ? "#{text}0" : text
39
58
  end
40
59
 
60
+ # Atomically replaces `.metrics/crap.edn`, creating the directory if necessary.
61
+ # An empty selection replaces the snapshot with an empty entries vector.
62
+ #
63
+ # @param entries [Array<Entry>] results in snapshot order
64
+ # @param root [String] project directory that receives the snapshot
65
+ # @return [String] snapshot path
66
+ # @raise [SystemCallError, IOError] if the snapshot cannot be written or replaced
41
67
  def self.write(entries, root:)
42
68
  directory = File.join(root, ".metrics")
43
69
  FileUtils.mkdir_p(directory)
@@ -1,10 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Crap4Ruby
4
+ # Combines static complexity and measured coverage into CRAP scores.
4
5
  module Score
6
+ # Calculates `complexity**2 * (1 - fraction)**3 + complexity`.
7
+ #
8
+ # @param method [MethodInfo] static method analysis
9
+ # @param coverage [Coverage, nil] coverage reader, or nil to disable scoring
10
+ # @return [Entry] result with percentage coverage; coverage and CRAP are nil
11
+ # when coverage is disabled or the method has no measurable lines
5
12
  def self.call(method, coverage)
6
13
  fraction = coverage&.fraction(method)
7
- crap = method.complexity**2 * (1 - fraction)**3 + method.complexity if fraction
14
+ crap = method.complexity * method.complexity * (1 - fraction)**3 + method.complexity if fraction
8
15
  Entry.new(method.name, method.namespace, method.complexity, fraction && fraction * 100, crap, method.path)
9
16
  end
10
17
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Crap4Ruby
4
- VERSION = "0.1.0"
4
+ # Gem version used by the CLI and release process.
5
+ VERSION = "0.1.1"
5
6
  end
data/lib/crap4ruby.rb CHANGED
@@ -3,10 +3,48 @@
3
3
  require "prism"
4
4
  require_relative "crap4ruby/version"
5
5
 
6
+ # Static method analysis, coverage, and CRAP reporting for the CLI.
7
+ # These Ruby interfaces are maintainer APIs, outside the compatibility policy.
8
+ #
9
+ # @api private
6
10
  module Crap4Ruby
7
- Error = Class.new(StandardError)
11
+ # Invalid source, coverage, command options, or external command results.
12
+ class Error < StandardError
13
+ end
14
+
15
+ # Immutable static analysis of one explicit method definition.
8
16
  MethodInfo = Data.define(:name, :namespace, :complexity, :lines, :path)
17
+
18
+ # Immutable analysis result with coverage and a CRAP score.
9
19
  Entry = Data.define(:name, :namespace, :complexity, :coverage, :crap, :path)
20
+
21
+ # @!parse
22
+ # class Crap4Ruby::MethodInfo
23
+ # # @return [String] method name, prefixed with `self.` for singleton methods
24
+ # attr_reader :name
25
+ # # @return [String] lexical owner, or `Object` for a top-level method
26
+ # attr_reader :namespace
27
+ # # @return [Integer] cyclomatic complexity, including parameter defaults
28
+ # attr_reader :complexity
29
+ # # @return [Array<Integer>] one-based body lines, excluding nested definitions
30
+ # attr_reader :lines
31
+ # # @return [String] source path supplied to the analyzer
32
+ # attr_reader :path
33
+ # end
34
+ # class Crap4Ruby::Entry
35
+ # # @return [String] method name, prefixed with `self.` for singleton methods
36
+ # attr_reader :name
37
+ # # @return [String] lexical owner, or `Object` for a top-level method
38
+ # attr_reader :namespace
39
+ # # @return [Integer] cyclomatic complexity
40
+ # attr_reader :complexity
41
+ # # @return [Float, nil] percentage from 0 to 100, or nil when unmeasurable or disabled
42
+ # attr_reader :coverage
43
+ # # @return [Float, nil] CRAP score, or nil when coverage is unknown
44
+ # attr_reader :crap
45
+ # # @return [String] source path supplied to the analyzer
46
+ # attr_reader :path
47
+ # end
10
48
  end
11
49
 
12
50
  require_relative "crap4ruby/analyzer"
data/sig/crap4ruby.rbs ADDED
@@ -0,0 +1,96 @@
1
+ module Crap4Ruby
2
+ VERSION: String
3
+
4
+ class Error < StandardError
5
+ end
6
+
7
+ class MethodInfo < Data
8
+ attr_reader name: String
9
+ attr_reader namespace: String
10
+ attr_reader complexity: Integer
11
+ attr_reader lines: Array[Integer]
12
+ attr_reader path: String
13
+ def self.new: (String name, String namespace, Integer complexity, Array[Integer] lines, String path) -> instance
14
+ | (name: String, namespace: String, complexity: Integer, lines: Array[Integer], path: String) -> instance
15
+ alias self.[] self.new
16
+ def with: (?name: String, ?namespace: String, ?complexity: Integer, ?lines: Array[Integer], ?path: String) -> instance
17
+ end
18
+
19
+ class Entry < Data
20
+ attr_reader name: String
21
+ attr_reader namespace: String
22
+ attr_reader complexity: Integer
23
+ attr_reader coverage: Float?
24
+ attr_reader crap: Float?
25
+ attr_reader path: String
26
+ def self.new: (String name, String namespace, Integer complexity, Float? coverage, Float? crap, String path) -> instance
27
+ | (name: String, namespace: String, complexity: Integer, coverage: Float?, crap: Float?, path: String) -> instance
28
+ alias self.[] self.new
29
+ def with: (?name: String, ?namespace: String, ?complexity: Integer, ?coverage: Float?, ?crap: Float?, ?path: String) -> instance
30
+ end
31
+
32
+ class Analyzer
33
+ def call: (String source, path: String) -> Array[MethodInfo]
34
+ private
35
+ def discover: (Prism::Node? node, Array[String] scope, bool singleton, String path) -> Array[MethodInfo]
36
+ def discover_method: (Prism::DefNode node, Array[String] scope, bool singleton, String path) -> Array[MethodInfo]
37
+ def qualify: (Prism::Node? node, Array[String] scope, ?declaration: bool) -> Array[String]?
38
+ def decisions: (Prism::Node? node) -> Integer
39
+ def boundary?: (Prism::Node node) -> bool
40
+ def body_lines: (Prism::Node? node) -> Array[Integer]
41
+ def collect_boundaries: (Prism::Node node, Array[Integer] excluded) -> void
42
+ end
43
+
44
+ type json = nil | bool | Integer | Float | String | Array[json] | Hash[String, json]
45
+
46
+ class Coverage
47
+ @files: Hash[String, Array[Integer?]]
48
+ def self.load: (String path, root: String) -> Coverage
49
+ def initialize: (json suites, root: String) -> void
50
+ def fraction: (MethodInfo method) -> Float?
51
+ private
52
+ def add_suite: (json suite, String root) -> void
53
+ def add_file: (String path, json data, String root) -> void
54
+ end
55
+
56
+ module Discovery
57
+ EXCLUDED: Array[String]
58
+ def self.call: (root: String, source_roots: Array[String], paths: Array[String], changed: bool) -> Array[String]
59
+ def self.walk: (String root) -> Array[String]
60
+ def self.git: (String root, *String arguments) -> String
61
+ def self.changed_files: (String root) -> Array[String]
62
+ end
63
+
64
+ module Score
65
+ def self.call: (MethodInfo method, Coverage? coverage) -> Entry
66
+ end
67
+
68
+ module Report
69
+ def self.sorted: (Array[Entry] entries) -> Array[Entry]
70
+ def self.table: (Array[Entry] entries) -> String
71
+ def self.edn: (Array[Entry] entries) -> String
72
+ def self.number: (Float? value) -> String
73
+ def self.write: (Array[Entry] entries, root: String) -> String
74
+ end
75
+
76
+ interface _Output
77
+ def puts: (*String | OptionParser objects) -> nil
78
+ def write: (String text) -> Integer
79
+ end
80
+
81
+ class CLI
82
+ type options = { root: String, source_roots: Array[String], changed: bool, coverage: String,
83
+ ?help: bool, ?version: bool, ?no_coverage: bool, ?existing: bool, ?command: String, ?threshold: Float }
84
+ @stdout: _Output
85
+ @stderr: _Output
86
+ def initialize: (?stdout: _Output, ?stderr: _Output) -> void
87
+ def run: (Array[String] arguments) -> Integer
88
+ private
89
+ def parse_options: (Array[String] arguments) -> [options, OptionParser, Array[String]]
90
+ def validate_options: (options options) -> void
91
+ def threshold_status: (Array[Entry] entries, Float? threshold) -> Integer
92
+ def load_coverage: (options options, String root) -> Coverage
93
+ def run_coverage: (String command, String root, String report) -> void
94
+ def report_stamp: (String path) -> [Integer, Time, Time, Integer]?
95
+ end
96
+ end
data/sig/manifest.yaml ADDED
@@ -0,0 +1,8 @@
1
+ dependencies:
2
+ - name: prism
3
+ - name: json
4
+ - name: optparse
5
+ - name: find
6
+ - name: open3
7
+ - name: fileutils
8
+ - name: tempfile
data/sig/stdlib.rbs ADDED
@@ -0,0 +1,10 @@
1
+ # Complete the stdlib signatures for the subprocess options used here.
2
+ module Open3
3
+ def self.popen2e: [T] (*String command, chdir: String) { (IO input, IO output, Process::Waiter process) -> T } -> T
4
+ def self.capture3: (*String command, chdir: String) -> [String, String, Process::Status]
5
+ | ...
6
+ end
7
+
8
+ class Process::Waiter
9
+ def value: () -> Process::Status
10
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: crap4ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leonid Svyatov
@@ -37,9 +37,12 @@ extensions: []
37
37
  extra_rdoc_files: []
38
38
  files:
39
39
  - CHANGELOG.md
40
+ - CODE_OF_CONDUCT.md
40
41
  - CONTRIBUTING.md
41
42
  - LICENSE
42
43
  - README.md
44
+ - SECURITY.md
45
+ - VERSIONING.md
43
46
  - exe/crap4ruby
44
47
  - lib/crap4ruby.rb
45
48
  - lib/crap4ruby/analyzer.rb
@@ -49,10 +52,14 @@ files:
49
52
  - lib/crap4ruby/report.rb
50
53
  - lib/crap4ruby/score.rb
51
54
  - lib/crap4ruby/version.rb
55
+ - sig/crap4ruby.rbs
56
+ - sig/manifest.yaml
57
+ - sig/stdlib.rbs
52
58
  homepage: https://github.com/svyatov/crap4ruby
53
59
  licenses:
54
60
  - MIT
55
61
  metadata:
62
+ documentation_uri: https://www.rubydoc.info/gems/crap4ruby
56
63
  source_code_uri: https://github.com/svyatov/crap4ruby
57
64
  bug_tracker_uri: https://github.com/svyatov/crap4ruby/issues
58
65
  changelog_uri: https://github.com/svyatov/crap4ruby/blob/main/CHANGELOG.md