metz-scan 0.5.3 → 0.6.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/lib/metz_scan/analyzers/inheritance_descendants.rb +1 -1
  3. data/lib/metz_scan/analyzers/namespace_leak_pressure.rb +1 -1
  4. data/lib/metz_scan/analyzers/package_dependency_pressure.rb +1 -1
  5. data/lib/metz_scan/analyzers/package_map.rb +1 -1
  6. data/lib/metz_scan/analyzers/test_calls_private_method/call_site_collector.rb +79 -0
  7. data/lib/metz_scan/analyzers/test_calls_private_method/finding.rb +23 -0
  8. data/lib/metz_scan/analyzers/test_calls_private_method/metadata.rb +27 -0
  9. data/lib/metz_scan/analyzers/test_calls_private_method/node_predicates.rb +57 -0
  10. data/lib/metz_scan/analyzers/test_calls_private_method/receiver_resolution.rb +80 -0
  11. data/lib/metz_scan/analyzers/test_calls_private_method/scope_facts.rb +76 -0
  12. data/lib/metz_scan/analyzers/test_calls_private_method/scope_resolution.rb +56 -0
  13. data/lib/metz_scan/analyzers/test_calls_private_method/triage.rb +23 -0
  14. data/lib/metz_scan/analyzers/test_calls_private_method.rb +125 -0
  15. data/lib/metz_scan/calibration/project_analyzer_evidence_runner/readiness_catalog.rb +7 -3
  16. data/lib/metz_scan/cli.rb +7 -13
  17. data/lib/metz_scan/commands/explain.rb +11 -2
  18. data/lib/metz_scan/commands/project_analyzers.rb +3 -2
  19. data/lib/metz_scan/commands/report.rb +6 -5
  20. data/lib/metz_scan/commands/rules.rb +16 -4
  21. data/lib/metz_scan/commands/scan/auto_fix.rb +9 -7
  22. data/lib/metz_scan/commands/scan/config_exclusions.rb +76 -0
  23. data/lib/metz_scan/commands/scan/exclude_entries.rb +59 -0
  24. data/lib/metz_scan/commands/scan/exclude_entry_locator.rb +74 -0
  25. data/lib/metz_scan/commands/scan/exclude_provenance.rb +48 -0
  26. data/lib/metz_scan/commands/scan/inline_directive_locator.rb +53 -0
  27. data/lib/metz_scan/commands/scan/project_analyzer_runner.rb +6 -3
  28. data/lib/metz_scan/commands/scan/project_config_scope.rb +26 -4
  29. data/lib/metz_scan/commands/scan/runner.rb +9 -44
  30. data/lib/metz_scan/commands/scan/suppression_ledger.rb +76 -0
  31. data/lib/metz_scan/commands/scan/suppression_ledger_formatter.rb +48 -0
  32. data/lib/metz_scan/commands/scan/text_renderer.rb +5 -4
  33. data/lib/metz_scan/commands/scan.rb +5 -5
  34. data/lib/metz_scan/project_index/rubydex_backend/method_declarations.rb +33 -12
  35. data/lib/metz_scan/project_index/rubydex_backend/method_visibility.rb +152 -0
  36. data/lib/metz_scan/project_index/rubydex_backend.rb +2 -1
  37. data/lib/metz_scan/project_index.rb +1 -1
  38. data/lib/metz_scan/version.rb +1 -1
  39. data/metz-scan.gemspec +11 -10
  40. metadata +23 -5
@@ -6,6 +6,7 @@ require "rubocop"
6
6
  require "stringio"
7
7
 
8
8
  require "metz_scan/commands/scan/project_config_scope"
9
+ require "metz_scan/commands/scan/suppression_ledger"
9
10
  require "metz_scan/commands/scan/target_ruby_version"
10
11
 
11
12
  module MetzScan
@@ -33,7 +34,7 @@ module MetzScan
33
34
  report = TargetRubyVersion.with_project_config(paths, all_cops: all_cops) do
34
35
  parse_output(capture_output(rubocop_argv(paths, all_cops: all_cops)))
35
36
  end
