rubocop-metz 0.5.2

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.

Potentially problematic release.


This version of rubocop-metz might be problematic. Click here for more details.

Files changed (98) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/lib/metz_scan/analyzers/contextual_node_walker.rb +82 -0
  4. data/lib/metz_scan/analyzers/cross_package_reference_metadata.rb +36 -0
  5. data/lib/metz_scan/analyzers/cross_package_reference_set.rb +33 -0
  6. data/lib/metz_scan/analyzers/implicit_context_pressure/ambient_context_collector.rb +54 -0
  7. data/lib/metz_scan/analyzers/implicit_context_pressure/ambient_context_reference.rb +37 -0
  8. data/lib/metz_scan/analyzers/implicit_context_pressure/current_attribute_collector.rb +73 -0
  9. data/lib/metz_scan/analyzers/implicit_context_pressure/reference_set.rb +37 -0
  10. data/lib/metz_scan/analyzers/implicit_context_pressure/thread_current_collector.rb +76 -0
  11. data/lib/metz_scan/analyzers/implicit_context_pressure/triage.rb +109 -0
  12. data/lib/metz_scan/analyzers/implicit_context_pressure.rb +139 -0
  13. data/lib/metz_scan/analyzers/inheritance_descendants/finding.rb +18 -0
  14. data/lib/metz_scan/analyzers/inheritance_descendants/metadata.rb +23 -0
  15. data/lib/metz_scan/analyzers/inheritance_descendants/root_kind.rb +58 -0
  16. data/lib/metz_scan/analyzers/inheritance_descendants/triage.rb +30 -0
  17. data/lib/metz_scan/analyzers/inheritance_descendants.rb +137 -0
  18. data/lib/metz_scan/analyzers/namespace_leak_pressure/finding.rb +20 -0
  19. data/lib/metz_scan/analyzers/namespace_leak_pressure/metadata.rb +40 -0
  20. data/lib/metz_scan/analyzers/namespace_leak_pressure/namespace.rb +41 -0
  21. data/lib/metz_scan/analyzers/namespace_leak_pressure/reference_collector.rb +50 -0
  22. data/lib/metz_scan/analyzers/namespace_leak_pressure/reference_set.rb +12 -0
  23. data/lib/metz_scan/analyzers/namespace_leak_pressure/shared_namespace_triage/exception_declaration.rb +117 -0
  24. data/lib/metz_scan/analyzers/namespace_leak_pressure/shared_namespace_triage.rb +69 -0
  25. data/lib/metz_scan/analyzers/namespace_leak_pressure.rb +139 -0
  26. data/lib/metz_scan/analyzers/occurrence.rb +28 -0
  27. data/lib/metz_scan/analyzers/package_dependency_pressure/finding.rb +20 -0
  28. data/lib/metz_scan/analyzers/package_dependency_pressure/metadata.rb +27 -0
  29. data/lib/metz_scan/analyzers/package_dependency_pressure/package_map.rb +11 -0
  30. data/lib/metz_scan/analyzers/package_dependency_pressure/reference_collector.rb +38 -0
  31. data/lib/metz_scan/analyzers/package_dependency_pressure/reference_set.rb +12 -0
  32. data/lib/metz_scan/analyzers/package_dependency_pressure/shared_dependency_triage.rb +124 -0
  33. data/lib/metz_scan/analyzers/package_dependency_pressure.rb +139 -0
  34. data/lib/metz_scan/analyzers/package_map.rb +81 -0
  35. data/lib/metz_scan/analyzers/project_analyzer_triage.rb +20 -0
  36. data/lib/metz_scan/analyzers/repeated_branching/branch_site_collector.rb +138 -0
  37. data/lib/metz_scan/analyzers/repeated_branching/branch_value.rb +27 -0
  38. data/lib/metz_scan/analyzers/repeated_branching/contextual_node_walker.rb +11 -0
  39. data/lib/metz_scan/analyzers/repeated_branching/decision_subject.rb +46 -0
  40. data/lib/metz_scan/analyzers/repeated_branching/predicate_chain.rb +58 -0
  41. data/lib/metz_scan/analyzers/repeated_branching/triage.rb +28 -0
  42. data/lib/metz_scan/analyzers/repeated_branching.rb +135 -0
  43. data/lib/metz_scan/analyzers/repeated_query_criteria/triage.rb +103 -0
  44. data/lib/metz_scan/analyzers/repeated_query_criteria.rb +285 -0
  45. data/lib/metz_scan/analyzers/ruby_file_enumerator.rb +37 -0
  46. data/lib/metz_scan/analyzers/service_soup/service_call_pattern.rb +53 -0
  47. data/lib/metz_scan/analyzers/service_soup/setup_workflow_triage.rb +48 -0
  48. data/lib/metz_scan/analyzers/service_soup/workflow_collector.rb +130 -0
  49. data/lib/metz_scan/analyzers/service_soup.rb +128 -0
  50. data/lib/metz_scan/analyzers/subclass_override_pressure/family_builder.rb +69 -0
  51. data/lib/metz_scan/analyzers/subclass_override_pressure/finding.rb +30 -0
  52. data/lib/metz_scan/analyzers/subclass_override_pressure/metadata.rb +74 -0
  53. data/lib/metz_scan/analyzers/subclass_override_pressure/method_body_facts.rb +100 -0
  54. data/lib/metz_scan/analyzers/subclass_override_pressure/triage.rb +106 -0
  55. data/lib/metz_scan/analyzers/subclass_override_pressure.rb +133 -0
  56. data/lib/metz_scan/calibration/project_analyzer_evidence_runner/artifact_writer.rb +51 -0
  57. data/lib/metz_scan/calibration/project_analyzer_evidence_runner/baseline_delta.rb +231 -0
  58. data/lib/metz_scan/calibration/project_analyzer_evidence_runner/collaborators.rb +76 -0
  59. data/lib/metz_scan/calibration/project_analyzer_evidence_runner/markdown_renderer.rb +353 -0
  60. data/lib/metz_scan/calibration/project_analyzer_evidence_runner/readiness_catalog.rb +123 -0
  61. data/lib/metz_scan/calibration/project_analyzer_evidence_runner/summary.rb +333 -0
  62. data/lib/metz_scan/calibration/project_analyzer_evidence_runner/target_set.rb +139 -0
  63. data/lib/metz_scan/calibration/project_analyzer_evidence_runner.rb +64 -0
  64. data/lib/metz_scan/calibration/rubydex_drift_formatter.rb +91 -0
  65. data/lib/metz_scan/cli.rb +120 -0
  66. data/lib/metz_scan/commands/explain.rb +128 -0
  67. data/lib/metz_scan/commands/project_analyzers.rb +105 -0
  68. data/lib/metz_scan/commands/report.rb +115 -0
  69. data/lib/metz_scan/commands/rules.rb +92 -0
  70. data/lib/metz_scan/commands/scan/auto_fix.rb +138 -0
  71. data/lib/metz_scan/commands/scan/compliance_scorecard.rb +124 -0
  72. data/lib/metz_scan/commands/scan/github_annotations_renderer.rb +55 -0
  73. data/lib/metz_scan/commands/scan/offense_extractor.rb +42 -0
  74. data/lib/metz_scan/commands/scan/project_analyzer_breakdown.rb +55 -0
  75. data/lib/metz_scan/commands/scan/project_analyzer_metadata.rb +78 -0
  76. data/lib/metz_scan/commands/scan/project_analyzer_offenses.rb +85 -0
  77. data/lib/metz_scan/commands/scan/project_analyzer_runner.rb +148 -0
  78. data/lib/metz_scan/commands/scan/project_analyzer_summary_aggregate_formatter.rb +74 -0
  79. data/lib/metz_scan/commands/scan/project_analyzer_summary_breakdown_formatter.rb +46 -0
  80. data/lib/metz_scan/commands/scan/project_analyzer_triage_formatter.rb +40 -0
  81. data/lib/metz_scan/commands/scan/project_analyzer_triage_priority.rb +38 -0
  82. data/lib/metz_scan/commands/scan/project_config_scope.rb +231 -0
  83. data/lib/metz_scan/commands/scan/runner.rb +199 -0
  84. data/lib/metz_scan/commands/scan/sarif_renderer.rb +103 -0
  85. data/lib/metz_scan/commands/scan/sarif_rule_descriptors.rb +82 -0
  86. data/lib/metz_scan/commands/scan/sarif_severity.rb +27 -0
  87. data/lib/metz_scan/commands/scan/target_ruby_version.rb +56 -0
  88. data/lib/metz_scan/commands/scan/text_renderer.rb +145 -0
  89. data/lib/metz_scan/commands/scan.rb +201 -0
  90. data/lib/metz_scan/project_index/null_backend.rb +35 -0
  91. data/lib/metz_scan/project_index/rubydex_backend/file_discovery.rb +33 -0
  92. data/lib/metz_scan/project_index/rubydex_backend/location_formatting.rb +36 -0
  93. data/lib/metz_scan/project_index/rubydex_backend/method_declarations.rb +118 -0
  94. data/lib/metz_scan/project_index/rubydex_backend.rb +133 -0
  95. data/lib/metz_scan/project_index.rb +109 -0
  96. data/lib/metz_scan/version.rb +5 -0
  97. data/lib/metz_scan.rb +5 -0
  98. metadata +185 -0
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "optparse"
5
+
6
+ require_relative "../version"
7
+ require_relative "scan/compliance_scorecard"
8
+ require_relative "scan/runner"
9
+ require_relative "scan/project_analyzer_runner"
10
+ require_relative "scan/text_renderer"
11
+ require_relative "scan/sarif_renderer"
12
+ require_relative "scan/github_annotations_renderer"
13
+ require_relative "scan/auto_fix"
14
+
15
+ module MetzScan
16
+ module Commands
17
+ class ScanOptions
18
+ USAGE = "Usage: metz-scan scan PATH... [--format text|json|sarif|gh-annotations] " \
19
+ "[--all-cops] [--project-analyzers] [--auto-fix [--unsafe] [--dry-run]]"
20
+ VALID_FORMATS = %w[text json sarif gh-annotations].freeze
21
+ DEFAULT_FORMAT = "text"
22
+
23
+ Options = Struct.new(:format, :paths, :all_cops, :project_analyzers, :auto_fix, :unsafe, :dry_run, :help,
24
+ keyword_init: true)
25
+
26
+ def self.parse(argv)
27
+ new.parse(argv)
28
+ end
29
+
30
+ def self.help
31
+ new.help
32
+ end
33
+
34
+ def parse(argv)
35
+ flags = default_flags
36
+ paths = option_parser(flags).parse(argv)
37
+ Options.new(**flags, paths: paths)
38
+ end
39
+
40
+ def help
41
+ option_parser(default_flags).help
42
+ end
43
+
44
+ private
45
+
46
+ def default_flags
47
+ { format: DEFAULT_FORMAT, all_cops: false, project_analyzers: false, auto_fix: false, unsafe: false,
48
+ dry_run: false, help: false }
49
+ end
50
+
51
+ def option_parser(flags)
52
+ OptionParser.new(USAGE) { |opts| configure_parser(opts, flags) }
53
+ end
54
+
55
+ def configure_parser(opts, flags)
56
+ configure_format_parser(opts, flags)
57
+ configure_cop_selection_parser(opts, flags)
58
+ configure_project_analyzer_parser(opts, flags)
59
+ configure_auto_fix_parser(opts, flags)
60
+ configure_help_parser(opts, flags)
61
+ end
62
+
63
+ def configure_format_parser(opts, flags)
64
+ opts.on("--format FORMAT", "Output format: text (default), json, sarif, gh-annotations") do |format|
65
+ flags[:format] = format
66
+ end
67
+ end
68
+
69
+ def configure_cop_selection_parser(opts, flags)
70
+ opts.on("--all-cops", "Run the full RuboCop suite instead of Metz/* cops by default") do
71
+ flags[:all_cops] = true
72
+ end
73
+ end
74
+
75
+ def configure_project_analyzer_parser(opts, flags)
76
+ opts.on("--project-analyzers", "Include all opt-in project analyzer findings") do
77
+ flags[:project_analyzers] = true
78
+ end
79
+ end
80
+
81
+ def configure_auto_fix_parser(opts, flags)
82
+ opts.on("--auto-fix", "Apply RuboCop's safe fixes (delegates to rubocop -a)") { flags[:auto_fix] = true }
83
+ opts.on("--unsafe", "With --auto-fix, also apply unsafe fixes (rubocop -A)") { flags[:unsafe] = true }
84
+ opts.on("--dry-run", "With --auto-fix, print diff without modifying files") { flags[:dry_run] = true }
85
+ end
86
+
87
+ def configure_help_parser(opts, flags)
88
+ opts.on("-h", "--help", "Show this help message and exit") { flags[:help] = true }
89
+ end
90
+ end
91
+
92
+ class Scan
93
+ USAGE = ScanOptions::USAGE
94
+ VALID_FORMATS = ScanOptions::VALID_FORMATS
95
+ RENDERERS = { "sarif" => SarifRenderer, "gh-annotations" => GithubAnnotationsRenderer }.freeze
96
+
97
+ def self.run(argv, stdout:, stderr:)
98
+ new(stdout: stdout, stderr: stderr).run(argv)
99
+ end
100
+
101
+ def initialize(stdout:, stderr:)
102
+ @stdout = stdout
103
+ @stderr = stderr
104
+ end
105
+
106
+ def run(argv)
107
+ handle_options(parse_options(argv))
108
+ rescue OptionParser::ParseError => e
109
+ parser_error(e)
110
+ rescue Runner::Error => e
111
+ runner_error(e)
112
+ end
113
+
114
+ private
115
+
116
+ attr_reader :stdout, :stderr
117
+
118
+ def handle_options(options)
119
+ return print_help if options.help
120
+
121
+ validate(options) || dispatch(options)
122
+ end
123
+
124
+ def parse_options(argv)
125
+ ScanOptions.parse(argv)
126
+ end
127
+
128
+ def validate(options)
129
+ return missing_path_arg if options.paths.empty?
130
+ return missing_paths(options.paths) unless options.paths.all? { |p| File.exist?(p) }
131
+ return nil if options.auto_fix
132
+
133
+ invalid_format(options.format) unless VALID_FORMATS.include?(options.format)
134
+ end
135
+
136
+ def dispatch(options)
137
+ return AutoFix.new(stdout: stdout, stderr: stderr).run(options) if options.auto_fix
138
+
139
+ scan(options)
140
+ end
141
+
142
+ def scan(options)
143
+ parsed = Runner.invoke(options.paths, all_cops: options.all_cops, stderr: stderr)
144
+ merge_project_analyzers(parsed, options)
145
+ add_compliance_summary(parsed)
146
+ render(parsed, options.format)
147
+ Runner.exit_code_for(parsed)
148
+ end
149
+
150
+ def add_compliance_summary(parsed)
151
+ ComplianceScorecard.add_to_summary!(parsed)
152
+ end
153
+
154
+ def merge_project_analyzers(parsed, options)
155
+ ProjectAnalyzerRunner.merge!(parsed, options.paths, **project_analyzer_options(options))
156
+ end
157
+
158
+ def project_analyzer_options(options)
159
+ { default_output: !options.project_analyzers }
160
+ end
161
+
162
+ def render(parsed, format)
163
+ return stdout.puts JSON.generate(parsed) if format == "json"
164
+
165
+ RENDERERS.fetch(format, TextRenderer).new(stdout, parsed).render
166
+ end
167
+
168
+ def parser_error(err)
169
+ stderr.puts "metz-scan scan: #{err.message}"
170
+ stderr.puts USAGE
171
+ 1
172
+ end
173
+
174
+ def print_help
175
+ stdout.puts ScanOptions.help
176
+ 0
177
+ end
178
+
179
+ def missing_path_arg
180
+ stderr.puts "metz-scan scan: missing PATH argument."
181
+ stderr.puts USAGE
182
+ 1
183
+ end
184
+
185
+ def missing_paths(paths)
186
+ paths.reject { |p| File.exist?(p) }.each { |p| stderr.puts "metz-scan scan: no such file or directory: #{p}" }
187
+ 1
188
+ end
189
+
190
+ def invalid_format(fmt)
191
+ stderr.puts "metz-scan scan: invalid --format '#{fmt}'. Valid formats: text, json, sarif, gh-annotations."
192
+ 1
193
+ end
194
+
195
+ def runner_error(err)
196
+ stderr.puts "metz-scan scan: RuboCop failed: #{err.message}"
197
+ 2
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetzScan
4
+ class ProjectIndex
5
+ class NullBackend
6
+ def initialize(reason:)
7
+ @reason = reason
8
+ end
9
+
10
+ attr_reader :reason
11
+
12
+ def name = :null
13
+
14
+ def available? = false
15
+
16
+ def indexed_files = []
17
+
18
+ def index_errors = []
19
+
20
+ def diagnostics = []
21
+
22
+ def declarations = []
23
+
24
+ def method_declarations = []
25
+
26
+ def documents = []
27
+
28
+ def descendants_of(_name) = []
29
+
30
+ def constant_references_to(_name) = []
31
+
32
+ def search(_query) = []
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetzScan
4
+ class ProjectIndex
5
+ class RubydexBackend
6
+ module FileDiscovery
7
+ RUBY_GLOB = "**/*.rb"
8
+ private_constant :RUBY_GLOB
9
+
10
+ def ruby_files_for(paths)
11
+ paths.flat_map { |path| ruby_files_under(path) }.uniq.sort
12
+ end
13
+
14
+ def workspace_path_for(paths)
15
+ expanded = File.expand_path(paths.first || Dir.pwd)
16
+ return expanded if File.directory?(expanded)
17
+
18
+ File.dirname(expanded)
19
+ end
20
+
21
+ private
22
+
23
+ def ruby_files_under(path)
24
+ expanded = File.expand_path(path)
25
+ return Dir.glob(File.join(expanded, RUBY_GLOB)) if File.directory?(expanded)
26
+ return [expanded] if File.file?(expanded) && File.extname(expanded) == ".rb"
27
+
28
+ []
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module MetzScan
6
+ class ProjectIndex
7
+ class RubydexBackend
8
+ module LocationFormatting
9
+ def path_from_location(location)
10
+ return unless location
11
+
12
+ path_from_file_location(location) || path_from_uri_location(location) || location.to_s
13
+ end
14
+
15
+ def path_from_file_location(location)
16
+ location.to_file_path if location.respond_to?(:to_file_path)
17
+ rescue StandardError
18
+ nil
19
+ end
20
+
21
+ def path_from_uri_location(location)
22
+ path_from_uri(location.uri) if location.respond_to?(:uri)
23
+ end
24
+
25
+ def path_from_uri(uri)
26
+ string = uri.to_s
27
+ return URI(string).path if string.start_with?("file:")
28
+
29
+ string
30
+ rescue URI::InvalidURIError
31
+ string
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetzScan
4
+ class ProjectIndex
5
+ class RubydexBackend
6
+ module MethodDeclarations
7
+ def method_declarations
8
+ method_declaration_entries.sort_by do |declaration|
9
+ [declaration.owner_name.to_s, declaration.method_identity.to_s, declaration.path.to_s,
10
+ declaration.line.to_i]
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def method_declaration_entries
17
+ graph.declarations.select { |declaration| method_declaration?(declaration) }
18
+ .filter_map { |declaration| method_declaration_for(declaration) }
19
+ end
20
+
21
+ def method_declaration?(declaration)
22
+ defined?(Rubydex::Method) && declaration.instance_of?(Rubydex::Method)
23
+ end
24
+
25
+ def method_declaration_for(declaration)
26
+ attributes = method_declaration_attributes(declaration)
27
+ MethodDeclaration.new(**attributes) if attributes
28
+ end
29
+
30
+ def method_declaration_attributes(declaration)
31
+ owner_attributes = method_owner_attributes(declaration)
32
+ return unless owner_attributes
33
+
34
+ method_name = method_name_for(owner_attributes.fetch(:signature))
35
+ identity_attributes(declaration, owner_attributes, method_name)
36
+ .merge(location_attributes(declaration))
37
+ end
38
+
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
42
+ end
43
+
44
+ def method_owner_attributes(declaration)
45
+ raw_owner_name = raw_owner_name_for(declaration)
46
+ signature = signature_for(declaration.name, raw_owner_name)
47
+ return unless raw_owner_name && signature
48
+
49
+ { owner_name: normalized_owner_name(raw_owner_name), signature: signature,
50
+ receiver_kind: receiver_kind_for(raw_owner_name) }
51
+ end
52
+
53
+ def identity_attributes(declaration, owner_attributes, method_name)
54
+ 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) }
58
+ end
59
+
60
+ def location_attributes(declaration)
61
+ location = definition_display_location(declaration)
62
+ { path: path_from_location(location), line: line_from_location(location),
63
+ column: column_from_location(location) }
64
+ end
65
+
66
+ def raw_owner_name_for(declaration)
67
+ declaration.owner.name if declaration.respond_to?(:owner) && declaration.owner.respond_to?(:name)
68
+ end
69
+
70
+ def signature_for(name, owner_name)
71
+ name.to_s.delete_prefix("#{owner_name}#")
72
+ .delete_prefix("#{owner_name}.")
73
+ .delete_prefix("#{owner_name}::")
74
+ end
75
+
76
+ def method_name_for(signature)
77
+ signature.to_s.sub(/\(.*\)\z/, "")
78
+ end
79
+
80
+ def receiver_kind_for(raw_owner_name)
81
+ singleton_owner_name?(raw_owner_name) ? "singleton" : "instance"
82
+ end
83
+
84
+ def normalized_owner_name(raw_owner_name)
85
+ singleton_owner_name?(raw_owner_name) ? singleton_owner_name(raw_owner_name) : raw_owner_name
86
+ end
87
+
88
+ def singleton_owner_name?(raw_owner_name)
89
+ singleton_owner_name(raw_owner_name)
90
+ end
91
+
92
+ def singleton_owner_name(raw_owner_name)
93
+ owner_name, singleton_tail = raw_owner_name.to_s.split("::<", 2)
94
+ return unless singleton_tail&.delete_suffix(">") == owner_name
95
+
96
+ owner_name
97
+ end
98
+
99
+ def method_identity_for(receiver_kind, method_name)
100
+ "#{receiver_kind}:#{method_name}"
101
+ end
102
+
103
+ def definition_display_location(declaration)
104
+ definition = declaration.definitions.first if declaration.respond_to?(:definitions)
105
+ display_location_for(definition)
106
+ end
107
+
108
+ def line_from_location(location)
109
+ location.start_line if location.respond_to?(:start_line)
110
+ end
111
+
112
+ def column_from_location(location)
113
+ location.start_column if location.respond_to?(:start_column)
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rubydex_backend/file_discovery"
4
+ require_relative "rubydex_backend/location_formatting"
5
+ require_relative "rubydex_backend/method_declarations"
6
+
7
+ module MetzScan
8
+ class ProjectIndex
9
+ class RubydexBackend
10
+ include LocationFormatting
11
+ include MethodDeclarations
12
+ extend FileDiscovery
13
+
14
+ DECLARATION_KINDS = { "Rubydex::Class" => :class, "Rubydex::Module" => :module }.freeze
15
+
16
+ def self.available?
17
+ require "rubydex"
18
+ true
19
+ rescue LoadError
20
+ false
21
+ end
22
+
23
+ def self.unavailable_reason
24
+ "rubydex is not installed; enable the optional rubydex bundle group first"
25
+ end
26
+
27
+ def self.build(paths, workspace: false)
28
+ require "rubydex"
29
+
30
+ files = ruby_files_for(paths)
31
+ graph = Rubydex::Graph.new(workspace_path: workspace_path_for(paths))
32
+ index_errors = index_graph(graph, files, workspace)
33
+
34
+ new(graph: graph, indexed_files: files, index_errors: index_errors)
35
+ end
36
+
37
+ def self.index_graph(graph, files, workspace)
38
+ errors = workspace ? graph.index_workspace : graph.index_all(files)
39
+ graph.resolve
40
+ errors
41
+ end
42
+ private_class_method :index_graph
43
+
44
+ def initialize(graph:, indexed_files:, index_errors:)
45
+ @graph = graph
46
+ @indexed_files = indexed_files
47
+ @index_errors = Array(index_errors)
48
+ end
49
+
50
+ attr_reader :graph, :indexed_files, :index_errors
51
+
52
+ def name = :rubydex
53
+
54
+ def available? = true
55
+
56
+ def reason = nil
57
+
58
+ def diagnostics
59
+ graph.diagnostics.to_a
60
+ end
61
+
62
+ def declarations
63
+ entries = graph.declarations.map do |declaration|
64
+ Declaration.new(name: declaration.name, path: definition_path(declaration),
65
+ kind: declaration_kind(declaration))
66
+ end
67
+
68
+ entries.sort_by { |declaration| [declaration.name.to_s, declaration.path.to_s] }
69
+ end
70
+
71
+ def documents
72
+ graph.documents.map { |document| path_from_uri(document.uri) }.compact.sort
73
+ end
74
+
75
+ def descendants_of(name)
76
+ declaration = graph[name]
77
+ return [] unless declaration.respond_to?(:descendants)
78
+
79
+ declaration.descendants.map(&:name).reject { |descendant| descendant == name }.sort
80
+ end
81
+
82
+ def constant_references_to(name)
83
+ declaration = graph[name]
84
+ return [] unless declaration.respond_to?(:references)
85
+
86
+ declaration.references.map { |reference| normalized_reference(reference) }.compact.sort_by do |reference|
87
+ [reference.path.to_s, reference.line.to_i, reference.column.to_i, reference.name.to_s]
88
+ end
89
+ end
90
+
91
+ def search(query)
92
+ graph.search(query).map(&:name).sort
93
+ end
94
+
95
+ private
96
+
97
+ def definition_path(declaration)
98
+ definition = declaration.definitions.first if declaration.respond_to?(:definitions)
99
+ path_from_display_location(definition)
100
+ end
101
+
102
+ def declaration_kind(declaration) = DECLARATION_KINDS[declaration.class.name]
103
+
104
+ def path_from_display_location(definition)
105
+ path_from_location(display_location_for(definition))
106
+ end
107
+
108
+ def display_location_for(definition)
109
+ location = definition&.location
110
+ return location.to_display if location.respond_to?(:to_display)
111
+
112
+ location
113
+ end
114
+
115
+ def normalized_reference(reference)
116
+ location = reference.location.to_display
117
+ Reference.new(**reference_attributes(reference, location))
118
+ end
119
+
120
+ def reference_attributes(reference, location)
121
+ { name: reference_name(reference), path: path_from_location(location),
122
+ line: location.start_line, column: location.start_column }
123
+ end
124
+
125
+ def reference_name(reference)
126
+ return reference.declaration.name if reference.respond_to?(:declaration)
127
+ return reference.name if reference.respond_to?(:name)
128
+
129
+ nil
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "project_index/null_backend"
4
+ require_relative "project_index/rubydex_backend"
5
+
6
+ module MetzScan
7
+ # Thin facade over optional project-wide indexes used by future analyzers.
8
+ class ProjectIndex
9
+ Declaration = Struct.new(:name, :path, :kind, keyword_init: true)
10
+ MethodDeclaration = Struct.new(:name, :owner_name, :method_name, :signature, :path, :line, :column,
11
+ :receiver_kind, :method_identity, keyword_init: true)
12
+ Reference = Struct.new(:name, :path, :line, :column, keyword_init: true)
13
+
14
+ class UnknownBackendError < StandardError; end
15
+ class UnavailableBackendError < StandardError; end
16
+
17
+ BACKENDS = %i[auto null rubydex].freeze
18
+ private_constant :BACKENDS
19
+
20
+ def self.build(paths, backend: :auto, workspace: false)
21
+ backend = backend.to_sym
22
+ raise UnknownBackendError, "unknown project index backend: #{backend}" unless BACKENDS.include?(backend)
23
+
24
+ new(build_backend(Array(paths), backend, workspace))
25
+ end
26
+
27
+ def self.build_backend(paths, backend, workspace)
28
+ case backend
29
+ when :auto then auto_backend(paths, workspace)
30
+ when :null then null_backend("project index disabled")
31
+ when :rubydex then rubydex_backend(paths, workspace)
32
+ end
33
+ end
34
+ private_class_method :build_backend
35
+
36
+ def self.auto_backend(paths, workspace)
37
+ return rubydex_backend(paths, workspace) if RubydexBackend.available?
38
+
39
+ null_backend(RubydexBackend.unavailable_reason)
40
+ end
41
+ private_class_method :auto_backend
42
+
43
+ def self.rubydex_backend(paths, workspace)
44
+ raise UnavailableBackendError, RubydexBackend.unavailable_reason unless RubydexBackend.available?
45
+
46
+ RubydexBackend.build(paths, workspace: workspace)
47
+ end
48
+ private_class_method :rubydex_backend
49
+
50
+ def self.null_backend(reason)
51
+ NullBackend.new(reason: reason)
52
+ end
53
+ private_class_method :null_backend
54
+
55
+ def initialize(backend)
56
+ @backend = backend
57
+ end
58
+
59
+ attr_reader :backend
60
+
61
+ def backend_name
62
+ backend.name
63
+ end
64
+
65
+ def available?
66
+ backend.available?
67
+ end
68
+
69
+ def reason
70
+ backend.reason
71
+ end
72
+
73
+ def indexed_files
74
+ backend.indexed_files
75
+ end
76
+
77
+ def index_errors
78
+ backend.index_errors
79
+ end
80
+
81
+ def diagnostics
82
+ backend.diagnostics
83
+ end
84
+
85
+ def declarations
86
+ backend.declarations
87
+ end
88
+
89
+ def method_declarations
90
+ backend.method_declarations
91
+ end
92
+
93
+ def documents
94
+ backend.documents
95
+ end
96
+
97
+ def descendants_of(name)
98
+ backend.descendants_of(name)
99
+ end
100
+
101
+ def constant_references_to(name)
102
+ backend.constant_references_to(name)
103
+ end
104
+
105
+ def search(query)
106
+ backend.search(query)
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetzScan
4
+ VERSION = "0.5.2"
5
+ end
data/lib/metz_scan.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "metz_scan/version"
4
+ require_relative "metz_scan/cli"
5
+ require_relative "metz_scan/project_index"