rubocop-metz 0.5.3

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.
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+ require_relative "../../../metz/cop_metadata"
5
+
6
+ module RuboCop
7
+ module Cop
8
+ module Metz
9
+ # Flags methods whose parameter list exceeds the configured `Max`.
10
+ # Counts positional, optional, rest, keyword, and keyword-rest
11
+ # parameters using the same rule as core's `Metrics/ParameterLists`,
12
+ # with a stricter Metz default of 4.
13
+ class MethodsTooManyParameters < RuboCop::Cop::Base
14
+ extend RuboCop::ExcludeLimit
15
+ extend ::Metz::CopMetadata
16
+
17
+ why_it_matters "Long parameter lists hide coupling and make callers responsible for too many decisions."
18
+ fix_safety :manual
19
+ suggested_next_moves [
20
+ "Group related parameters into a parameter object or value object.",
21
+ "Replace flag arguments with separate, well-named methods.",
22
+ "Inject collaborators through the constructor so action methods stay narrow."
23
+ ]
24
+
25
+ MSG = "Avoid parameter lists longer than %<max>d parameters. [%<count>d/%<max>d]"
26
+
27
+ exclude_limit "Max"
28
+
29
+ def on_args(node)
30
+ return unless method_definition_args?(node)
31
+
32
+ register_offense(node, args_count(node))
33
+ end
34
+
35
+ private
36
+
37
+ def register_offense(node, count)
38
+ return unless count > max_params
39
+
40
+ add_offense(node, message: format(MSG, max: max_params, count: count)) do
41
+ self.max = count
42
+ end
43
+ end
44
+
45
+ def method_definition_args?(node)
46
+ parent = node.parent
47
+ return false unless parent
48
+
49
+ parent.def_type? || parent.defs_type?
50
+ end
51
+
52
+ def args_count(node)
53
+ node.children.count { |arg| !arg.blockarg_type? }
54
+ end
55
+
56
+ def max_params
57
+ cop_config["Max"]
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Metz
6
+ # Gives a cop identical `send` and `csend` dispatch when it defines
7
+ # `on_send`, preserving the project-wide safe-navigation invariant.
8
+ module OnSendCsendBridge
9
+ def self.included(base)
10
+ base.extend(ClassMethods)
11
+ base.alias_method(:on_csend, :on_send) if base.method_defined?(:on_send)
12
+ end
13
+
14
+ module ClassMethods
15
+ def method_added(name)
16
+ super
17
+ alias_method(:on_csend, :on_send) if name == :on_send
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "demeter_train_wreck"
4
+ require_relative "../../../metz/file_classifier"
5
+
6
+ module RuboCop
7
+ module Cop
8
+ module Metz
9
+ # Flags Rails view templates whose ERB / HAML / SLIM expressions
10
+ # traverse the object graph deeper than `MaxChainLength`. Reuses
11
+ # `Metz/DemeterTrainWreck`'s chain walker and value-object skip logic,
12
+ # so a chain like `name.upcase.strip.split(' ').first` (every link a
13
+ # recognised value-object call) stays silent. Path-classified through
14
+ # `Metz::FileClassifier.view?`: a deep chain inside an `.erb` file
15
+ # outside `app/views/` will not fire.
16
+ # DDR: docs/ddrs/2026-06-24-views-deep-navigation-inherits-demeter.md
17
+ # explains why this specialization uses inheritance instead of composition.
18
+ class ViewsDeepNavigation < DemeterTrainWreck
19
+ include OnSendCsendBridge
20
+
21
+ MSG = "View object-graph traversal of %<count>d exceeds MaxChainLength (%<max>d). " \
22
+ "Push the lookup into the controller or expose it via a presenter."
23
+
24
+ why_it_matters "Deep object-graph chains in views couple templates to the internal " \
25
+ "structure of every collaborator they touch, making refactors and " \
26
+ "test setup painful."
27
+ fix_safety :manual
28
+ suggested_next_moves [
29
+ "Compute the value in the controller and expose it via an instance variable.",
30
+ "Wrap the chain in a presenter or view model that exposes one explicit method.",
31
+ "Use Rails `delegate :foo, to: :collaborator` to flatten the chain at the model boundary."
32
+ ]
33
+
34
+ # RuboCop's dispatcher needs the subclass to own the callback, while the
35
+ # inherited implementation remains the shared chain analyzer.
36
+ # rubocop:disable Lint/UselessMethodDefinition -- required for subclass callback registration.
37
+ def on_send(node)
38
+ super
39
+ end
40
+ # rubocop:enable Lint/UselessMethodDefinition
41
+
42
+ def relevant_file?(file)
43
+ return super if file.nil? || file.empty? || file == "(string)"
44
+
45
+ !file_name_matches_any?(file, "Exclude", false) &&
46
+ ::Metz::FileClassifier.view?(file)
47
+ end
48
+
49
+ private
50
+
51
+ def max
52
+ cop_config["MaxChainLength"] || 3
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Central require point for every Metz cop. Keep one
4
+ # `require_relative "metz/<cop_name>"` line per cop so registry wiring stays in
5
+ # a single discoverable location.
6
+
7
+ require_relative "metz/classes_too_long"
8
+ require_relative "metz/methods_too_long"
9
+ require_relative "metz/methods_too_many_parameters"
10
+ require_relative "metz/demeter_train_wreck"
11
+ require_relative "metz/controllers_too_many_direct_collaborators"
12
+ require_relative "metz/views_deep_navigation"
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+
5
+ module RuboCop
6
+ module Formatter
7
+ # Wraps RuboCop's standard JSON formatter and enriches every offense whose
8
+ # `cop_name` starts with `Metz/` with the metadata DSL fields published by
9
+ # `Metz::CopMetadata`. Non-Metz offenses are passed through unchanged so the
10
+ # top-level JSON shape and any consumer expecting standard RuboCop output
11
+ # keeps working.
12
+ class MetzJsonFormatter < JSONFormatter
13
+ METZ_PREFIX = "Metz/"
14
+
15
+ EMPTY_METADATA = {
16
+ why_it_matters: nil,
17
+ fix_safety: nil,
18
+ suggested_next_moves: []
19
+ }.freeze
20
+
21
+ def hash_for_offense(offense)
22
+ hash = super
23
+ return hash unless offense.cop_name.to_s.start_with?(METZ_PREFIX)
24
+
25
+ hash.merge!(metz_metadata_for(offense.cop_name))
26
+ end
27
+
28
+ private
29
+
30
+ def metz_metadata_for(cop_name)
31
+ cop_class = RuboCop::Cop::Registry.global.find_by_cop_name(cop_name)
32
+ return EMPTY_METADATA.dup unless cop_class.respond_to?(:metz_metadata)
33
+
34
+ format_metadata(cop_class.metz_metadata)
35
+ end
36
+
37
+ def format_metadata(meta)
38
+ {
39
+ why_it_matters: stringify_or_nil(meta[:why_it_matters]),
40
+ fix_safety: stringify_or_nil(meta[:fix_safety]),
41
+ suggested_next_moves: Array(meta[:suggested_next_moves]).map(&:to_s)
42
+ }
43
+ end
44
+
45
+ def stringify_or_nil(value)
46
+ value&.to_s
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lint_roller"
4
+ require "pathname"
5
+
6
+ require_relative "version"
7
+
8
+ module RuboCop
9
+ module Metz
10
+ class Plugin < LintRoller::Plugin
11
+ ABOUT = LintRoller::About.new(
12
+ name: "rubocop-metz",
13
+ version: RuboCop::Metz::VERSION,
14
+ homepage: "https://github.com/users/fuentesjr/packages/rubygems/package/rubocop-metz",
15
+ description: "Sandi-Metz-inspired RuboCop cops."
16
+ ).freeze
17
+ private_constant :ABOUT
18
+
19
+ def about
20
+ ABOUT
21
+ end
22
+
23
+ def supported?(context)
24
+ context.engine == :rubocop
25
+ end
26
+
27
+ def rules(_context)
28
+ LintRoller::Rules.new(
29
+ type: :path,
30
+ config_format: :rubocop,
31
+ value: Pathname.new(__dir__).join("../../../config/default.yml").expand_path
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Metz
5
+ VERSION = "0.5.3"
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+ require "rubocop-ast"
5
+
6
+ require_relative "rubocop/metz/version"
7
+ require_relative "rubocop/metz/plugin"
8
+ require_relative "metz/cop_metadata"
9
+ require_relative "metz/file_classifier"
10
+ require_relative "metz/erb_ruby_extractor"
11
+ require_relative "metz/haml_ruby_extractor"
12
+ require_relative "metz/slim_ruby_extractor"
13
+ require_relative "rubocop/cop/metz/on_send_csend_bridge"
14
+ require_relative "rubocop/cop/metz_cops"
15
+ require_relative "rubocop/formatter/metz_json_formatter"
16
+
17
+ Metz::ErbRubyExtractor.install!
18
+ Metz::HamlRubyExtractor.install!
19
+ Metz::SlimRubyExtractor.install!
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/rubocop/metz/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "rubocop-metz"
7
+ spec.version = RuboCop::Metz::VERSION
8
+ spec.authors = ["Salvador Fuentes Jr"]
9
+ spec.email = ["fuentesjr@duck.com"]
10
+
11
+ spec.summary = "Sandi-Metz-inspired RuboCop cops."
12
+ spec.description = "A RuboCop plugin that ships custom cops capturing " \
13
+ "Sandi-Metz-style design heuristics for Ruby and Rails code."
14
+ spec.homepage = "https://github.com/fuentesjr/metz-scan"
15
+ spec.license = "MIT"
16
+
17
+ spec.required_ruby_version = ">= 3.3"
18
+
19
+ spec.metadata["source_code_uri"] = spec.homepage
20
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/releases"
21
+ spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
22
+ spec.metadata["github_repo"] = "ssh://github.com/fuentesjr/metz-scan"
23
+ spec.metadata["github_package_uri"] = "https://github.com/users/fuentesjr/packages/rubygems/package/rubocop-metz"
24
+ spec.metadata["default_lint_roller_plugin"] = "RuboCop::Metz::Plugin"
25
+
26
+ spec.files = Dir.glob("{lib,config}/**/*", File::FNM_DOTMATCH).reject { |f| File.directory?(f) } +
27
+ ["LICENSE", "rubocop-metz.gemspec"].select { |f| File.exist?(f) }
28
+ # This gemspec's Dir.glob is relative to the build CWD, so building from the
29
+ # repo root (`gem build rubocop-metz/rubocop-metz.gemspec`) would silently
30
+ # package the wrapper's lib/metz_scan files instead of these cops. Fail loudly
31
+ # instead of shipping a corrupt gem. Build from this directory:
32
+ # cd rubocop-metz && gem build rubocop-metz.gemspec
33
+ unless spec.files.include?("lib/rubocop-metz.rb")
34
+ raise "rubocop-metz.gemspec: lib/rubocop-metz.rb missing from packaged files; " \
35
+ "build from rubocop-metz/ (cd rubocop-metz && gem build rubocop-metz.gemspec)."
36
+ end
37
+ spec.require_paths = ["lib"]
38
+
39
+ spec.add_dependency "lint_roller", "~> 1.1"
40
+ spec.add_dependency "rubocop", "~> 1.80"
41
+ spec.add_dependency "rubocop-ast", "~> 1.49"
42
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubocop-metz
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.3
5
+ platform: ruby
6
+ authors:
7
+ - Salvador Fuentes Jr
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: lint_roller
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rubocop
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.80'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.80'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rubocop-ast
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.49'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.49'
54
+ description: A RuboCop plugin that ships custom cops capturing Sandi-Metz-style design
55
+ heuristics for Ruby and Rails code.
56
+ email:
57
+ - fuentesjr@duck.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - LICENSE
63
+ - config/default.yml
64
+ - lib/metz/cop_metadata.rb
65
+ - lib/metz/erb_ruby_extractor.rb
66
+ - lib/metz/file_classifier.rb
67
+ - lib/metz/haml_ruby_extractor.rb
68
+ - lib/metz/slim_ruby_extractor.rb
69
+ - lib/metz/template_ruby_extractor.rb
70
+ - lib/rubocop-metz.rb
71
+ - lib/rubocop/cop/metz/classes_too_long.rb
72
+ - lib/rubocop/cop/metz/controllers_too_many_direct_collaborators.rb
73
+ - lib/rubocop/cop/metz/demeter_train_wreck.rb
74
+ - lib/rubocop/cop/metz/demeter_train_wreck/type_inference.rb
75
+ - lib/rubocop/cop/metz/methods_too_long.rb
76
+ - lib/rubocop/cop/metz/methods_too_many_parameters.rb
77
+ - lib/rubocop/cop/metz/on_send_csend_bridge.rb
78
+ - lib/rubocop/cop/metz/views_deep_navigation.rb
79
+ - lib/rubocop/cop/metz_cops.rb
80
+ - lib/rubocop/formatter/metz_json_formatter.rb
81
+ - lib/rubocop/metz/plugin.rb
82
+ - lib/rubocop/metz/version.rb
83
+ - rubocop-metz.gemspec
84
+ homepage: https://github.com/fuentesjr/metz-scan
85
+ licenses:
86
+ - MIT
87
+ metadata:
88
+ source_code_uri: https://github.com/fuentesjr/metz-scan
89
+ changelog_uri: https://github.com/fuentesjr/metz-scan/releases
90
+ bug_tracker_uri: https://github.com/fuentesjr/metz-scan/issues
91
+ github_repo: ssh://github.com/fuentesjr/metz-scan
92
+ github_package_uri: https://github.com/users/fuentesjr/packages/rubygems/package/rubocop-metz
93
+ default_lint_roller_plugin: RuboCop::Metz::Plugin
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '3.3'
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubygems_version: 4.0.8
109
+ specification_version: 4
110
+ summary: Sandi-Metz-inspired RuboCop cops.
111
+ test_files: []