36
- all_cops ? report : ProjectCopScope.honor(report)
37
+ all_cops ? report : SuppressionLedger.apply(report)
37
38
  end
38
39
 
39
40
  def self.with_errors
@@ -44,7 +45,8 @@ module MetzScan
44
45
 
45
46
  def self.rubocop_argv(paths, all_cops:)
46
47
  require_metz_plugin
47
- ["--plugin", "rubocop-metz", *cop_selection_argv(all_cops), "--format", FORMATTER, *paths]
48
+ ["--plugin", "rubocop-metz", *cop_selection_argv(all_cops), *ledger_argv(all_cops), "--format", FORMATTER,
49
+ *paths]
48
50
  end
49
51
 
50
52
  def self.require_metz_plugin
@@ -59,9 +61,10 @@ module MetzScan
59
61
  TargetFileDiscovery.for_project_config(paths).map { |path| display_path(path) }
60
62
  end
61
63
 
62
- def self.cop_selection_argv(all_cops)
63
- all_cops ? [] : ["--force-default-config", "--enable-all-cops", "--only", "Metz"]
64
- end
64
+ def self.cop_selection_argv(all_cops) = all_cops ? [] : ["--force-default-config", "--only", "Metz"]
65
+
66
+ # The suppression ledger needs inline-suppressed offenses; AutoFix reuses cop_selection_argv without it.
67
+ def self.ledger_argv(all_cops) = all_cops ? [] : ["--display-suppressed"]
65
68
 
66
69
  def self.capture_output(argv)
67
70
  out = StringIO.new
@@ -120,7 +123,7 @@ module MetzScan
120
123
  end
121
124
 
122
125
  def self.empty_report
123
- { "metadata" => metadata, "files" => [],
126
+ { "metadata" => metadata, "files" => [], "suppressions" => [],
124
127
  "summary" => { "offense_count" => 0, "target_file_count" => 0, "inspected_file_count" => 0 } }
125
128
  end
126
129
 
@@ -139,44 +142,6 @@ module MetzScan
139
142
  end
140
143
  end
141
144
 
142
- # Default mode forces stock Metz *tuning* (--force-default-config) but must
143
- # still honor the project's per-cop file *scope* (Include/Exclude), the
144
- # same way #33 honors AllCops: Exclude. RuboCop's own excluded_file?
145
- # resolves the project config's scope per cop; offenses on files a cop is
146
- # scoped off are dropped here. Invalid project config -> nothing to honor
147
- # (matches the forced-default target-discovery fallback). See #37.
148
- module ProjectCopScope
149
- module_function
150
-
151
- def honor(report)
152
- store = ProjectConfigScope.store
153
- files = Array(report["files"]).map { |file| reject_scoped_off(store, file) }
154
- recount(report.merge("files" => files))
155
- rescue RuboCop::Error, Psych::Exception
156
- report
157
- end
158
-
159
- def reject_scoped_off(store, file)
160
- absolute = File.expand_path(file.fetch("path"))
161
- kept = Array(file["offenses"]).reject { |o| scoped_off?(store, o.fetch("cop_name"), absolute) }
162
- file.merge("offenses" => kept)
163
- end
164
-
165
- def scoped_off?(store, cop_name, absolute_path)
166
- cop_class = RuboCop::Cop::Registry.global.find_by_cop_name(cop_name)
167
- return false unless cop_class
168
-
169
- cop_class.new(store.for_file(absolute_path)).excluded_file?(absolute_path)
170
- end
171
-
172
- def recount(report)
173
- return report unless report["summary"]
174
-
175
- count = Array(report["files"]).sum { |file| Array(file["offenses"]).size }
176
- report.merge("summary" => report["summary"].merge("offense_count" => count))
177
- end
178
- end
179
-
180
145
  module TargetFileDiscovery
181
146
  module_function
182
147
 
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "psych"
4
+ require "rubocop"
5
+
6
+ require "metz_scan/commands/scan/config_exclusions"
7
+ require "metz_scan/commands/scan/inline_directive_locator"
8
+ require "metz_scan/commands/scan/project_config_scope"
9
+
10
+ module MetzScan
11
+ module Commands
12
+ class Scan
13
+ # Default mode asks RuboCop to --display-suppressed, then partitions every
14
+ # offense: a project per-cop Exclude hides it (credited even when an
15
+ # inline disable also covers it), else an inline directive hides it, else
16
+ # it is live. Hidden offenses leave `files` and the counts; the ones a
17
+ # project wrote a suppression for are listed under `suppressions`.
18
+ class SuppressionLedger
19
+ SORT_KEYS = %w[path line column cop_name].freeze
20
+
21
+ def self.apply(report)
22
+ new(report, ConfigExclusions.new(ProjectConfigScope.store)).apply
23
+ rescue RuboCop::Error, Psych::Exception
24
+ new(report, NoConfigExclusions).apply
25
+ end
26
+
27
+ def initialize(report, exclusions)
28
+ @report = report
29
+ @exclusions = exclusions
30
+ @directives = InlineDirectiveLocator.new
31
+ @records = []
32
+ end
33
+
34
+ def apply
35
+ files = Array(report["files"]).map { |file| file.merge("offenses" => live_offenses(file)) }
36
+ recount(report.merge("files" => files, "suppressions" => records.sort_by { |r| r.values_at(*SORT_KEYS) }))
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :report, :exclusions, :directives, :records
42
+
43
+ def live_offenses(file)
44
+ path = file.fetch("path")
45
+ sourced = Array(file["offenses"]).map { |offense| [offense, suppression_source(path, offense)] }
46
+ live, hidden = sourced.partition { |_offense, source| source.nil? }
47
+ hidden.each { |offense, source| record(path, offense, source.record_fields(path, offense)) }
48
+ live.map(&:first)
49
+ end
50
+
51
+ def suppression_source(path, offense)
52
+ return exclusions if exclusions.scoped_off?(path, offense)
53
+
54
+ directives if offense["suppressed"]
55
+ end
56
+
57
+ # Hidden offenses with no project-written suppression (stock-default
58
+ # scope, synthetic opt-in coverage) produce no record.
59
+ def record(path, offense, fields)
60
+ records << finding(path, offense).merge(fields) if fields
61
+ end
62
+
63
+ def finding(path, offense)
64
+ location = offense.fetch("location")
65
+ { "cop_name" => offense.fetch("cop_name"), "path" => path, "line" => location.fetch("start_line"),
66
+ "column" => location.fetch("start_column"), "message" => offense.fetch("message") }
67
+ end
68
+
69
+ def recount(report)
70
+ count = report["files"].sum { |file| file["offenses"].size }
71
+ report.merge("summary" => report.fetch("summary").merge("offense_count" => count))
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetzScan
4
+ module Commands
5
+ class Scan
6
+ class SuppressionLedgerFormatter
7
+ def initialize(parsed)
8
+ @records = Array(parsed["suppressions"])
9
+ end
10
+
11
+ def lines
12
+ return [] if records.empty?
13
+
14
+ [heading, *records.flat_map { |record| record_lines(record) }, ""]
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :records
20
+
21
+ def heading
22
+ missing = records.count { |record| record.fetch("reason_status") == "missing" }
23
+ "Suppressed findings: #{records.size}, #{missing} without a reason"
24
+ end
25
+
26
+ def record_lines(record)
27
+ [" #{record.fetch('path')}:#{record.fetch('line')}:#{record.fetch('column')} #{record.fetch('message')}",
28
+ " #{source(record)}, #{reason(record)}"]
29
+ end
30
+
31
+ def source(record)
32
+ return "inline disable at line #{record.dig('directive', 'line')}" unless record.fetch("config")
33
+
34
+ config = record.fetch("config")
35
+ "#{[config.fetch('path'), config.fetch('line')].compact.join(':')} Exclude \"#{config.fetch('pattern')}\""
36
+ end
37
+
38
+ def reason(record)
39
+ case record.fetch("reason_status")
40
+ when "present" then "reason: #{record.fetch('reason')}"
41
+ when "missing" then "no reason"
42
+ else "reason not checked"
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -6,6 +6,7 @@ require_relative "project_analyzer_summary_aggregate_formatter"
6
6
  require_relative "project_analyzer_summary_breakdown_formatter"
7
7
  require_relative "project_analyzer_triage_formatter"
8
8
  require_relative "project_analyzer_triage_priority"
9
+ require_relative "suppression_ledger_formatter"
9
10
 
10
11
  module MetzScan
11
12
  module Commands
@@ -22,16 +23,16 @@ module MetzScan
22
23
 
23
24
  def render
24
25
  emit_project_analyzer_summary
25
- sorted_offense_blocks.each do |cop_name, list|
26
- render_block(cop_name, list)
27
- end
28
- ComplianceScorecard.new(parsed).lines.each { |line| stdout.puts line }
26
+ sorted_offense_blocks.each { |cop_name, list| render_block(cop_name, list) }
27
+ closing_lines.each { |line| stdout.puts line }
29
28
  end
30
29
 
31
30
  private
32
31
 
33
32
  attr_reader :stdout, :parsed
34
33
 
34
+ def closing_lines = SuppressionLedgerFormatter.new(parsed).lines + ComplianceScorecard.new(parsed).lines
35
+
35
36
  def render_block(cop_name, list)
36
37
  stdout.puts heading(cop_name)
37
38
  emit_block_metadata(cop_name, representative_offense(list))
@@ -11,6 +11,7 @@ require_relative "scan/text_renderer"
11
11
  require_relative "scan/sarif_renderer"
12
12
  require_relative "scan/github_annotations_renderer"
13
13
  require_relative "scan/auto_fix"
14
+ require_relative "../cli"
14
15
 
15
16
  module MetzScan
16
17
  module Commands
@@ -128,7 +129,6 @@ module MetzScan
128
129
  def validate(options)
129
130
  return missing_path_arg if options.paths.empty?
130
131
  return missing_paths(options.paths) unless options.paths.all? { |p| File.exist?(p) }
131
- return nil if options.auto_fix
132
132
 
133
133
  invalid_format(options.format) unless VALID_FORMATS.include?(options.format)
134
134
  end
@@ -168,7 +168,7 @@ module MetzScan
168
168
  def parser_error(err)
169
169
  stderr.puts "metz-scan scan: #{err.message}"
170
170
  stderr.puts USAGE
171
- 1
171
+ CLI::USAGE_ERROR
172
172
  end
173
173
 
174
174
  def print_help
@@ -179,17 +179,17 @@ module MetzScan
179
179
  def missing_path_arg
180
180
  stderr.puts "metz-scan scan: missing PATH argument."
181
181
  stderr.puts USAGE
182
- 1
182
+ CLI::USAGE_ERROR
183
183
  end
184
184
 
185
185
  def missing_paths(paths)
186
186
  paths.reject { |p| File.exist?(p) }.each { |p| stderr.puts "metz-scan scan: no such file or directory: #{p}" }
187
- 1
187
+ CLI::USAGE_ERROR
188
188
  end
189
189
 
190
190
  def invalid_format(fmt)
191
191
  stderr.puts "metz-scan scan: invalid --format '#{fmt}'. Valid formats: text, json, sarif, gh-annotations."
192
- 1
192
+ CLI::USAGE_ERROR
193
193
  end
194
194
 
195
195
  def runner_error(err)
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "method_visibility"
4
+
3
5
  module MetzScan
4
6
  class ProjectIndex
5
7
  class RubydexBackend
6
8
  module MethodDeclarations
9
+ include MethodVisibility
10
+
7
11
  def method_declarations
8
12
  method_declaration_entries.sort_by do |declaration|
9
13
  [declaration.owner_name.to_s, declaration.method_identity.to_s, declaration.path.to_s,
@@ -15,7 +19,7 @@ module MetzScan
15
19
 
16
20
  def method_declaration_entries
17
21
  graph.declarations.select { |declaration| method_declaration?(declaration) }
18
- .filter_map { |declaration| method_declaration_for(declaration) }
22
+ .filter_map { |declaration| method_declaration_for(declaration) }
19
23
  end
20
24
 
21
25
  def method_declaration?(declaration)
@@ -31,14 +35,13 @@ module MetzScan
31
35
  owner_attributes = method_owner_attributes(declaration)
32
36
  return unless owner_attributes
33
37
 
34
- method_name = method_name_for(owner_attributes.fetch(:signature))
35
- identity_attributes(declaration, owner_attributes, method_name)
36
- .merge(location_attributes(declaration))
38
+ method_declaration_identity(declaration, owner_attributes)
37
39
  end
38
40
 
39
- def method_owner_and_signature(declaration)
40
- owner_attributes = method_owner_attributes(declaration)
41
- [owner_attributes[:owner_name], owner_attributes[:signature]] if owner_attributes
41
+ def method_declaration_identity(declaration, owner_attributes)
42
+ location = location_attributes(declaration)
43
+ method_name = method_name_for(owner_attributes.fetch(:signature))
44
+ identity_attributes(declaration, owner_attributes, method_name, location).merge(location)
42
45
  end
43
46
 
44
47
  def method_owner_attributes(declaration)
@@ -50,11 +53,28 @@ module MetzScan
50
53
  receiver_kind: receiver_kind_for(raw_owner_name) }
51
54
  end
52
55
 
53
- def identity_attributes(declaration, owner_attributes, method_name)
56
+ def identity_attributes(declaration, owner_attributes, method_name, location_attributes)
57
+ # For supported Ruby visibility forms, Rubydex::Method#visibility is this analyzer's one index assumption.
58
+ method_identity_attributes(declaration, owner_attributes, method_name, location_attributes)
59
+ end
60
+
61
+ def method_identity_attributes(declaration, owner_attributes, method_name, location_attributes)
54
62
  receiver_kind = owner_attributes.fetch(:receiver_kind)
55
- { name: declaration.name, owner_name: owner_attributes.fetch(:owner_name), method_name: method_name,
56
- signature: owner_attributes.fetch(:signature), receiver_kind: receiver_kind,
57
- method_identity: method_identity_for(receiver_kind, method_name) }
63
+ core_identity_attributes(declaration, owner_attributes, method_name, receiver_kind)
64
+ .merge(visibility: visibility_for(declaration,
65
+ visibility_attributes(owner_attributes, method_name, receiver_kind,
66
+ location_attributes)))
67
+ end
68
+
69
+ def core_identity_attributes(declaration, owner_attributes, method_name, receiver_kind)
70
+ { name: declaration.name, owner_name: owner_attributes.fetch(:owner_name),
71
+ method_name: method_name, signature: owner_attributes.fetch(:signature),
72
+ receiver_kind: receiver_kind, method_identity: method_identity_for(receiver_kind, method_name) }
73
+ end
74
+
75
+ def visibility_attributes(owner_attributes, method_name, receiver_kind, location_attributes)
76
+ { owner_name: owner_attributes.fetch(:owner_name), method_name: method_name,
77
+ receiver_kind: receiver_kind, path: location_attributes.fetch(:path) }
58
78
  end
59
79
 
60
80
  def location_attributes(declaration)
@@ -91,7 +111,8 @@ module MetzScan
91
111
 
92
112
  def singleton_owner_name(raw_owner_name)
93
113
  owner_name, singleton_tail = raw_owner_name.to_s.split("::<", 2)
94
- return unless singleton_tail&.delete_suffix(">") == owner_name
114
+ tail_name = singleton_tail&.delete_suffix(">")
115
+ return unless tail_name == owner_name || tail_name == owner_name.split("::").last
95
116
 
96
117
  owner_name
97
118
  end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+
5
+ module MetzScan
6
+ class ProjectIndex
7
+ class RubydexBackend
8
+ module MethodVisibility
9
+ def visibility_for(declaration, attributes)
10
+ return module_function_visibility(attributes) if module_function_method?(attributes)
11
+
12
+ declaration.visibility.to_s
13
+ end
14
+
15
+ def module_function_visibility(attributes)
16
+ attributes.fetch(:receiver_kind) == "singleton" ? "public" : "private"
17
+ end
18
+
19
+ def module_function_method?(attributes)
20
+ facts = module_function_facts_for(attributes.fetch(:path))
21
+ facts.fetch(module_function_owner_name(attributes.fetch(:owner_name)), [])
22
+ .include?(attributes.fetch(:method_name))
23
+ end
24
+
25
+ def module_function_owner_name(owner_name)
26
+ owner_name.to_s.split("::<", 2).first
27
+ end
28
+
29
+ def module_function_facts_for(path)
30
+ return {} unless path && File.file?(path)
31
+
32
+ @module_function_facts_by_path ||= {}
33
+ @module_function_facts_by_path[path] ||= ModuleFunctionFacts.new(path).to_h
34
+ end
35
+
36
+ class ModuleFunctionFacts
37
+ def initialize(path)
38
+ @path = path
39
+ end
40
+
41
+ def to_h
42
+ add_scope_tree_facts(ast, [], {})
43
+ rescue Parser::SyntaxError
44
+ {}
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :path
50
+
51
+ def ast
52
+ processed_source.ast
53
+ end
54
+
55
+ def processed_source
56
+ RuboCop::ProcessedSource.new(File.read(path), RUBY_VERSION.to_f)
57
+ end
58
+
59
+ def add_scope_tree_facts(node, owner_stack, facts)
60
+ return facts unless node
61
+ return add_scope_facts(node, owner_stack, facts) if scope_node?(node)
62
+
63
+ add_child_scope_facts(node, owner_stack, facts)
64
+ end
65
+
66
+ def add_child_scope_facts(node, owner_stack, facts)
67
+ node&.children&.each do |child|
68
+ add_child_scope_fact(child, owner_stack, facts) if child.is_a?(Parser::AST::Node)
69
+ end
70
+
71
+ facts
72
+ end
73
+
74
+ def add_child_scope_fact(child, owner_stack, facts)
75
+ return add_scope_facts(child, owner_stack, facts) if scope_node?(child)
76
+
77
+ add_child_scope_facts(child, owner_stack, facts)
78
+ end
79
+
80
+ def add_scope_facts(scope, owner_stack, facts)
81
+ owner_name = owner_name_for(scope, owner_stack)
82
+ nested_stack = owner_name ? [owner_name] : owner_stack
83
+ add_module_function_facts(facts, owner_name, scope) if owner_name
84
+ add_scope_tree_facts(scope.body, nested_stack, facts)
85
+ end
86
+
87
+ def add_module_function_facts(facts, owner_name, scope)
88
+ names = explicit_module_function_names(scope) + default_module_function_names(scope)
89
+ facts[owner_name] = Array(facts[owner_name]) | names unless names.empty?
90
+ end
91
+
92
+ def owner_name_for(scope, owner_stack)
93
+ name = scope.identifier.const_name if scope.identifier&.const_type?
94
+ return unless name
95
+ return name if name.include?("::") || owner_stack.empty?
96
+
97
+ "#{owner_stack.last}::#{name}"
98
+ end
99
+
100
+ def explicit_module_function_names(scope)
101
+ module_function_sends(scope).flat_map { |node| literal_names(node.arguments) }
102
+ end
103
+
104
+ def default_module_function_names(scope)
105
+ lines = bare_module_function_lines(scope)
106
+ direct_defs(scope).select { |node| lines.any? { |line| line < node.loc.keyword.line } }
107
+ .map { |node| node.method_name.to_s }
108
+ end
109
+
110
+ def bare_module_function_lines(scope)
111
+ module_function_sends(scope).select { |node| node.arguments.empty? }
112
+ .map { |node| node.loc.selector.line }
113
+ end
114
+
115
+ def module_function_sends(scope)
116
+ direct_descendants(scope, :send).select do |node|
117
+ !node.receiver && node.method_name == :module_function
118
+ end
119
+ end
120
+
121
+ def direct_defs(scope)
122
+ direct_descendants(scope, :def)
123
+ end
124
+
125
+ def literal_names(arguments)
126
+ arguments.filter_map { |argument| literal_name(argument) }
127
+ end
128
+
129
+ def literal_name(argument)
130
+ argument.value.to_s if argument&.sym_type? || argument&.str_type?
131
+ end
132
+
133
+ def direct_descendants(scope, *types)
134
+ scope.each_descendant(*types).reject { |node| nested_scope_between?(node, scope) }
135
+ end
136
+
137
+ def nested_scope_between?(node, scope)
138
+ node.each_ancestor.any? do |ancestor|
139
+ break false if ancestor.equal?(scope)
140
+
141
+ scope_node?(ancestor)
142
+ end
143
+ end
144
+
145
+ def scope_node?(node)
146
+ node&.class_type? || node&.module_type?
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -28,7 +28,8 @@ module MetzScan
28
28
  require "rubydex"
29
29
 
30
30
  files = ruby_files_for(paths)
31
- graph = Rubydex::Graph.new(workspace_path: workspace_path_for(paths))
31
+ # 0.4.0: Config owns workspace_path; Graph.new no longer takes it (Shopify/rubydex#965).
32
+ graph = Rubydex::Graph.configure_for_workspace(workspace_path_for(paths))
32
33
  index_errors = index_graph(graph, files, workspace)
33
34
 
34
35
  new(graph: graph, indexed_files: files, index_errors: index_errors)
@@ -8,7 +8,7 @@ module MetzScan
8
8
  class ProjectIndex
9
9
  Declaration = Struct.new(:name, :path, :kind, keyword_init: true)
10
10
  MethodDeclaration = Struct.new(:name, :owner_name, :method_name, :signature, :path, :line, :column,
11
- :receiver_kind, :method_identity, keyword_init: true)
11
+ :receiver_kind, :method_identity, :visibility, keyword_init: true)
12
12
  Reference = Struct.new(:name, :path, :line, :column, keyword_init: true)
13
13
 
14
14
  class UnknownBackendError < StandardError; end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MetzScan
4
- VERSION = "0.5.3"
4
+ VERSION = "0.6.0"
5
5
  end
data/metz-scan.gemspec CHANGED
@@ -18,21 +18,22 @@ Gem::Specification.new do |spec|
18
18
  spec.required_ruby_version = ">= 3.3"
19
19
 
20
20
  spec.metadata["source_code_uri"] = spec.homepage
21
+ spec.metadata["documentation_uri"] = "#{spec.homepage}/blob/main/README.md"
21
22
  spec.metadata["changelog_uri"] = "#{spec.homepage}/releases"
22
23
  spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
23
24
  spec.metadata["github_repo"] = "ssh://github.com/fuentesjr/metz-scan"
24
25
  spec.metadata["github_package_uri"] = "https://github.com/users/fuentesjr/packages/rubygems/package/metz-scan"
25
26
 
26
- spec.files = Dir.glob("lib/**/*", File::FNM_DOTMATCH).reject { |f| File.directory?(f) } +
27
- Dir.glob("bin/metz-scan").select { |f| File.file?(f) } +
28
- ["LICENSE", "metz-scan.gemspec"].select { |f| File.exist?(f) }
29
- # See rubocop-metz.gemspec: fail loudly if built from the wrong directory
30
- # rather than shipping a gem missing its own lib. Build from the repo root:
31
- # gem build metz-scan.gemspec
32
- unless spec.files.include?("lib/metz_scan.rb")
33
- raise "metz-scan.gemspec: lib/metz_scan.rb missing from packaged files; " \
34
- "build from the repo root (gem build metz-scan.gemspec)."
35
- end
27
+ # Resolve files from the gemspec directory so evaluation is CWD-independent.
28
+ # Do not raise when the entrypoint is absent: Dependabot evaluates gemspecs in
29
+ # a sparse tree that only materializes require_relative targets (#41). Package
30
+ # completeness is checked by release_metadata_test against the full checkout.
31
+ gem_root = __dir__
32
+ spec.files = Dir.glob("lib/**/*", File::FNM_DOTMATCH, base: gem_root)
33
+ .reject { |f| File.directory?(File.join(gem_root, f)) } +
34
+ Dir.glob("bin/metz-scan", base: gem_root)
35
+ .select { |f| File.file?(File.join(gem_root, f)) } +
36
+ ["LICENSE", "metz-scan.gemspec"].select { |f| File.exist?(File.join(gem_root, f)) }
36
37
  spec.bindir = "bin"
37
38
  spec.executables = ["metz-scan"]
38
39
  spec.require_paths = ["lib"]